diff --git a/package.json b/package.json index 64b8a45..5be145a 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,8 @@ "nanoid": "^3.3.8", "node-fetch": "^3.3.2", "set-cookie-parser": "^2.7.1", - "unpacker": "^1.0.1" + "unpacker": "^1.0.1", + "wyzie-lib": "^2.2.1" }, "packageManager": "pnpm@9.14.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 654fe37..49c8820 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: unpacker: specifier: ^1.0.1 version: 1.0.1 + wyzie-lib: + specifier: ^2.2.1 + version: 2.2.1 devDependencies: '@nabla/vite-plugin-eslint': specifier: ^2.0.5 @@ -2525,6 +2528,9 @@ packages: utf-8-validate: optional: true + wyzie-lib@2.2.1: + resolution: {integrity: sha512-oZtJnCTQCqLZimlD1Ex195+8e21/G5BWhixCE5/4Of77AI9igmIQCo/thWngOsdOufcnzvzTPYkWoNDxHc5lQg==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5225,6 +5231,8 @@ snapshots: ws@8.18.0: {} + wyzie-lib@2.2.1: {} + y18n@5.0.8: {} yallist@4.0.0: {} diff --git a/src/providers/captions.ts b/src/providers/captions.ts index d64dcc2..2ee775c 100644 --- a/src/providers/captions.ts +++ b/src/providers/captions.ts @@ -10,6 +10,7 @@ export type Caption = { type: CaptionType; id: string; // only unique per stream opensubtitles?: boolean; + wyziesubs?: boolean; url: string; hasCorsRestrictions: boolean; language: string; diff --git a/src/runners/individualRunner.ts b/src/runners/individualRunner.ts index f96ab3c..7506b67 100644 --- a/src/runners/individualRunner.ts +++ b/src/runners/individualRunner.ts @@ -9,6 +9,7 @@ import { NotFoundError } from '@/utils/errors'; import { addOpenSubtitlesCaptions } from '@/utils/opensubtitles'; import { requiresProxy, setupProxy } from '@/utils/proxy'; import { isValidStream, validatePlayableStreams } from '@/utils/valid'; +import { addWyzieCaptions } from '@/utils/wyziesubs'; export type IndividualSourceRunnerOptions = { features: FeatureMap; @@ -92,18 +93,33 @@ export async function scrapeInvidualSource( if (playableStreams.length === 0) throw new NotFoundError('No playable streams found'); // opensubtitles - if (!ops.disableOpensubtitles) + if (!ops.disableOpensubtitles) { for (const playableStream of playableStreams) { - playableStream.captions = await addOpenSubtitlesCaptions( - playableStream.captions, - ops, - btoa( - `${ops.media.imdbId}${ - ops.media.type === 'show' ? `.${ops.media.season.number}.${ops.media.episode.number}` : '' - }`, - ), - ); + // Try Wyzie subs first + if (ops.media.imdbId) { + playableStream.captions = await addWyzieCaptions( + playableStream.captions, + ops.media.tmdbId, + ops.media.imdbId, + ops.media.type === 'show' ? ops.media.season.number : undefined, + ops.media.type === 'show' ? ops.media.episode.number : undefined, + ); + + // Fall back to OpenSubtitles if no Wyzie subs found + if (!playableStream.captions.some((caption) => caption.wyziesubs)) { + playableStream.captions = await addOpenSubtitlesCaptions( + playableStream.captions, + ops, + btoa( + `${ops.media.imdbId}${ + ops.media.type === 'show' ? `.${ops.media.season.number}.${ops.media.episode.number}` : '' + }`, + ), + ); + } + } } + } output.stream = playableStreams; } return output; diff --git a/src/runners/runner.ts b/src/runners/runner.ts index 6c8ccaa..064d064 100644 --- a/src/runners/runner.ts +++ b/src/runners/runner.ts @@ -11,6 +11,7 @@ import { reorderOnIdList } from '@/utils/list'; import { addOpenSubtitlesCaptions } from '@/utils/opensubtitles'; import { requiresProxy, setupProxy } from '@/utils/proxy'; import { isValidStream, validatePlayableStream } from '@/utils/valid'; +import { addWyzieCaptions } from '@/utils/wyziesubs'; export type RunOutput = { sourceId: string; @@ -116,16 +117,31 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt if (!playableStream) throw new NotFoundError('No streams found'); // opensubtitles - if (!ops.disableOpensubtitles) - playableStream.captions = await addOpenSubtitlesCaptions( - playableStream.captions, - ops, - btoa( - `${ops.media.imdbId}${ - ops.media.type === 'show' ? `.${ops.media.season.number}.${ops.media.episode.number}` : '' - }`, - ), - ); + if (!ops.disableOpensubtitles) { + if (ops.media.imdbId) { + // Try Wyzie subs first + playableStream.captions = await addWyzieCaptions( + playableStream.captions, + ops.media.tmdbId, + ops.media.imdbId, + ops.media.type === 'show' ? ops.media.season.number : undefined, + ops.media.type === 'show' ? ops.media.episode.number : undefined, + ); + + // Fall back to OpenSubtitles if no Wyzie subs found + if (!playableStream.captions.some((caption) => caption.wyziesubs)) { + playableStream.captions = await addOpenSubtitlesCaptions( + playableStream.captions, + ops, + btoa( + `${ops.media.imdbId}${ + ops.media.type === 'show' ? `.${ops.media.season.number}.${ops.media.episode.number}` : '' + }`, + ), + ); + } + } + } return { sourceId: source.id, @@ -179,16 +195,31 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt if (!playableStream) throw new NotFoundError('No streams found'); // opensubtitles - if (!ops.disableOpensubtitles) - playableStream.captions = await addOpenSubtitlesCaptions( - playableStream.captions, - ops, - btoa( - `${ops.media.imdbId}${ - ops.media.type === 'show' ? `.${ops.media.season.number}.${ops.media.episode.number}` : '' - }`, - ), - ); + if (!ops.disableOpensubtitles) { + if (ops.media.imdbId) { + // Try Wyzie subs first + playableStream.captions = await addWyzieCaptions( + playableStream.captions, + ops.media.tmdbId, + ops.media.imdbId, + ops.media.type === 'show' ? ops.media.season.number : undefined, + ops.media.type === 'show' ? ops.media.episode.number : undefined, + ); + + // Fall back to OpenSubtitles if no Wyzie subs found + if (!playableStream.captions.some((caption) => caption.wyziesubs)) { + playableStream.captions = await addOpenSubtitlesCaptions( + playableStream.captions, + ops, + btoa( + `${ops.media.imdbId}${ + ops.media.type === 'show' ? `.${ops.media.season.number}.${ops.media.episode.number}` : '' + }`, + ), + ); + } + } + } embedOutput.stream = [playableStream]; } catch (error) { const updateParams: UpdateEvent = { diff --git a/src/utils/wyziesubs.ts b/src/utils/wyziesubs.ts new file mode 100644 index 0000000..9252ee9 --- /dev/null +++ b/src/utils/wyziesubs.ts @@ -0,0 +1,50 @@ +import { type SubtitleData, searchSubtitles } from 'wyzie-lib'; + +import { Caption } from '@/providers/captions'; + +export async function addWyzieCaptions( + captions: Caption[], + tmdbId: string | number, + imdbId: string, + season?: number, + episode?: number, +): Promise { + try { + const searchParams: any = { + format: 'srt', + }; + + // Prefer TMDB ID if available, otherwise use IMDB ID + if (tmdbId) { + // Convert TMDB ID to number if it's a string + searchParams.tmdb_id = typeof tmdbId === 'string' ? parseInt(tmdbId, 10) : tmdbId; + } else if (imdbId) { + // Remove 'tt' prefix from IMDB ID if present + searchParams.imdb_id = imdbId.replace(/^tt/, ''); + } + + // Add season and episode if provided (for TV shows) + if (season && episode) { + searchParams.season = season; + searchParams.episode = episode; + } + + console.log('Searching Wyzie subtitles with params:', searchParams); + const wyzieSubtitles: SubtitleData[] = await searchSubtitles(searchParams); + console.log('Found Wyzie subtitles:', wyzieSubtitles); + + const wyzieCaptions: Caption[] = wyzieSubtitles.map((subtitle) => ({ + id: subtitle.id, + url: subtitle.url, + type: subtitle.format as 'srt' | 'vtt', + hasCorsRestrictions: false, + language: subtitle.language, + wyziesubs: true, + })); + + return [...captions, ...wyzieCaptions]; + } catch (error) { + console.error('Error fetching Wyzie subtitles:', error); + return captions; + } +}