From feddf9c215791d2e2784357884e7495eb4e9f9de Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 26 Dec 2023 17:00:35 +0100 Subject: [PATCH] Refactored dev-cli --- package.json | 2 +- src/dev-cli.ts | 423 ---------------------------------------- src/dev-cli/config.ts | 12 ++ src/dev-cli/index.ts | 227 +++++++++++++++++++++ src/dev-cli/logging.ts | 5 + src/dev-cli/tmdb.ts | 95 +++++++++ src/dev-cli/validate.ts | 90 +++++++++ src/providers/base.ts | 39 +++- 8 files changed, 463 insertions(+), 430 deletions(-) delete mode 100644 src/dev-cli.ts create mode 100644 src/dev-cli/config.ts create mode 100644 src/dev-cli/index.ts create mode 100644 src/dev-cli/logging.ts create mode 100644 src/dev-cli/tmdb.ts create mode 100644 src/dev-cli/validate.ts diff --git a/package.json b/package.json index e93259a..7dbd0b3 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "homepage": "https://providers.docs.movie-web.app/", "scripts": { "build": "vite build && tsc --noEmit", + "cli": "ts-node ./src/dev-cli/index.ts", "test": "vitest run", - "test:dev": "ts-node ./src/dev-cli.ts", "test:watch": "vitest", "test:integration": "node ./tests/cjs && node ./tests/esm && node ./tests/browser", "test:coverage": "vitest run --coverage", diff --git a/src/dev-cli.ts b/src/dev-cli.ts deleted file mode 100644 index 54d663a..0000000 --- a/src/dev-cli.ts +++ /dev/null @@ -1,423 +0,0 @@ -/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ - -import util from 'node:util'; - -import { program } from 'commander'; -import dotenv from 'dotenv'; -import { prompt } from 'enquirer'; -import nodeFetch from 'node-fetch'; -import Spinnies from 'spinnies'; - -import { MetaOutput, MovieMedia, ProviderControls, ShowMedia, makeProviders, makeStandardFetcher, targets } from '.'; - -dotenv.config(); - -type ProviderSourceAnswers = { - id: string; - type: string; -}; - -type EmbedSourceAnswers = { - url: string; -}; - -type CommonAnswers = { - fetcher: string; - source: string; -}; - -type ShowAnswers = { - season: string; - episode: string; -}; - -type CommandLineArguments = { - fetcher: string; - sourceId: string; - tmdbId: string; - type: string; - season: string; - episode: string; - url: string; -}; - -const TMDB_API_KEY = process.env.MOVIE_WEB_TMDB_API_KEY ?? ''; - -if (!TMDB_API_KEY?.trim()) { - throw new Error('Missing MOVIE_WEB_TMDB_API_KEY environment variable'); -} - -function logDeepObject(object: Record) { - console.log(util.inspect(object, { showHidden: false, depth: null, colors: true })); -} - -function getAllSources() { - // * The only way to get a list of all sources is to - // * create all these things. Maybe this should change - const providers = makeProviders({ - fetcher: makeStandardFetcher(nodeFetch), - target: targets.NATIVE, - }); - - const combined = [...providers.listSources(), ...providers.listEmbeds()]; - - // * Remove dupes - const map = new Map(combined.map((source) => [source.id, source])); - - return [...map.values()]; -} - -// * Defined here cuz ESLint didn't like the order these were defined in -const sources = getAllSources(); - -async function makeTMDBRequest(url: string): Promise { - const headers: { - accept: 'application/json'; - authorization?: string; - } = { - accept: 'application/json', - }; - - // * Used to get around ESLint - // * Assignment to function parameter 'url'. eslint (no-param-reassign) - let requestURL = url; - - // * JWT keys always start with ey and are ONLY valid as a header. - // * All other keys are ONLY valid as a query param. - // * Thanks TMDB. - if (TMDB_API_KEY.startsWith('ey')) { - headers.authorization = `Bearer ${TMDB_API_KEY}`; - } else { - requestURL += `?api_key=${TMDB_API_KEY}`; - } - - return fetch(requestURL, { - method: 'GET', - headers, - }); -} - -async function getMovieMediaDetails(id: string): Promise { - const response = await makeTMDBRequest(`https://api.themoviedb.org/3/movie/${id}`); - const movie = await response.json(); - - if (movie.success === false) { - throw new Error(movie.status_message); - } - - if (!movie.release_date) { - throw new Error(`${movie.title} has no release_date. Assuming unreleased`); - } - - return { - type: 'movie', - title: movie.title, - releaseYear: Number(movie.release_date.split('-')[0]), - tmdbId: id, - }; -} - -async function getShowMediaDetails(id: string, seasonNumber: string, episodeNumber: string): Promise { - // * TV shows require the TMDB ID for the series, season, and episode - // * and the name of the series. Needs multiple requests - let response = await makeTMDBRequest(`https://api.themoviedb.org/3/tv/${id}`); - const series = await response.json(); - - if (series.success === false) { - throw new Error(series.status_message); - } - - if (!series.first_air_date) { - throw new Error(`${series.name} has no first_air_date. Assuming unaired`); - } - - response = await makeTMDBRequest(`https://api.themoviedb.org/3/tv/${id}/season/${seasonNumber}`); - const season = await response.json(); - - if (season.success === false) { - throw new Error(season.status_message); - } - - response = await makeTMDBRequest( - `https://api.themoviedb.org/3/tv/${id}/season/${seasonNumber}/episode/${episodeNumber}`, - ); - const episode = await response.json(); - - if (episode.success === false) { - throw new Error(episode.status_message); - } - - return { - type: 'show', - title: series.name, - releaseYear: Number(series.first_air_date.split('-')[0]), - tmdbId: id, - episode: { - number: episode.episode_number, - tmdbId: episode.id, - }, - season: { - number: season.season_number, - tmdbId: season.id, - }, - }; -} - -function joinMediaTypes(mediaTypes: string[] | undefined) { - if (mediaTypes) { - const formatted = mediaTypes - .map((type: string) => { - return `${type[0].toUpperCase() + type.substring(1).toLowerCase()}s`; - }) - .join(' / '); - - return `(${formatted})`; - } - return ''; // * Embed sources pass through here too -} - -async function runScraper(providers: ProviderControls, source: MetaOutput, options: CommandLineArguments) { - const spinnies = new Spinnies(); - - if (source.type === 'embed') { - spinnies.add('scrape', { text: `Running ${source.name} scraper on ${options.url}` }); - try { - const result = await providers.runEmbedScraper({ - url: options.url, - id: source.id, - }); - spinnies.succeed('scrape', { text: 'Done!' }); - logDeepObject(result); - } catch (error) { - let message = 'Unknown error'; - if (error instanceof Error) { - message = error.message; - } - - spinnies.fail('scrape', { text: `ERROR: ${message}` }); - } - } else { - let media; - - if (options.type === 'movie') { - media = await getMovieMediaDetails(options.tmdbId); - } else { - media = await getShowMediaDetails(options.tmdbId, options.season, options.episode); - } - - spinnies.add('scrape', { text: `Running ${source.name} scraper on ${media.title}` }); - try { - const result = await providers.runSourceScraper({ - media, - id: source.id, - }); - spinnies.succeed('scrape', { text: 'Done!' }); - logDeepObject(result); - } catch (error) { - let message = 'Unknown error'; - if (error instanceof Error) { - message = error.message; - } - - spinnies.fail('scrape', { text: `ERROR: ${message}` }); - } - } -} - -async function processOptions(options: CommandLineArguments) { - if (options.fetcher !== 'node-fetch' && options.fetcher !== 'native') { - throw new Error("Fetcher must be either 'native' or 'node-fetch'"); - } - - if (!options.sourceId.trim()) { - throw new Error('Source ID must be provided'); - } - - const source = sources.find(({ id }) => id === options.sourceId); - - if (!source) { - throw new Error('Invalid source ID. No source found'); - } - - if (source.type === 'embed' && !options.url.trim()) { - throw new Error('Must provide an embed URL for embed sources'); - } - - if (source.type === 'source') { - if (!options.tmdbId.trim()) { - throw new Error('Must provide a TMDB ID for provider sources'); - } - - if (Number.isNaN(Number(options.tmdbId)) || Number(options.tmdbId) < 0) { - throw new Error('TMDB ID must be a number greater than 0'); - } - - if (!options.type.trim()) { - throw new Error('Must provide a type for provider sources'); - } - - if (options.type !== 'movie' && options.type !== 'show') { - throw new Error("Invalid media type. Must be either 'movie' or 'show'"); - } - - if (options.type === 'show') { - if (!options.season.trim()) { - throw new Error('Must provide a season number for TV shows'); - } - - if (!options.episode.trim()) { - throw new Error('Must provide an episode number for TV shows'); - } - - if (Number.isNaN(Number(options.season)) || Number(options.season) <= 0) { - throw new Error('Season number must be a number greater than 0'); - } - - if (Number.isNaN(Number(options.episode)) || Number(options.episode) <= 0) { - throw new Error('Episode number must be a number greater than 0'); - } - } - } - - let fetcher; - - if (options.fetcher === 'native') { - fetcher = makeStandardFetcher(fetch); - } else { - fetcher = makeStandardFetcher(nodeFetch); - } - - const providers = makeProviders({ - fetcher, - target: targets.NATIVE, - }); - - await runScraper(providers, source, options); -} - -async function runQuestions() { - const options = { - fetcher: 'node-fetch', - sourceId: '', - tmdbId: '', - type: 'movie', - season: '0', - episode: '0', - url: '', - }; - - const answers = await prompt([ - { - type: 'select', - name: 'fetcher', - message: 'Select a fetcher', - choices: [ - { - message: 'Native', - name: 'native', - }, - { - message: 'Node fetch', - name: 'node-fetch', - }, - ], - }, - { - type: 'select', - name: 'source', - message: 'Select a source', - choices: sources.map((source) => ({ - message: `[${source.type.toLocaleUpperCase()}] ${source.name} ${joinMediaTypes(source.mediaTypes)}`.trim(), - name: source.id, - })), - }, - ]); - - options.fetcher = answers.fetcher; - options.sourceId = answers.source; - - const source = sources.find(({ id }) => id === answers.source); - - if (!source) { - throw new Error(`No source with ID ${answers.source} found`); - } - - if (source.type === 'embed') { - const sourceAnswers = await prompt([ - { - type: 'input', - name: 'url', - message: 'Embed URL', - }, - ]); - - options.url = sourceAnswers.url; - } else { - const sourceAnswers = await prompt([ - { - type: 'input', - name: 'id', - message: 'TMDB ID', - }, - { - type: 'select', - name: 'type', - message: 'Media type', - choices: [ - { - message: 'Movie', - name: 'movie', - }, - { - message: 'TV Show', - name: 'show', - }, - ], - }, - ]); - - options.tmdbId = sourceAnswers.id; - options.type = sourceAnswers.type; - - if (sourceAnswers.type === 'show') { - const seriesAnswers = await prompt([ - { - type: 'input', - name: 'season', - message: 'Season', - }, - { - type: 'input', - name: 'episode', - message: 'Episode', - }, - ]); - - options.season = seriesAnswers.season; - options.episode = seriesAnswers.episode; - } - } - - await processOptions(options); -} - -async function runCommandLine() { - program - .option('-f, --fetcher ', "Fetcher to use. Either 'native' or 'node-fetch'", 'node-fetch') - .option('-sid, --source-id ', 'ID for the source to use. Either an embed or provider', '') - .option('-tid, --tmdb-id ', 'TMDB ID for the media to scrape. Only used if source is a provider', '') - .option('-t, --type ', "Media type. Either 'movie' or 'show'. Only used if source is a provider", 'movie') - .option('-s, --season ', "Season number. Only used if type is 'show'", '0') - .option('-e, --episode ', "Episode number. Only used if type is 'show'", '0') - .option('-u, --url ', 'URL to a video embed. Only used if source is an embed', ''); - - program.parse(); - - await processOptions(program.opts()); -} - -if (process.argv.length === 2) { - runQuestions(); -} else { - runCommandLine(); -} diff --git a/src/dev-cli/config.ts b/src/dev-cli/config.ts new file mode 100644 index 0000000..6deb539 --- /dev/null +++ b/src/dev-cli/config.ts @@ -0,0 +1,12 @@ +export function getConfig() { + let tmdbApiKey = process.env.MOVIE_WEB_TMDB_API_KEY ?? ''; + tmdbApiKey = tmdbApiKey.trim(); + + if (!tmdbApiKey) { + throw new Error('Missing MOVIE_WEB_TMDB_API_KEY environment variable'); + } + + return { + tmdbApiKey, + }; +} diff --git a/src/dev-cli/index.ts b/src/dev-cli/index.ts new file mode 100644 index 0000000..13a8210 --- /dev/null +++ b/src/dev-cli/index.ts @@ -0,0 +1,227 @@ +/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ + +import { program } from 'commander'; +import dotenv from 'dotenv'; +import { prompt } from 'enquirer'; +import Spinnies from 'spinnies'; + +import { logDeepObject } from '@/dev-cli/logging'; +import { getMovieMediaDetails, getShowMediaDetails } from '@/dev-cli/tmdb'; +import { CommandLineArguments, processOptions } from '@/dev-cli/validate'; + +import { MetaOutput, ProviderControls, getBuiltinEmbeds, getBuiltinSources } from '..'; + +dotenv.config(); + +type ProviderSourceAnswers = { + id: string; + type: string; +}; + +type EmbedSourceAnswers = { + url: string; +}; + +type CommonAnswers = { + fetcher: string; + source: string; +}; + +type ShowAnswers = { + season: string; + episode: string; +}; + +const sourceScrapers = getBuiltinSources().sort((a, b) => b.rank - a.rank); +const embedScrapers = getBuiltinEmbeds().sort((a, b) => b.rank - a.rank); +const sources = [...sourceScrapers, ...embedScrapers]; + +function joinMediaTypes(mediaTypes: string[] | undefined) { + if (mediaTypes) { + const formatted = mediaTypes + .map((type: string) => { + return `${type[0].toUpperCase() + type.substring(1).toLowerCase()}s`; + }) + .join(' / '); + + return `(${formatted})`; + } + return ''; // * Embed sources pass through here too +} + +async function runScraper(providers: ProviderControls, source: MetaOutput, options: CommandLineArguments) { + const spinnies = new Spinnies(); + + if (source.type === 'embed') { + spinnies.add('scrape', { text: `Running ${source.name} scraper on ${options.url}` }); + try { + const result = await providers.runEmbedScraper({ + url: options.url, + id: source.id, + }); + spinnies.succeed('scrape', { text: 'Done!' }); + logDeepObject(result); + } catch (error) { + let message = 'Unknown error'; + if (error instanceof Error) { + message = error.message; + } + + spinnies.fail('scrape', { text: `ERROR: ${message}` }); + } + } else { + let media; + + if (options.type === 'movie') { + media = await getMovieMediaDetails(options.tmdbId); + } else { + media = await getShowMediaDetails(options.tmdbId, options.season, options.episode); + } + + spinnies.add('scrape', { text: `Running ${source.name} scraper on ${media.title}` }); + try { + const result = await providers.runSourceScraper({ + media, + id: source.id, + }); + spinnies.succeed('scrape', { text: 'Done!' }); + logDeepObject(result); + } catch (error) { + let message = 'Unknown error'; + if (error instanceof Error) { + message = error.message; + } + + spinnies.fail('scrape', { text: `ERROR: ${message}` }); + } + } +} + +async function runQuestions() { + const options = { + fetcher: 'node-fetch', + sourceId: '', + tmdbId: '', + type: 'movie', + season: '0', + episode: '0', + url: '', + }; + + const answers = await prompt([ + { + type: 'select', + name: 'fetcher', + message: 'Select a fetcher', + choices: [ + { + message: 'Native', + name: 'native', + }, + { + message: 'Node fetch', + name: 'node-fetch', + }, + ], + }, + { + type: 'select', + name: 'source', + message: 'Select a source', + choices: sources.map((source) => ({ + message: `[${source.type.toLocaleUpperCase()}] ${source.name} ${joinMediaTypes(source.mediaTypes)}`.trim(), + name: source.id, + })), + }, + ]); + + options.fetcher = answers.fetcher; + options.sourceId = answers.source; + + const source = sources.find(({ id }) => id === answers.source); + + if (!source) { + throw new Error(`No source with ID ${answers.source} found`); + } + + if (source.type === 'embed') { + const sourceAnswers = await prompt([ + { + type: 'input', + name: 'url', + message: 'Embed URL', + }, + ]); + + options.url = sourceAnswers.url; + } else { + const sourceAnswers = await prompt([ + { + type: 'input', + name: 'id', + message: 'TMDB ID', + }, + { + type: 'select', + name: 'type', + message: 'Media type', + choices: [ + { + message: 'Movie', + name: 'movie', + }, + { + message: 'TV Show', + name: 'show', + }, + ], + }, + ]); + + options.tmdbId = sourceAnswers.id; + options.type = sourceAnswers.type; + + if (sourceAnswers.type === 'show') { + const seriesAnswers = await prompt([ + { + type: 'input', + name: 'season', + message: 'Season', + }, + { + type: 'input', + name: 'episode', + message: 'Episode', + }, + ]); + + options.season = seriesAnswers.season; + options.episode = seriesAnswers.episode; + } + } + + const { providers, source: validatedSource, options: validatedOps } = await processOptions(sources, options); + await runScraper(providers, validatedSource, validatedOps); +} + +async function runCommandLine() { + program + .option('-f, --fetcher ', "Fetcher to use. Either 'native' or 'node-fetch'", 'node-fetch') + .option('-sid, --source-id ', 'ID for the source to use. Either an embed or provider', '') + .option('-tid, --tmdb-id ', 'TMDB ID for the media to scrape. Only used if source is a provider', '') + .option('-t, --type ', "Media type. Either 'movie' or 'show'. Only used if source is a provider", 'movie') + .option('-s, --season ', "Season number. Only used if type is 'show'", '0') + .option('-e, --episode ', "Episode number. Only used if type is 'show'", '0') + .option('-u, --url ', 'URL to a video embed. Only used if source is an embed', ''); + + program.parse(); + + const { providers, source: validatedSource, options: validatedOps } = await processOptions(sources, program.opts()); + await runScraper(providers, validatedSource, validatedOps); +} + +if (process.argv.length === 2) { + runQuestions().catch(() => console.error('Exited.')); +} else { + runCommandLine().catch(() => console.error('Exited.')); +} diff --git a/src/dev-cli/logging.ts b/src/dev-cli/logging.ts new file mode 100644 index 0000000..1a519f4 --- /dev/null +++ b/src/dev-cli/logging.ts @@ -0,0 +1,5 @@ +import { inspect } from 'node:util'; + +export function logDeepObject(object: Record) { + console.log(inspect(object, { showHidden: false, depth: null, colors: true })); +} diff --git a/src/dev-cli/tmdb.ts b/src/dev-cli/tmdb.ts new file mode 100644 index 0000000..7490336 --- /dev/null +++ b/src/dev-cli/tmdb.ts @@ -0,0 +1,95 @@ +import { getConfig } from '@/dev-cli/config'; + +import { MovieMedia, ShowMedia } from '..'; + +export async function makeTMDBRequest(url: string): Promise { + const headers: { + accept: 'application/json'; + authorization?: string; + } = { + accept: 'application/json', + }; + + let requestURL = url; + const key = getConfig().tmdbApiKey; + + // * JWT keys always start with ey and are ONLY valid as a header. + // * All other keys are ONLY valid as a query param. + // * Thanks TMDB. + if (key.startsWith('ey')) { + headers.authorization = `Bearer ${key}`; + } else { + requestURL += `?api_key=${key}`; + } + + return fetch(requestURL, { + method: 'GET', + headers, + }); +} + +export async function getMovieMediaDetails(id: string): Promise { + const response = await makeTMDBRequest(`https://api.themoviedb.org/3/movie/${id}`); + const movie = await response.json(); + + if (movie.success === false) { + throw new Error(movie.status_message); + } + + if (!movie.release_date) { + throw new Error(`${movie.title} has no release_date. Assuming unreleased`); + } + + return { + type: 'movie', + title: movie.title, + releaseYear: Number(movie.release_date.split('-')[0]), + tmdbId: id, + }; +} + +export async function getShowMediaDetails(id: string, seasonNumber: string, episodeNumber: string): Promise { + // * TV shows require the TMDB ID for the series, season, and episode + // * and the name of the series. Needs multiple requests + let response = await makeTMDBRequest(`https://api.themoviedb.org/3/tv/${id}`); + const series = await response.json(); + + if (series.success === false) { + throw new Error(series.status_message); + } + + if (!series.first_air_date) { + throw new Error(`${series.name} has no first_air_date. Assuming unaired`); + } + + response = await makeTMDBRequest(`https://api.themoviedb.org/3/tv/${id}/season/${seasonNumber}`); + const season = await response.json(); + + if (season.success === false) { + throw new Error(season.status_message); + } + + response = await makeTMDBRequest( + `https://api.themoviedb.org/3/tv/${id}/season/${seasonNumber}/episode/${episodeNumber}`, + ); + const episode = await response.json(); + + if (episode.success === false) { + throw new Error(episode.status_message); + } + + return { + type: 'show', + title: series.name, + releaseYear: Number(series.first_air_date.split('-')[0]), + tmdbId: id, + episode: { + number: episode.episode_number, + tmdbId: episode.id, + }, + season: { + number: season.season_number, + tmdbId: season.id, + }, + }; +} diff --git a/src/dev-cli/validate.ts b/src/dev-cli/validate.ts new file mode 100644 index 0000000..60c8340 --- /dev/null +++ b/src/dev-cli/validate.ts @@ -0,0 +1,90 @@ +import nodeFetch from 'node-fetch'; + +import { Embed, Sourcerer } from '@/providers/base'; + +import { makeProviders, makeStandardFetcher, targets } from '..'; + +export type CommandLineArguments = { + fetcher: string; + sourceId: string; + tmdbId: string; + type: string; + season: string; + episode: string; + url: string; +}; + +export async function processOptions(sources: Array, options: CommandLineArguments) { + if (options.fetcher !== 'node-fetch' && options.fetcher !== 'native') { + throw new Error("Fetcher must be either 'native' or 'node-fetch'"); + } + + if (!options.sourceId.trim()) { + throw new Error('Source ID must be provided'); + } + + const source = sources.find(({ id }) => id === options.sourceId); + + if (!source) { + throw new Error('Invalid source ID. No source found'); + } + + if (source.type === 'embed' && !options.url.trim()) { + throw new Error('Must provide an embed URL for embed sources'); + } + + if (source.type === 'source') { + if (!options.tmdbId.trim()) { + throw new Error('Must provide a TMDB ID for provider sources'); + } + + if (Number.isNaN(Number(options.tmdbId)) || Number(options.tmdbId) < 0) { + throw new Error('TMDB ID must be a number greater than 0'); + } + + if (!options.type.trim()) { + throw new Error('Must provide a type for provider sources'); + } + + if (options.type !== 'movie' && options.type !== 'show') { + throw new Error("Invalid media type. Must be either 'movie' or 'show'"); + } + + if (options.type === 'show') { + if (!options.season.trim()) { + throw new Error('Must provide a season number for TV shows'); + } + + if (!options.episode.trim()) { + throw new Error('Must provide an episode number for TV shows'); + } + + if (Number.isNaN(Number(options.season)) || Number(options.season) <= 0) { + throw new Error('Season number must be a number greater than 0'); + } + + if (Number.isNaN(Number(options.episode)) || Number(options.episode) <= 0) { + throw new Error('Episode number must be a number greater than 0'); + } + } + } + + let fetcher; + + if (options.fetcher === 'native') { + fetcher = makeStandardFetcher(fetch); + } else { + fetcher = makeStandardFetcher(nodeFetch); + } + + const providers = makeProviders({ + fetcher, + target: targets.NATIVE, + }); + + return { + providers, + options, + source, + }; +} diff --git a/src/providers/base.ts b/src/providers/base.ts index 7371166..0d43895 100644 --- a/src/providers/base.ts +++ b/src/providers/base.ts @@ -2,6 +2,8 @@ import { Flags } from '@/entrypoint/utils/targets'; import { Stream } from '@/providers/streams'; import { EmbedScrapeContext, MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +export type MediaScraperTypes = 'show' | 'movie'; + export type SourcererEmbed = { embedId: string; url: string; @@ -12,7 +14,7 @@ export type SourcererOutput = { stream?: Stream[]; }; -export type Sourcerer = { +export type SourcererOptions = { id: string; name: string; // displayed in the UI rank: number; // the higher the number, the earlier it gets put on the queue @@ -22,15 +24,29 @@ export type Sourcerer = { scrapeShow?: (input: ShowScrapeContext) => Promise; }; -export function makeSourcerer(state: Sourcerer): Sourcerer { - return state; +export type Sourcerer = SourcererOptions & { + type: 'source'; + disabled: boolean; + mediaTypes: MediaScraperTypes[]; +}; + +export function makeSourcerer(state: SourcererOptions): Sourcerer { + const mediaTypes: MediaScraperTypes[] = []; + if (state.scrapeMovie) mediaTypes.push('movie'); + if (state.scrapeShow) mediaTypes.push('show'); + return { + ...state, + type: 'source', + disabled: state.disabled ?? false, + mediaTypes, + }; } export type EmbedOutput = { stream: Stream[]; }; -export type Embed = { +export type EmbedOptions = { id: string; name: string; // displayed in the UI rank: number; // the higher the number, the earlier it gets put on the queue @@ -38,6 +54,17 @@ export type Embed = { scrape: (input: EmbedScrapeContext) => Promise; }; -export function makeEmbed(state: Embed): Embed { - return state; +export type Embed = EmbedOptions & { + type: 'embed'; + disabled: boolean; + mediaTypes: undefined; +}; + +export function makeEmbed(state: EmbedOptions): Embed { + return { + ...state, + type: 'embed', + disabled: state.disabled ?? false, + mediaTypes: undefined, + }; }