Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/flo-bit/atmo-watch. atmo.watch
Something went wrong. Try again.
5.3 kB · 162 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163import { command, getRequestEvent } from '$app/server';import { error } from '@sveltejs/kit';import { createTID } from '@svelte-atproto/oauth/helper';import * as v from 'valibot';import { contrail } from '$lib/contrail-client.server';import type { Main as VideoRecord } from '$lib/contrail/types/types/watch/atmo/alpha/video';import { parseYouTubeVideo, VIDEO_TYPES } from '$lib/videos';
const VIDEO_COLLECTION = 'watch.atmo.alpha.video';
const targetSchema = v.pipe( v.object({ creativeWorkType: v.picklist(['movie', 'tv_show', 'tv_season', 'tv_episode']), tmdbId: v.pipe(v.number(), v.integer(), v.minValue(1)), tmdbTvSeriesId: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))), seasonNumber: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))), episodeNumber: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))), title: v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(1000)) }), v.check((target) => { switch (target.creativeWorkType) { case 'movie': case 'tv_show': return ( target.tmdbTvSeriesId === undefined && target.seasonNumber === undefined && target.episodeNumber === undefined ); case 'tv_season': return ( target.tmdbTvSeriesId !== undefined && target.seasonNumber !== undefined && target.episodeNumber === undefined ); case 'tv_episode': return ( target.tmdbTvSeriesId !== undefined && target.seasonNumber !== undefined && target.episodeNumber !== undefined ); } }, 'The movie or TV target is incomplete'));
const submitVideoSchema = v.object({ target: targetSchema, videoUrl: v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(2048)), videoType: v.picklist(VIDEO_TYPES), containsSpoilers: v.boolean()});
function responseMessage(data: unknown, fallback: string) { if (data && typeof data === 'object' && 'message' in data && typeof data.message === 'string') { return data.message; } return fallback;}
function optionalHttpsUrl(value: unknown) { if (typeof value !== 'string' || value.length > 2048) return undefined; try { const url = new URL(value); return url.protocol === 'https:' ? url.toString() : undefined; } catch { return undefined; }}
async function getYouTubeMetadata(videoUrl: string) { const endpoint = new URL('https://www.youtube.com/oembed'); endpoint.searchParams.set('url', videoUrl); endpoint.searchParams.set('format', 'json');
let response: Response; try { response = await fetch(endpoint, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(10_000) }); } catch { error(502, 'Could not reach YouTube to load the video details'); }
if ([400, 401, 403, 404].includes(response.status)) { error(422, 'YouTube could not find a public video at that link'); } if (!response.ok) error(502, 'YouTube could not load the video details');
let data: unknown; try { data = await response.json(); } catch { error(502, 'YouTube returned invalid video details'); } if (!data || typeof data !== 'object') error(502, 'YouTube returned invalid video details');
const title = 'title' in data && typeof data.title === 'string' ? data.title.trim() : ''; if (!title || title.length > 500) error(502, 'YouTube returned an invalid video title');
const channelName = 'author_name' in data && typeof data.author_name === 'string' ? data.author_name.trim().slice(0, 500) : ''; const channelUrl = 'author_url' in data ? optionalHttpsUrl(data.author_url) : undefined; const thumbnailUrl = 'thumbnail_url' in data ? optionalHttpsUrl(data.thumbnail_url) : undefined;
return { videoTitle: title, ...(channelName ? { channelName } : {}), ...(channelUrl ? { channelUrl } : {}), ...(thumbnailUrl ? { thumbnailUrl } : {}) };}
export const submitVideo = command( submitVideoSchema, async ({ target, videoUrl, videoType, containsSpoilers }) => { const { locals } = getRequestEvent(); if (!locals.client || !locals.did) error(401, 'Log in to submit a video');
const youtube = parseYouTubeVideo(videoUrl); if (!youtube) error(422, 'Enter a valid YouTube video link'); const youtubeMetadata = await getYouTubeMetadata(youtube.videoUrl);
const identifiers = { tmdbId: String(target.tmdbId), ...(target.tmdbTvSeriesId !== undefined ? { tmdbTvSeriesId: String(target.tmdbTvSeriesId) } : {}), ...(target.seasonNumber !== undefined ? { seasonNumber: target.seasonNumber } : {}), ...(target.episodeNumber !== undefined ? { episodeNumber: target.episodeNumber } : {}) }; const record = { $type: VIDEO_COLLECTION, videoUrl: youtube.videoUrl, youtubeId: youtube.youtubeId, ...youtubeMetadata, videoType, creativeWorkType: target.creativeWorkType, identifiers, title: target.title, containsSpoilers, createdAt: new Date().toISOString() } as VideoRecord;
const response = await contrail .authenticated(locals.client) .post('com.atproto.repo.createRecord', { input: { repo: locals.did, collection: VIDEO_COLLECTION, rkey: createTID(), record } }); if (!response.ok) { error(response.status, responseMessage(response.data, 'Could not submit the video')); }
return { uri: response.data.uri }; });