From e7cf14d3a00c9bc8cd69f45bf17715446f165c60 Mon Sep 17 00:00:00 2001 From: Anon <206556099+An0n-01@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:45:06 +0100 Subject: [PATCH 1/3] feat: implement VidSrc provider for fetching movie and TV sources --- src/providers/rgshows/rgshows.ts | 1 + src/providers/vidsrc/vidsrc.ts | 268 +++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 src/providers/vidsrc/vidsrc.ts diff --git a/src/providers/rgshows/rgshows.ts b/src/providers/rgshows/rgshows.ts index 90bc725..e332888 100644 --- a/src/providers/rgshows/rgshows.ts +++ b/src/providers/rgshows/rgshows.ts @@ -51,6 +51,7 @@ export class RgShowsProvider extends BaseProvider { try { // Build page URL const pageUrl = this.buildPageUrl(media); + this.console.log(`Fetching URL: ${pageUrl}`); // Fetch page json const data = await this.fetchPage(pageUrl, media); diff --git a/src/providers/vidsrc/vidsrc.ts b/src/providers/vidsrc/vidsrc.ts new file mode 100644 index 0000000..8262d23 --- /dev/null +++ b/src/providers/vidsrc/vidsrc.ts @@ -0,0 +1,268 @@ +import { BaseProvider } from '@omss/framework'; +import type { + ProviderCapabilities, + ProviderMediaObject, + ProviderResult, + Source, + Subtitle +} from '@omss/framework'; +import axios from 'axios'; + +export class VidSrcProvider extends BaseProvider { + readonly id = 'vidsrc'; + readonly name = 'VidSrc'; + readonly enabled = true; + readonly BASE_URL = 'https://vsembed.ru/'; + readonly HEADERS = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150 Safari/537.36', + Referer: this.BASE_URL + }; + + readonly capabilities: ProviderCapabilities = { + supportedContentTypes: ['movies', 'tv'] + }; + + /** + * Fetch movie sources + */ + async getMovieSources(media: ProviderMediaObject): Promise { + return this.getSources(media); + } + + /** + * Fetch TV episode sources + */ + async getTVSources(media: ProviderMediaObject): Promise { + return this.getSources(media); + } + + /** + * Main scraping logic + */ + private async getSources( + media: ProviderMediaObject + ): Promise { + try { + // Build page URL + const pageUrl = this.buildPageUrl(media); + + // Fetch page HTML + const html = await this.fetchPage(pageUrl, media); + if (!html) { + return this.emptyResult('Failed to fetch page', media); + } + + // Extract second URL + const secondUrl = this.extractSecondUrl(html); + if (!secondUrl) { + return this.emptyResult('Invalid or expired token', media); + } + + // Fetch second page HTML + const secondHtml = await this.fetchPage(secondUrl.url, media); + if (!secondHtml) { + return this.emptyResult('Failed to fetch stream page', media); + } + + // Extract third URL + const thirdUrl = this.extractThirdUrl(secondHtml, secondUrl.url); + if (!thirdUrl) { + return this.emptyResult('Failed to extract stream URL', media); + } + + // Fetch third page HTML + const thirdHtml = await this.fetchPage(thirdUrl.url, media); + if (!thirdHtml) { + return this.emptyResult( + 'Failed to fetch final stream page', + media + ); + } + + const m3u8Urls = this.extractM3u8Urls(thirdHtml); + if (!m3u8Urls || m3u8Urls.length === 0) { + return this.emptyResult('Failed to extract m3u8 URLs', media); + } + + const sources: Source[] = m3u8Urls.map((url) => ({ + url: this.createProxyUrl(url, { + ...this.HEADERS, + Referer: 'https://cloudnestra.com/', // Use second URL as referer for the final stream request + Origin: 'https://cloudnestra.com' // Set Origin header to second URL's origin + }), + type: 'hls', // m3u8 = HLS streaming + quality: `up to HD`, // VidSrc does not provide explicit quality labels, so we use a generic one + audioTracks: [{ + label: 'English', + language: 'eng' + }], // No audio track info available + provider: { + id: this.id, + name: this.name + } + })); + + return { + sources, + subtitles: [], // VidSrc does not provide subtitle info in the player config or HTML + diagnostics: [] + }; + + // Fetch second URL + } catch (error) { + return this.emptyResult( + error instanceof Error + ? error.message + : 'Unknown provider error', + media + ); + } + } + + /** + * Build page URL based on media type + */ + private buildPageUrl(media: ProviderMediaObject): string { + if (media.type === 'movie') { + return `${this.BASE_URL}/embed/movie?tmdb=${media.tmdbId}`; + } else { + return `${this.BASE_URL}/embed/tv?tmdb=${media.tmdbId}&season=${media.s}&episode=${media.e}`; + } + } + + /** + * Fetch page HTML + */ + private async fetchPage( + url: string, + media: ProviderMediaObject + ): Promise { + try { + if (url.startsWith('//')) { + url = 'https:' + url; + } + + const response = await axios.get(url, { + headers: this.HEADERS, + timeout: 10000 + }); + + if (response.status !== 200) { + return null; + } + + return response.data; + } catch (error) { + return null; + } + } + + /** + * Extract token, expires, and playlist URL from HTML + */ + private extractSecondUrl(html: string): { url: string } | null { + const src = html.match( + /]*\s+src=["']([^"']+)["'][^>]*>/i + )?.[1]; + + if (!src) { + return null; + } + + return { url: src }; + } + + /** + * Extract third URL from inline JS (loadIframe) + * and resolve it against the second URL domain + */ + private extractThirdUrl( + html: string, + secondUrl: string + ): { url: string } | null { + // 1. Extract the relative src, e.g. '/prorcp/[path....]' + const relSrc = html.match(/src:\s*['"]([^'"]+)['"]/i)?.[1]; + if (!relSrc) { + return null; + } + + if (secondUrl.startsWith('//')) { + secondUrl = 'https:' + secondUrl; + } + + // 2. Build absolute URL from secondUrl + relSrc + let url: string; + try { + url = new URL(relSrc, secondUrl).href; + } catch { + return null; + } + + return { url }; + } + + private extractM3u8Urls(thirdHtml: string): string[] | null { + const fileField = thirdHtml.match(/file\s*:\s*["']([^"']+)["']/i)?.[1]; + if (!fileField) return null; + + const playerDomains = new Map(); + playerDomains.set('{v1}', 'neonhorizonworkshops.com'); + playerDomains.set('{v2}', 'wanderlynest.com'); + playerDomains.set('{v3}', 'orchidpixelgardens.com'); + playerDomains.set('{v4}', 'cloudnestra.com'); + + const rawUrls = fileField.split(/\s+or\s+/i); + + const m3u8Urls = rawUrls.map((template) => { + let url = template; + for (const [placeholder, domain] of playerDomains.entries()) { + url = url.replace(placeholder, domain); + } + if (url.includes('{') || url.includes('}')) { + return null; // Return null if any placeholder remains unresolved + } + return url; + }); + + const filteredM3u8Urls = m3u8Urls.filter((url): url is string => url !== null); + + return filteredM3u8Urls.length > 0 ? filteredM3u8Urls : null; + } + + /** + * Return empty result with diagnostic + */ + private emptyResult( + message: string, + media: ProviderMediaObject + ): ProviderResult { + return { + sources: [], + subtitles: [], + diagnostics: [ + { + code: 'PROVIDER_ERROR', + message: `${this.name}: ${message}. Note that VidSrc blocks all kinds of VPN IPs, so if you are using one, try disabling it and see if that helps.`, + field: '', + severity: 'error' + } + ] + }; + } + + /** + * Health check + */ + async healthCheck(): Promise { + try { + const response = await axios.head(this.BASE_URL, { + timeout: 5000, + headers: this.HEADERS + }); + return response.status === 200; + } catch { + return false; + } + } +} From bf9a4d3b78f17328b744dcd8fd7b020d88bbac10 Mon Sep 17 00:00:00 2001 From: Anon <206556099+An0n-01@users.noreply.github.com> Date: Sun, 22 Feb 2026 12:17:56 +0100 Subject: [PATCH 2/3] chore: remove console --- src/providers/rgshows/rgshows.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/providers/rgshows/rgshows.ts b/src/providers/rgshows/rgshows.ts index e332888..90bc725 100644 --- a/src/providers/rgshows/rgshows.ts +++ b/src/providers/rgshows/rgshows.ts @@ -51,7 +51,6 @@ export class RgShowsProvider extends BaseProvider { try { // Build page URL const pageUrl = this.buildPageUrl(media); - this.console.log(`Fetching URL: ${pageUrl}`); // Fetch page json const data = await this.fetchPage(pageUrl, media); From 60f9a9dff09edfc074d8a9b98298f147d7251eb1 Mon Sep 17 00:00:00 2001 From: Anon <206556099+An0n-01@users.noreply.github.com> Date: Sun, 22 Feb 2026 12:24:05 +0100 Subject: [PATCH 3/3] fix: prettier --- .github/PULL_REQUEST_TEMPLATE.md | 47 +++++++++++++++----------------- .github/SECURITY.md | 17 ++++++------ src/providers/vidsrc/vidsrc.ts | 14 ++++++---- 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 87efb85..c887dcb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -37,9 +37,9 @@ Closes # -- -- -- +- +- +- ## Provider Information (if applicable) @@ -47,12 +47,12 @@ Closes # ### Provider Details -- **Provider Name**: -- **Provider ID**: -- **Base URL**: -- **Content Types Supported**: - - [ ] Movies - - [ ] TV Shows +- **Provider Name**: +- **Provider ID**: +- **Base URL**: +- **Content Types Supported**: + - [ ] Movies + - [ ] TV Shows ### Provider Testing @@ -61,14 +61,14 @@ Closes # **Tested with the following TMDB IDs:** - Movies: - - [ ] TMDB ID: 550 (Fight Club) - - [ ] TMDB ID: _____ (Your test movie) - + - [ ] TMDB ID: 550 (Fight Club) + - [ ] TMDB ID: **\_** (Your test movie) - TV Shows: - - [ ] TMDB ID: 1399/1/1 (Game of Thrones S01E01) - - [ ] TMDB ID: _____ (Your test episode) + - [ ] TMDB ID: 1399/1/1 (Game of Thrones S01E01) + - [ ] TMDB ID: **\_** (Your test episode) **Test Results:** + ``` @@ -120,15 +120,16 @@ Paste relevant API response or logs here **Test Configuration:** -- Node.js version: -- Operating System: + +- Node.js version: +- Operating System: - Cache type used: (memory/redis) **Testing Steps:** -1. -2. -3. +1. +2. +3. ### Test Output @@ -156,27 +157,23 @@ Paste test output here **Details:** - ### Breaking Changes **Details:** - ### Dependencies -- -- +- +- ## Reviewer Notes - - --- ## For Maintainers diff --git a/.github/SECURITY.md b/.github/SECURITY.md index d0288a8..c7c5b1e 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -25,6 +25,7 @@ Before reporting a security vulnerability, please determine where the issue orig #### 1. Framework-Level Vulnerabilities If the vulnerability is related to: + - Core routing or API endpoints - Proxy system implementation - TMDB integration @@ -67,10 +68,10 @@ When reporting a vulnerability, please include: 5. **Proof of Concept**: Code or requests demonstrating the vulnerability 6. **Suggested Fix**: If you have ideas on how to fix it (optional) 7. **Environment Details**: - - CinePro Core version - - @omss/framework version - - Node.js version - - Operating system + - CinePro Core version + - @omss/framework version + - Node.js version + - Operating system ### Response Timeline @@ -153,9 +154,9 @@ Our dependencies include: ```json { - "@omss/framework": "^1.1.10", // Core backend logic - "crypto-js": "^4.2.0", // Cryptographic utilities - "dotenv": "^16.4.5" // Environment configuration + "@omss/framework": "^1.1.10", // Core backend logic + "crypto-js": "^4.2.0", // Cryptographic utilities + "dotenv": "^16.4.5" // Environment configuration } ``` @@ -199,4 +200,4 @@ CinePro Core is provided "as is" without warranty. See the [MIT License](../LICE --- -**Thank you for helping keep CinePro Core and its users safe! 🔒** \ No newline at end of file +**Thank you for helping keep CinePro Core and its users safe! 🔒** diff --git a/src/providers/vidsrc/vidsrc.ts b/src/providers/vidsrc/vidsrc.ts index 8262d23..0b65770 100644 --- a/src/providers/vidsrc/vidsrc.ts +++ b/src/providers/vidsrc/vidsrc.ts @@ -93,10 +93,12 @@ export class VidSrcProvider extends BaseProvider { }), type: 'hls', // m3u8 = HLS streaming quality: `up to HD`, // VidSrc does not provide explicit quality labels, so we use a generic one - audioTracks: [{ - label: 'English', - language: 'eng' - }], // No audio track info available + audioTracks: [ + { + label: 'English', + language: 'eng' + } + ], // No audio track info available provider: { id: this.id, name: this.name @@ -225,7 +227,9 @@ export class VidSrcProvider extends BaseProvider { return url; }); - const filteredM3u8Urls = m3u8Urls.filter((url): url is string => url !== null); + const filteredM3u8Urls = m3u8Urls.filter( + (url): url is string => url !== null + ); return filteredM3u8Urls.length > 0 ? filteredM3u8Urls : null; }