diff --git a/src/providers/captions.ts b/src/providers/captions.ts index 92e5db3..d64dcc2 100644 --- a/src/providers/captions.ts +++ b/src/providers/captions.ts @@ -9,6 +9,7 @@ export type CaptionType = keyof typeof captionTypes; export type Caption = { type: CaptionType; id: string; // only unique per stream + opensubtitles?: boolean; url: string; hasCorsRestrictions: boolean; language: string; diff --git a/src/runners/individualRunner.ts b/src/runners/individualRunner.ts index b309180..da7fca9 100644 --- a/src/runners/individualRunner.ts +++ b/src/runners/individualRunner.ts @@ -6,6 +6,7 @@ import { EmbedOutput, SourcererOutput } from '@/providers/base'; import { ProviderList } from '@/providers/get'; import { ScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; +import { addOpenSubtitlesCaptions } from '@/utils/opensubtitles'; import { isValidStream, validatePlayableStreams } from '@/utils/valid'; export type IndividualSourceRunnerOptions = { @@ -66,6 +67,14 @@ export async function scrapeInvidualSource( return true; }); + // opensubtitles + for (const embed of output.embeds) + embed.url = `${embed.url}${btoa('MEDIA=')}${btoa( + `${ops.media.imdbId}${ + ops.media.type === 'show' ? `.${ops.media.season.number}.${ops.media.episode.number}` : '' + }`, + )}`; + if ((!output.stream || output.stream.length === 0) && output.embeds.length === 0) throw new NotFoundError('No streams found'); @@ -73,6 +82,19 @@ export async function scrapeInvidualSource( if (output.stream && output.stream.length > 0 && output.embeds.length === 0) { const playableStreams = await validatePlayableStreams(output.stream, ops, sourceScraper.id); if (playableStreams.length === 0) throw new NotFoundError('No playable streams found'); + + // opensubtitles + 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}` : '' + }`, + ), + ); + } output.stream = playableStreams; } return output; @@ -94,10 +116,14 @@ export async function scrapeIndividualEmbed( const embedScraper = list.embeds.find((v) => ops.id === v.id); if (!embedScraper) throw new Error('Embed with ID not found'); + let url = ops.url; + let media; + if (ops.url.includes(btoa('MEDIA='))) [url, media] = url.split(btoa('MEDIA=')); + const output = await embedScraper.scrape({ fetcher: ops.fetcher, proxiedFetcher: ops.proxiedFetcher, - url: ops.url, + url, progress(val) { ops.events?.update?.({ id: embedScraper.id, @@ -114,6 +140,11 @@ export async function scrapeIndividualEmbed( const playableStreams = await validatePlayableStreams(output.stream, ops, embedScraper.id); if (playableStreams.length === 0) throw new NotFoundError('No playable streams found'); + + if (media) + for (const playableStream of playableStreams) + playableStream.captions = await addOpenSubtitlesCaptions(playableStream.captions, ops, media); + output.stream = playableStreams; return output; diff --git a/src/runners/runner.ts b/src/runners/runner.ts index c5f5de3..e6dea21 100644 --- a/src/runners/runner.ts +++ b/src/runners/runner.ts @@ -8,6 +8,7 @@ import { Stream } from '@/providers/streams'; import { ScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; import { reorderOnIdList } from '@/utils/list'; +import { addOpenSubtitlesCaptions } from '@/utils/opensubtitles'; import { isValidStream, validatePlayableStream } from '@/utils/valid'; export type RunOutput = { @@ -106,6 +107,18 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt if (output.stream?.[0]) { const playableStream = await validatePlayableStream(output.stream[0], ops, source.id); if (!playableStream) throw new NotFoundError('No streams found'); + + // opensubtitles + 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, stream: playableStream, @@ -153,6 +166,17 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt } const playableStream = await validatePlayableStream(embedOutput.stream[0], ops, embed.embedId); if (!playableStream) throw new NotFoundError('No streams found'); + + // opensubtitles + 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/opensubtitles.ts b/src/utils/opensubtitles.ts new file mode 100644 index 0000000..559536c --- /dev/null +++ b/src/utils/opensubtitles.ts @@ -0,0 +1,49 @@ +import { Caption, labelToLanguageCode, removeDuplicatedLanguages } from '@/providers/captions'; +import { IndividualEmbedRunnerOptions } from '@/runners/individualRunner'; +import { ProviderRunnerOptions } from '@/runners/runner'; + +export async function addOpenSubtitlesCaptions( + captions: Caption[], + ops: ProviderRunnerOptions | IndividualEmbedRunnerOptions, + media: string, +): Promise { + try { + const [imdbId, season, episode] = atob(media) + .split('.') + .map((x, i) => (i === 0 ? x : Number(x) || null)); + if (!imdbId) return captions; + const Res: { + LanguageName: string; + SubDownloadLink: string; + SubFormat: 'srt' | 'vtt'; + }[] = await ops.proxiedFetcher( + `https://rest.opensubtitles.org/search/${ + season && episode ? `episode-${episode}/` : '' + }imdbid-${(imdbId as string).slice(2)}${season && episode ? `/season-${season}` : ''}`, + { + headers: { + 'X-User-Agent': 'VLSub 0.10.2', + }, + }, + ); + + const openSubtilesCaptions: Caption[] = []; + for (const caption of Res) { + const url = caption.SubDownloadLink.replace('.gz', '').replace('download/', 'download/subencoding-utf8/'); + const language = labelToLanguageCode(caption.LanguageName); + if (!url || !language) continue; + else + openSubtilesCaptions.push({ + id: url, + opensubtitles: true, + url, + type: caption.SubFormat || 'srt', + hasCorsRestrictions: false, + language, + }); + } + return [...captions, ...removeDuplicatedLanguages(openSubtilesCaptions)]; + } catch { + return captions; + } +}