Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 79 additions & 19 deletions src/providers/popr/popr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ export class PoprProvider extends BaseProvider {
supportedContentTypes: ['movies', 'tv']
};

/**
* Fetch movie sources
*/
async getMovieSources(media: ProviderMediaObject): Promise<ProviderResult> {
try {
let movieSource = await this.fetchSource(media, 'movie');
Expand All @@ -46,9 +43,6 @@ export class PoprProvider extends BaseProvider {
}
}

/**
* Fetch TV episode sources
*/
async getTVSources(media: ProviderMediaObject): Promise<ProviderResult> {
try {
let tvSource = await this.fetchSource(media, 'tv');
Expand All @@ -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<string, string> = {},
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('<!doctype html>') ||
trimmed.toLowerCase().includes('<html')
) {
this.console.log(`[Popr] [${serverName}] Validation failed: Returned HTML error page.`);
return { isValid: false, type: 'mp4' };
}

return { isValid: true, type: 'mp4' };
} catch (error) {
this.console.log(`[Popr] [${serverName}] Validation request failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
return { isValid: false, type: 'mp4' };
}
}

private async fetchSource(
media: ProviderMediaObject,
type: 'tv' | 'movie' = 'movie'
Expand Down Expand Up @@ -106,28 +156,38 @@ export class PoprProvider extends BaseProvider {
})
.then(async (res) => {
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',
Expand All @@ -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);
Expand All @@ -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),
Expand Down Expand Up @@ -194,9 +256,7 @@ export class PoprProvider extends BaseProvider {
]
};
}
/**
* Health check
*/

async healthCheck(): Promise<boolean> {
try {
const response = await fetch(this.BASE_URL, {
Expand Down