From cafa41add6535b8a49212ee2ea248b627468ef8b Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:32:15 -0700 Subject: [PATCH] add debrid source (not enabled yet) --- src/providers/all.ts | 2 + src/providers/sources/debrid/index.ts | 186 ++++++++++++++++++++++++++ src/providers/sources/debrid/types.ts | 21 +++ 3 files changed, 209 insertions(+) create mode 100644 src/providers/sources/debrid/index.ts create mode 100644 src/providers/sources/debrid/types.ts diff --git a/src/providers/all.ts b/src/providers/all.ts index b24fef8..c157948 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -98,6 +98,7 @@ import vidnestScraper from './sources/vidnest'; import { warezcdnScraper } from './sources/warezcdn'; import { wecimaScraper } from './sources/wecima'; import { zunimeScraper } from './sources/zunime'; +import { debridScraper } from './sources/debrid'; export function gatherAllSources(): Array { // all sources are gathered here @@ -142,6 +143,7 @@ export function gatherAllSources(): Array { pelisplushdScraper, primewireScraper, movies4fScraper, + debridScraper, ]; } diff --git a/src/providers/sources/debrid/index.ts b/src/providers/sources/debrid/index.ts new file mode 100644 index 0000000..69da859 --- /dev/null +++ b/src/providers/sources/debrid/index.ts @@ -0,0 +1,186 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +import { debridProviders, torrentioResponse } from './types'; + +const OVERRIDE_TOKEN = ''; +const OVERRIDE_SERVICE = ''; // torbox or realdebrid (or real-debrid) + +const getDebridToken = (): string | null => { + try { + if (OVERRIDE_TOKEN) return OVERRIDE_TOKEN; + } catch { + // Ignore + } + try { + if (typeof window === 'undefined') return null; + const prefData = window.localStorage.getItem('__MW::preferences'); + if (!prefData) return null; + const parsedAuth = JSON.parse(prefData); + return parsedAuth?.state?.debridToken || null; + } catch (e) { + console.error('Error getting debrid token:', e); + return null; + } +}; + +// torbox or realdebrid (coverted to `real-debrid` which is the type for torrentio) +const getDebridService = (): debridProviders => { + try { + if (OVERRIDE_SERVICE) return OVERRIDE_SERVICE as debridProviders; + } catch { + // Ignore + } + try { + if (typeof window === 'undefined') return 'real-debrid'; + const prefData = window.localStorage.getItem('__MW::preferences'); + if (!prefData) return 'real-debrid'; + const parsedPrefs = JSON.parse(prefData); + const saved = parsedPrefs?.state?.debridService; + if (saved === 'realdebrid' || !saved) return 'real-debrid'; + return saved as debridProviders; + } catch (e) { + console.error('Error getting debrid service (defaulting to real-debrid):', e); + return 'real-debrid'; + } +}; + +type DebridParsedStream = { + resolution?: string; + year?: number; + source?: string; + bitDepth?: string; + codec?: string; + audio?: string; + container?: string; + seasons?: number[]; + season?: number; + episodes?: number[]; + episode?: number; + complete?: boolean; + unrated?: boolean; + remastered?: boolean; + languages?: string[]; + dubbed?: boolean; + title: string; + url: string; +}; + +function normalizeQuality(resolution?: string): '4k' | 1080 | 720 | 480 | 360 | 'unknown' { + if (!resolution) return 'unknown'; + const res = resolution.toLowerCase(); + if (res === '4k' || res === '2160p') return '4k'; + if (res === '1080p') return 1080; + if (res === '720p') return 720; + if (res === '480p') return 480; + if (res === '360p') return 360; + return 'unknown'; +} + +// Helper to score streams for compatibility (higher is better) +function scoreStream(stream: DebridParsedStream): number { + let score = 0; + // Prefer mp4 container + if (stream.container === 'mp4') score += 10; + // Prefer aac audio + if (stream.audio === 'aac') score += 5; + // Prefer h265 codec + if (stream.codec === 'h265') score += 2; + // Penalize mkv container + if (stream.container === 'mkv') score -= 2; + // Prefer complete + if (stream.complete) score += 1; + return score; +} + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const apiKey = getDebridToken(); + if (!apiKey) { + throw new NotFoundError('Debrid API token is required'); + } + + const debridProvider: debridProviders = getDebridService(); + + let torrentioUrl = `https://torrentio.strem.fun/${debridProvider}=${apiKey}/stream/`; + + if (ctx.media.type === 'show') { + torrentioUrl += `series/${ctx.media.imdbId}:${ctx.media.season.number}:${ctx.media.episode.number}.json`; + } else { + torrentioUrl += `movie/${ctx.media.imdbId}.json`; + } + const torrentioStreams: torrentioResponse = (await ctx.proxiedFetcher(torrentioUrl)).streams; + + const response: DebridParsedStream[] = await ctx.proxiedFetcher('https://torrent-parse.pstream.mov/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(torrentioStreams), + }); + + // Group by quality, pick the most compatible stream for each + const qualities: Partial> = {}; + + // Group streams by normalized quality + const byQuality: Record = {}; + for (const stream of response) { + const quality = normalizeQuality(stream.resolution); + if (!byQuality[quality]) byQuality[quality] = []; + byQuality[quality].push(stream); + } + + // For each quality, pick the best compatible stream (prefer mp4+aac, fallback to mkv) + for (const [quality, streams] of Object.entries(byQuality)) { + // Prefer mp4 + aac + const mp4Aac = streams.find((s) => s.container === 'mp4' && s.audio === 'aac'); + if (mp4Aac) { + qualities[quality as keyof typeof qualities] = { + type: 'mp4', + url: mp4Aac.url, + }; + continue; + } + // Fallback: any mp4 + const mp4 = streams.find((s) => s.container === 'mp4'); + if (mp4) { + qualities[quality as keyof typeof qualities] = { + type: 'mp4', + url: mp4.url, + }; + continue; + } + streams.sort((a, b) => scoreStream(b) - scoreStream(a)); + const best = streams[0]; + if (best) { + qualities[quality as keyof typeof qualities] = { + type: 'mp4', // has to be set as mp4 because of types..... But mkvs *can* work in a browser depending on codec, usually it cant be hevc and has to have AAC audio + url: best.url, + }; + } + } + + return { + embeds: [], + stream: [ + { + id: 'primary', + type: 'file', + qualities, + captions: [], + flags: [], + }, + ], + }; +} + +export const debridScraper = makeSourcerer({ + id: 'debrid', + name: 'Debrid', + rank: 999, + disabled: true, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); diff --git a/src/providers/sources/debrid/types.ts b/src/providers/sources/debrid/types.ts new file mode 100644 index 0000000..155f9ad --- /dev/null +++ b/src/providers/sources/debrid/types.ts @@ -0,0 +1,21 @@ +// these need to be the names that correspond with Torrentio's +export type debridProviders = 'torbox' | 'real-debrid'; + +export interface stremioStream { + name: string; + title: string; + url: string; + behaviorHints?: { + bingeGroup: string; + filename: string; + videoSize: number; + videoHash: string; + }; +} + +export interface torrentioResponse { + streams: stremioStream[]; + cacheMaxAge: number; + staleRevalidate: number; + staleError: number; +}