Skip to content

feat: add Popr provider with streaming and subtitles support - #26

Merged
An0n-00 merged 9 commits into
cinepro-org:devfrom
nikhil-dev-395:feat/popr-provider
Apr 27, 2026
Merged

feat: add Popr provider with streaming and subtitles support#26
An0n-00 merged 9 commits into
cinepro-org:devfrom
nikhil-dev-395:feat/popr-provider

Conversation

@nikhil-dev-395

Copy link
Copy Markdown
Contributor

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 response

New Provider Integration:

  • Added the PoprProvider class in src/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 VidnestResponse type in src/providers/popr/popr.types.ts to define the structure of the Popr API responses, supporting type safety and easier data handling.

Copilot AI review requested due to automatic review settings April 26, 2026 12:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 PoprProvider with movie/TV source fetching, subtitle mapping, and health checks.
  • Adds VidnestResponse typings for the Popr /api/vidnest response shape.
  • Adds vitest to 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.

Comment thread src/providers/popr/popr.ts Outdated
Comment on lines +148 to +151
url: sub.url,
format: 'vtt',
label: sub.lang || 'Unknown'
}))

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread package.json Outdated
Comment on lines +23 to +24
"typescript": "^5.6.2",
"vitest": "^4.1.5"

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +39
return {
sources: movieSource.sources,
subtitles: movieSource.subtitles,
diagnostics: []
};

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +67
return {
sources: tvSource.sources,
subtitles: tvSource.subtitles,
diagnostics: []
};

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
this.console.error(
error instanceof Error
? error.message
: 'error at getting movie source'

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
: 'error at getting movie source'
: 'error at getting tv source'

Copilot uses AI. Check for mistakes.
Comment thread src/providers/popr/popr.ts Outdated
`${this.BASE_URL}/api/vidnest?id=${media.tmdbId}&type=movie` +
(server !== 'default' ? `&server=${server}` : '');
}
let data = await axios.get<VidnestResponse>(requestUrl, {

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
let data = await axios.get<VidnestResponse>(requestUrl, {
let data = await axios.get<VidnestResponse>(requestUrl, {
timeout: 10000,

Copilot uses AI. Check for mistakes.
Comment thread src/providers/popr/popr.ts Outdated
return {
sources: [
{
url: this.createProxyUrl(url, streamHeader),

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
url: this.createProxyUrl(url, streamHeader),
url: this.createProxyUrl(url, {
...this.HEADERS,
...(streamHeader || {}),
Referer: `${this.BASE_URL}/`
}),

Copilot uses AI. Check for mistakes.
}
],
subtitles: subtitles.map((sub) => ({
url: sub.url,

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
url: sub.url,
url: this.createProxyUrl(sub.url, {
...this.HEADERS,
Referer: `${this.BASE_URL}/`
}),

Copilot uses AI. Check for mistakes.
@An0n-00 An0n-00 added the enhancement New feature or request label Apr 26, 2026

@An0n-00 An0n-00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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())
    };
}

Comment thread src/providers/popr/popr.ts Outdated
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let streamType = path.extname(new URL(url).pathname);
let streamType = (new URL(url).pathname.match(/\.[^./]+$/) || [''])[0];

instead of using path.extname use regex.

Comment thread src/providers/popr/popr.ts Outdated
} from '@omss/framework';
import axios from 'axios';
import { VidnestResponse } from './popr.types.js';
import path from 'node:path';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

@An0n-00 An0n-00 closed this Apr 27, 2026
@An0n-00 An0n-00 reopened this Apr 27, 2026
@nikhil-dev-395
nikhil-dev-395 requested a review from An0n-00 April 27, 2026 13:14

@An0n-00 An0n-00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Thanks!

@An0n-00
An0n-00 merged commit 2737e3b into cinepro-org:dev Apr 27, 2026
2 checks passed
@nikhil-dev-395

Copy link
Copy Markdown
Contributor Author

Thank you so much, i learnt lot from this single contribution

@An0n-00

An0n-00 commented Apr 27, 2026

Copy link
Copy Markdown
Member

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: url: this.createProxyUrl(sub.url) so that we don't get CORS error later on...

@nikhil-dev-395

Copy link
Copy Markdown
Contributor Author

Ok I'll update it

@An0n-00

An0n-00 commented Apr 27, 2026

Copy link
Copy Markdown
Member

Ok I'll update it

no need. already did it👍👍

@nikhil-dev-395

Copy link
Copy Markdown
Contributor Author

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.

@nischaldoescode

Copy link
Copy Markdown
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants