diff --git a/src/providers/popr/popr.ts b/src/providers/popr/popr.ts index e3a0776..1c1a053 100644 --- a/src/providers/popr/popr.ts +++ b/src/providers/popr/popr.ts @@ -24,9 +24,6 @@ export class PoprProvider extends BaseProvider { supportedContentTypes: ['movies', 'tv'] }; - /** - * Fetch movie sources - */ async getMovieSources(media: ProviderMediaObject): Promise { try { let movieSource = await this.fetchSource(media, 'movie'); @@ -46,9 +43,6 @@ export class PoprProvider extends BaseProvider { } } - /** - * Fetch TV episode sources - */ async getTVSources(media: ProviderMediaObject): Promise { try { let tvSource = await this.fetchSource(media, 'tv'); @@ -68,7 +62,63 @@ export class PoprProvider extends BaseProvider { } } - // https://popr.ink/api/vidnest?id=262848&type=tv&server=catflix&season=1&episode=1 + private async checkStreamType( + url: string, + headers: Record = {}, + serverName: string + ): Promise<{ isValid: boolean; type: SourceType }> { + try { + const res = await fetch(url, { + headers: { ...this.HEADERS, ...headers }, + signal: AbortSignal.timeout(5000), + redirect: 'follow' + }); + + if (!res.ok) { + this.console.log(`[Popr] [${serverName}] Validation failed: HTTP ${res.status}`); + return { isValid: false, type: 'mp4' }; + } + + const contentType = res.headers.get('content-type') || ''; + if ( + contentType.includes('video/mp4') || + contentType.includes('video/webm') + ) { + return { isValid: true, type: 'mp4' }; + } + + const text = await res.text(); + const trimmed = text.trim(); + + if (trimmed.startsWith('#EXTM3U')) { + const segmentLines = trimmed.split('\n').filter((l) => { + const t = l.trim(); + return t && !t.startsWith('#'); + }); + + if (segmentLines.length === 0) { + this.console.log(`[Popr] [${serverName}] Validation failed: Empty M3U8 playlist.`); + return { isValid: false, type: 'hls' }; + } + + return { isValid: true, type: 'hls' }; + } + + if ( + trimmed.toLowerCase().includes('') || + trimmed.toLowerCase().includes(' { if (res.status !== 200) return null; + const data = (await res.json()) as VidnestResponse; const stream = data?.results?.[0]?.streams?.[0]; + if (!stream?.url) return null; - const ext = (new URL(stream.url).pathname.match( - /\.[^./]+$/ - ) || [''])[0]; + const streamHeaders = stream.headers || {}; + const { isValid, type } = await this.checkStreamType( + stream.url, + streamHeaders, + server + ); + + if (!isValid) return null; const quality = stream.quality; const INVALID_QUALITIES = ['Hindi', 'English', 'MAIN']; const QUALITIES = ['Hindi', 'English']; const languages = QUALITIES.includes(quality); + const proxyHeaders = { + ...this.HEADERS, + ...streamHeaders + }; + return { source: { url: this.createProxyUrl( stream.url, - stream.headers + proxyHeaders ), - type: (ext === '.m3u8' - ? 'hls' - : 'mp4') as SourceType, + type, quality: INVALID_QUALITIES.includes(quality) ? 'auto' : quality || 'auto', @@ -144,7 +204,10 @@ export class PoprProvider extends BaseProvider { subtitles: data.results?.[0]?.subtitles || [] }; }) - .catch(() => null) // swallow per-request errors + .catch((error) => { + this.console.error(`[Popr] [${server}] Request failed: ${error instanceof Error ? error.message : 'Unknown'}`); + return null; + }) ); const results = await Promise.allSettled(requests); @@ -160,7 +223,6 @@ export class PoprProvider extends BaseProvider { for (const sub of res.value.subtitles) { if (!sub?.url) continue; - // dedupe subtitles by URL if (!subtitlesMap.has(sub.url)) { subtitlesMap.set(sub.url, { url: this.createProxyUrl(sub.url), @@ -194,9 +256,7 @@ export class PoprProvider extends BaseProvider { ] }; } - /** - * Health check - */ + async healthCheck(): Promise { try { const response = await fetch(this.BASE_URL, {