feat: add Popr provider with streaming and subtitles support - #26
Conversation
…ded by using vitest
…subtitles handling
There was a problem hiding this comment.
Pull request overview
Adds a new Popr-backed provider to the OMSS provider registry (auto-discovered under src/providers/), intended to fetch movie/TV streams plus subtitles and expose a health check.
Changes:
- Introduces
PoprProviderwith movie/TV source fetching, subtitle mapping, and health checks. - Adds
VidnestResponsetypings for the Popr/api/vidnestresponse shape. - Adds
vitestto devDependencies (no accompanying test script/config in this PR).
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| src/providers/popr/popr.types.ts | Defines the expected Popr API response structure used by the provider. |
| src/providers/popr/popr.ts | Implements Popr provider source/subtitle fetching and health checks. |
| package.json | Adds vitest devDependency. |
| package-lock.json | Lockfile updates reflecting the new devDependency and resulting dependency graph. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| url: sub.url, | ||
| format: 'vtt', | ||
| label: sub.lang || 'Unknown' | ||
| })) |
There was a problem hiding this comment.
The Popr API response includes a format field for subtitles (popr.types.ts), but the provider currently hard-codes format: 'vtt'. If the API can return SRT/ASS/etc, this will mislabel subtitles for consumers. Prefer mapping sub.format (or infer from the URL extension) into the framework Subtitle['format'] values.
| "typescript": "^5.6.2", | ||
| "vitest": "^4.1.5" |
There was a problem hiding this comment.
vitest is added as a devDependency but package.json currently has no test script/config and this PR doesn't add tests. If this PR isn't intentionally setting up a test runner, consider removing it to avoid pulling in a large dependency tree; otherwise add a test script so the dependency is actually used.
| return { | ||
| sources: movieSource.sources, | ||
| subtitles: movieSource.subtitles, | ||
| diagnostics: [] | ||
| }; |
There was a problem hiding this comment.
In getMovieSources, if fetchSource returns no sources/subtitles (it returns empty arrays when all servers fail), this currently returns diagnostics: [], which makes failures silent. Consider returning emptyResult('No playable sources found', media) (or adding a warning diagnostic) when movieSource.sources.length === 0 so consumers can distinguish 'no sources' from 'provider succeeded'.
| return { | ||
| sources: tvSource.sources, | ||
| subtitles: tvSource.subtitles, | ||
| diagnostics: [] | ||
| }; |
There was a problem hiding this comment.
In getTVSources, same as movies: when fetchSource returns empty arrays after trying all servers, this returns diagnostics: [] and silently fails. Add an empty-result diagnostic when no sources are found so callers can surface a meaningful provider error/warning.
| this.console.error( | ||
| error instanceof Error | ||
| ? error.message | ||
| : 'error at getting movie source' |
There was a problem hiding this comment.
The non-Error fallback message in the getTVSources catch block says "error at getting movie source". This looks like a copy/paste error and will be misleading in logs and diagnostics; update it to reference TV sources/episodes instead.
| : 'error at getting movie source' | |
| : 'error at getting tv source' |
| `${this.BASE_URL}/api/vidnest?id=${media.tmdbId}&type=movie` + | ||
| (server !== 'default' ? `&server=${server}` : ''); | ||
| } | ||
| let data = await axios.get<VidnestResponse>(requestUrl, { |
There was a problem hiding this comment.
axios.get here has no timeout. Most other providers use a request timeout (commonly 10s) to avoid hanging the provider request indefinitely when the upstream stalls. Add a reasonable timeout (and optionally retries/backoff) to improve reliability.
| let data = await axios.get<VidnestResponse>(requestUrl, { | |
| let data = await axios.get<VidnestResponse>(requestUrl, { | |
| timeout: 10000, |
| return { | ||
| sources: [ | ||
| { | ||
| url: this.createProxyUrl(url, streamHeader), |
There was a problem hiding this comment.
createProxyUrl is called with only streamHeader from the API. If streamHeader is undefined or incomplete, the proxied request may miss required defaults like the provider User-Agent/Referer. Consider passing a merged header object (e.g., { ...this.HEADERS, ...streamHeader, Referer: ... }) so proxying remains consistent with the requests that fetched the API data.
| url: this.createProxyUrl(url, streamHeader), | |
| url: this.createProxyUrl(url, { | |
| ...this.HEADERS, | |
| ...(streamHeader || {}), | |
| Referer: `${this.BASE_URL}/` | |
| }), |
| } | ||
| ], | ||
| subtitles: subtitles.map((sub) => ({ | ||
| url: sub.url, |
There was a problem hiding this comment.
Subtitles are returned with the raw sub.url instead of being proxied. Other providers typically proxy subtitle URLs to avoid CORS issues and to attach any required headers/referer. Consider using createProxyUrl here (and include headers if Popr requires them).
| url: sub.url, | |
| url: this.createProxyUrl(sub.url, { | |
| ...this.HEADERS, | |
| Referer: `${this.BASE_URL}/` | |
| }), |
An0n-00
left a comment
There was a problem hiding this comment.
thank you for your PR. this is a solid start. however there are some things that should be changed.
like: using node:path for URL parsing is kinda weird.
right now you do one server after another. you can also use promise.all to do them all at the same time.
additionally, now you only return one (the first) url. we want all.
and you've added vitest to the package.json. please remove that.
here you have a refactor of the provider. please review the changes. thank you:
private async fetchSource(
media: ProviderMediaObject,
type: 'tv' | 'movie' = 'movie'
): Promise<{ sources: Source[]; subtitles: Subtitle[] }> {
const servers = [
'default', 'catflix', 'hexa', 'Gama', 'Liligoon',
'Sigma', 'Prime', 'Alfa', 'Lamda', 'ynx_vidsrc'
];
const ep = media.e || 1;
const season = media.s || 1;
const buildUrl = (server: string) => {
if (type === 'tv') {
return `${this.BASE_URL}/api/vidnest?id=${media.tmdbId}&type=tv&server=${server}&season=${season}&episode=${ep}`;
}
return `${this.BASE_URL}/api/vidnest?id=${media.tmdbId}&type=movie` +
(server !== 'default' ? `&server=${server}` : '');
};
const requests = servers.map(server =>
axios
.get<VidnestResponse>(buildUrl(server), {
headers: this.HEADERS,
timeout: 8000
})
.then(({ data }) => {
const stream = data?.results?.[0]?.streams?.[0];
if (!stream?.url) return null;
const ext =
(new URL(stream.url).pathname.match(/\.[^./]+$/) || [''])[0];
const quality = stream.quality;
const INVALID_QUALITIES = ['Hindi', 'English', 'MAIN'];
const QUALITIES = ['Hindi', 'English'];
const languages = QUALITIES.includes(quality);
return {
source: {
url: this.createProxyUrl(stream.url, stream.headers),
type: ext === '.m3u8' ? 'hls' : 'mp4',
quality: INVALID_QUALITIES.includes(quality)
? 'auto'
: quality || 'auto',
audioTracks: [
{
language: languages
? quality.toLowerCase().slice(0, 3)
: 'eng',
label: languages ? quality : 'English'
}
],
provider: { name: this.name, id: this.id }
},
subtitles: data.results?.[0]?.subtitles || []
};
})
.catch(() => null) // swallow per-request errors
);
const results = await Promise.allSettled(requests);
const sources: Source[] = [];
const subtitlesMap = new Map<string, Subtitle>();
for (const res of results) {
if (res.status !== 'fulfilled' || !res.value) continue;
sources.push(res.value.source);
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: sub.url,
format: 'vtt',
label: sub.lang || 'Unknown'
});
}
}
}
return {
sources,
subtitles: Array.from(subtitlesMap.values())
};
}| if (!url) continue; | ||
| let streamHeader = response?.results?.[0].streams?.[0].headers; | ||
| let quality = response?.results?.[0].streams?.[0].quality; | ||
| let streamType = path.extname(new URL(url).pathname); |
There was a problem hiding this comment.
| let streamType = path.extname(new URL(url).pathname); | |
| let streamType = (new URL(url).pathname.match(/\.[^./]+$/) || [''])[0]; |
instead of using path.extname use regex.
| } from '@omss/framework'; | ||
| import axios from 'axios'; | ||
| import { VidnestResponse } from './popr.types.js'; | ||
| import path from 'node:path'; |
There was a problem hiding this comment.
| import path from 'node:path'; |
remove the import from node... try to solve it without node:path as it might give weird behaviors in web environements
…nt regex for stream extension
|
Thank you so much, i learnt lot from this single contribution |
|
that's great. we are always open to new contributions. And btw, a small thing i've just noticed: if (!subtitlesMap.has(sub.url)) {
subtitlesMap.set(sub.url, {
url: sub.url,
format: 'vtt',
label: sub.lang || 'Unknown'
});
}here we are mapping the subtitles to the Subtitle Type. Please note that the OMSS specifies that ALL links must be proxied. that means that the url property should equal: |
|
Ok I'll update it |
no need. already did it👍👍 |
|
By the way, do you have any suggestions on how I can find open-source projects like yours to contribute to? I’m new to this, so any guidance would be helpful. |
GitHub has a search button !. Spend some time in searching it. Thanks for your contribution. |
This pull request introduces a new provider,
PoprProvider, to the codebase, enabling integration with the Popr streaming source. It adds all the necessary logic for fetching movie and TV sources, handling subtitles, and performing health checks. Additionally, it includes a new type definition for the expected API responseNew Provider Integration:
Added the
PoprProviderclass insrc/providers/popr/popr.ts, implementing methods to fetch movie and TV episode sources, handle subtitles, perform health checks, and manage error cases. This class interfaces with the Popr API and processes its responses for use in the application.Introduced the
VidnestResponsetype insrc/providers/popr/popr.types.tsto define the structure of the Popr API responses, supporting type safety and easier data handling.