feat: add RgShows

This commit is contained in:
WebStreamr 2026-01-09 15:33:50 +00:00
parent 4bec9e556f
commit dcad7db29c
No known key found for this signature in database
16 changed files with 237 additions and 3 deletions

View file

@ -0,0 +1,20 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { RgShows } from './RgShows';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new RgShows(new FetcherMock(`${__dirname}/__fixtures__/RgShows`))]);
const ctx = createTestContext();
describe('RgShows', () => {
test('handle one battle after another', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/movie/1054867'))).toMatchSnapshot();
});
test('handle stranger things s05e01', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/tv/66732/5/1'))).toMatchSnapshot();
});
});

45
src/extractor/RgShows.ts Normal file
View file

@ -0,0 +1,45 @@
import { Context, Format, Meta, UrlResult } from '../types';
import { guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
interface RgShowsApiData {
stream: {
url: string;
};
}
export class RgShows extends Extractor {
public readonly id = 'rgshows';
public readonly label = 'RgShows';
public override readonly ttl: number = 10800000; // 3h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/rgshows/);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
const nonSubDomainUrl = new URL(url);
nonSubDomainUrl.hostname = nonSubDomainUrl.hostname.split('.').slice(-2).join('.');
const headers = { 'Referer': `${nonSubDomainUrl.origin}/`, 'Origin': nonSubDomainUrl.origin, 'User-Agent': 'Mozilla' };
const data = await this.fetcher.json(ctx, url, { headers, noProxy: true }) as RgShowsApiData;
const playlistUrl = new URL(data.stream.url);
return [
{
url: playlistUrl,
format: Format.hls,
label: this.label,
ttl: this.ttl,
meta: {
...meta,
height: await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, url, { headers, noProxy: true }),
},
requestHeaders: headers,
},
];
};
}

View file

@ -0,0 +1 @@
{"stream":{"url":"https://i-arch-400.lethe399key.com/stream2/i-arch-400/01df0d24fc785e08a44241ed17dc9aef/MJTMsp1RshGTygnMNRUR2N2MSlnWXZEdMNDZzQWe5MDZzMmdZJTO1R2RWVHZDljekhkSsl1VwYnWtx2cihVT2pFVNlnT6FVNPR1a04kMJlnTUJkaO1WWzklaNFTTHV1daRUW04ERFhnWUFVP:1767971011:193.233.230.8:338a99d2e898667664ed62657ac0b65a05ed7308fe98419b3a65225ee36a7ae1/index.m3u8"}}

View file

@ -0,0 +1 @@
{"stream":{"url":"https://i-cdn-0.lethe399key.com/stream2/i-cdn-0/01df0d24fc785e08a44241ed17dc9aef/MJTMsp1RshGTygnMNRUR2N2MSlnWXZEdMNDZzQWe5MDZzMmdZJTO1R2RWVHZDljekhkSsl1VwYnWtx2cihVT290VZBjTH1kMORVR390RFRTT6lFMPRUS18EVNhnWUtWMapWQ5l1VRJjTHlUP:1767971582:193.233.230.8:3e079e35a7d560d1b37aa2a38f4b3e6cf2d6449ab480651c4c3557eda220addd/index.m3u8"}}

View file

@ -0,0 +1,9 @@
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=400000,RESOLUTION=640x358
./360/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=858x482
./480/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=1280x720
./720/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1920x1080
./1080/index.m3u8

View file

@ -0,0 +1,9 @@
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=400000,RESOLUTION=640x358
./360/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=858x482
./480/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=1280x720
./720/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1920x1080
./1080/index.m3u8

View file

@ -0,0 +1,43 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`RgShows handle one battle after another 1`] = `
[
{
"format": "hls",
"label": "RgShows",
"meta": {
"countryCodes": [],
"extractorId": "rgshows",
"height": 1080,
},
"requestHeaders": {
"Origin": "https://rgshows.ru",
"Referer": "https://rgshows.ru/",
"User-Agent": "Mozilla",
},
"ttl": 10800000,
"url": "https://i-arch-400.lethe399key.com/stream2/i-arch-400/01df0d24fc785e08a44241ed17dc9aef/MJTMsp1RshGTygnMNRUR2N2MSlnWXZEdMNDZzQWe5MDZzMmdZJTO1R2RWVHZDljekhkSsl1VwYnWtx2cihVT2pFVNlnT6FVNPR1a04kMJlnTUJkaO1WWzklaNFTTHV1daRUW04ERFhnWUFVP:1767971011:193.233.230.8:338a99d2e898667664ed62657ac0b65a05ed7308fe98419b3a65225ee36a7ae1/index.m3u8",
},
]
`;
exports[`RgShows handle stranger things s05e01 1`] = `
[
{
"format": "hls",
"label": "RgShows",
"meta": {
"countryCodes": [],
"extractorId": "rgshows",
"height": 1080,
},
"requestHeaders": {
"Origin": "https://rgshows.ru",
"Referer": "https://rgshows.ru/",
"User-Agent": "Mozilla",
},
"ttl": 10800000,
"url": "https://i-cdn-0.lethe399key.com/stream2/i-cdn-0/01df0d24fc785e08a44241ed17dc9aef/MJTMsp1RshGTygnMNRUR2N2MSlnWXZEdMNDZzQWe5MDZzMmdZJTO1R2RWVHZDljekhkSsl1VwYnWtx2cihVT290VZBjTH1kMORVR390RFRTT6lFMPRUS18EVNhnWUtWMapWQ5l1VRJjTHlUP:1767971582:193.233.230.8:3e079e35a7d560d1b37aa2a38f4b3e6cf2d6449ab480651c4c3557eda220addd/index.m3u8",
},
]
`;

View file

@ -11,6 +11,7 @@ import { HubCloud } from './HubCloud';
import { KinoGer } from './KinoGer';
import { LuluStream } from './LuluStream';
import { Mixdrop } from './Mixdrop';
import { RgShows } from './RgShows';
import { SaveFiles } from './SaveFiles';
import { StreamEmbed } from './StreamEmbed';
import { Streamtape } from './Streamtape';
@ -38,6 +39,7 @@ export const createExtractors = (fetcher: Fetcher): Extractor[] => {
new KinoGer(fetcher),
new LuluStream(fetcher),
new Mixdrop(fetcher),
new RgShows(fetcher),
new SaveFiles(fetcher),
new StreamEmbed(fetcher),
new Streamtape(fetcher),

View file

@ -0,0 +1,23 @@
import { createTestContext } from '../test';
import { FetcherMock, TmdbId } from '../utils';
import { RgShows } from './RgShows';
const ctx = createTestContext();
describe('RgShows', () => {
let source: RgShows;
beforeEach(() => {
source = new RgShows(new FetcherMock(`${__dirname}/__fixtures__/RgShows`));
});
test('handle stranger things s05e01', async () => {
const streams = await source.handle(ctx, 'series', new TmdbId(66732, 5, 1));
expect(streams).toMatchSnapshot();
});
test('handle one battle after another', async () => {
const streams = await source.handle(ctx, 'movie', new TmdbId(1054867, undefined, undefined));
expect(streams).toMatchSnapshot();
});
});

42
src/source/RgShows.ts Normal file
View file

@ -0,0 +1,42 @@
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, getTmdbNameAndYear, Id } from '../utils';
import { Source, SourceResult } from './Source';
export class RgShows extends Source {
public readonly id = 'rgshows';
public readonly label = 'RgShows';
public readonly contentTypes: ContentType[] = ['movie', 'series'];
public readonly countryCodes: CountryCode[] = [CountryCode.en];
public readonly baseUrl = 'https://rgshows.ru';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const [name, year] = await getTmdbNameAndYear(ctx, this.fetcher, tmdbId);
let title: string = name;
if (tmdbId.season) {
title += ` ${tmdbId.formatSeasonAndEpisode()}`;
} else {
title += ` (${year})`;
}
const url = tmdbId.season
? new URL(`https://api.rgshows.ru/main/tv/${tmdbId.id}/${tmdbId.season}/${tmdbId.episode}`, this.baseUrl)
: new URL(`https://api.rgshows.ru/main/movie/${tmdbId.id}`, this.baseUrl);
return [{ url, meta: { countryCodes: [CountryCode.multi], title } }];
};
}

View file

@ -0,0 +1 @@
{"adult":false,"backdrop_path":"/zpEWFNqoN8Qg1SzMMHmaGyOBTdW.jpg","belongs_to_collection":null,"budget":175000000,"genres":[{"id":53,"name":"Thriller"},{"id":80,"name":"Crime"},{"id":28,"name":"Action"}],"homepage":"https://www.onebattleafteranothermovie.com","id":1054867,"imdb_id":"tt30144839","origin_country":["US"],"original_language":"en","original_title":"One Battle After Another","overview":"Washed-up revolutionary Bob exists in a state of stoned paranoia, surviving off-grid with his spirited, self-reliant daughter, Willa. When his evil nemesis resurfaces after 16 years and she goes missing, the former radical scrambles to find her, father and daughter both battling the consequences of his past.","popularity":88.9734,"poster_path":"/r4uXvqCeco0KmO0CjlhXuTEFuSE.jpg","production_companies":[{"id":174,"logo_path":"/ingPVoHnINIrFR7WHmYux1f5pES.png","name":"Warner Bros. Pictures","origin_country":"US"},{"id":178,"logo_path":null,"name":"Ghoulardi Film Company","origin_country":"US"},{"id":216687,"logo_path":"/kKVYqekveOvLK1IgqdJojLjQvtu.png","name":"Domain Entertainment","origin_country":"US"}],"production_countries":[{"iso_3166_1":"US","name":"United States of America"}],"release_date":"2025-09-23","revenue":205168736,"runtime":162,"spoken_languages":[{"english_name":"English","iso_639_1":"en","name":"English"},{"english_name":"French","iso_639_1":"fr","name":"Français"},{"english_name":"Spanish","iso_639_1":"es","name":"Español"},{"english_name":"Telugu","iso_639_1":"te","name":"తెలుగు"}],"status":"Released","tagline":"Some search for battle, others are born into it...","title":"One Battle After Another","video":false,"vote_average":7.462,"vote_count":2171}

View file

@ -0,0 +1 @@
{"adult":false,"backdrop_path":"/8zbAoryWbtH0DKdev8abFAjdufy.jpg","created_by":[{"id":1179422,"credit_id":"57599b039251410a99001cce","name":"Ross Duffer","original_name":"Ross Duffer","gender":2,"profile_path":"/kN1HdFViQkcJOQlNcvvFJIx9Uju.jpg"},{"id":1179419,"credit_id":"57599b0e925141378a002c87","name":"Matt Duffer","original_name":"Matt Duffer","gender":2,"profile_path":"/kXO5CnSxC0znMAICGxnPeuGP73U.jpg"}],"episode_run_time":[],"first_air_date":"2016-07-15","genres":[{"id":10765,"name":"Sci-Fi & Fantasy"},{"id":9648,"name":"Mystery"},{"id":10759,"name":"Action & Adventure"}],"homepage":"https://www.netflix.com/title/80057281","id":66732,"in_production":false,"languages":["en"],"last_air_date":"2025-12-31","last_episode_to_air":{"id":5607051,"name":"Chapter Eight: The Rightside Up","overview":"As Vecna prepares to destroy the world as we know it, the party must put everything on the line to defeat him once and for all.","vote_average":7.113,"vote_count":80,"air_date":"2025-12-31","episode_number":8,"episode_type":"finale","production_code":"","runtime":129,"season_number":5,"show_id":66732,"still_path":"/kP23RWbUWM6vGhT9PxFyP5VT3y4.jpg"},"name":"Stranger Things","next_episode_to_air":null,"networks":[{"id":213,"logo_path":"/wwemzKWzjKYJFfCeiB57q3r4Bcm.png","name":"Netflix","origin_country":""}],"number_of_episodes":42,"number_of_seasons":5,"origin_country":["US"],"original_language":"en","original_name":"Stranger Things","overview":"When a young boy vanishes, a small town uncovers a mystery involving secret experiments, terrifying supernatural forces, and one strange little girl.","popularity":1622.2589,"poster_path":"/uOOtwVbSr4QDjAGIifLDwpb2Pdl.jpg","production_companies":[{"id":2575,"logo_path":"/9YJrHYlcfHtwtulkFMAies3aFEl.png","name":"21 Laps Entertainment","origin_country":"US"},{"id":170090,"logo_path":null,"name":"Monkey Massacre Productions","origin_country":"US"},{"id":184664,"logo_path":"/4qv40ryAKUzvwptbv13eXUAl1Wm.png","name":"Upside Down Pictures","origin_country":"US"}],"production_countries":[{"iso_3166_1":"US","name":"United States of America"}],"seasons":[{"air_date":"2016-07-15","episode_count":8,"id":77680,"name":"Season 1","overview":"Strange things are afoot in Hawkins, Indiana, where a young boy's sudden disappearance unearths a young girl with otherworldly powers.","poster_path":"/hb4KmdA4F1XMnf0vVjevPKKBEjV.jpg","season_number":1,"vote_average":8.4},{"air_date":"2017-10-27","episode_count":9,"id":83248,"name":"Stranger Things 2","overview":"It's been nearly a year since Will's strange disappearance. But life's hardly back to normal in Hawkins. Not even close.","poster_path":"/74nFJmiapxKuUBXRbSu6VqGGcuo.jpg","season_number":2,"vote_average":8.2},{"air_date":"2019-07-04","episode_count":8,"id":115216,"name":"Stranger Things 3","overview":"Budding romance. A brand-new mall. And rabid rats running toward danger. It's the summer of 1985 in Hawkins ... and one summer can change everything.","poster_path":"/sDms9g40ZBhjMIfX9YqqaqId8sK.jpg","season_number":3,"vote_average":8.1},{"air_date":"2022-05-27","episode_count":9,"id":163313,"name":"Stranger Things 4","overview":"Darkness returns to Hawkins just in time for spring break, igniting fresh terror, disturbing memories — and an ominous new threat.","poster_path":"/zvGTZYDCoMSMIBkXExxRxLYimqN.jpg","season_number":4,"vote_average":8.5},{"air_date":"2025-11-26","episode_count":8,"id":402292,"name":"Stranger Things 5","overview":"The fall of 1987. Hawkins is scarred by rifts. Vecna has vanished and the government has placed the town under military quarantine, forcing Eleven back into hiding. To end this nightmare, they'll need everyone together, one last time.","poster_path":"/AaLrOh33YLkK1WLEB8Uml7FL8fm.jpg","season_number":5,"vote_average":7.5}],"spoken_languages":[{"english_name":"English","iso_639_1":"en","name":"English"}],"status":"Ended","tagline":"One last adventure.","type":"Scripted","vote_average":8.591,"vote_count":19952}

View file

@ -0,0 +1,29 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`RgShows handle one battle after another 1`] = `
[
{
"meta": {
"countryCodes": [
"multi",
],
"title": "One Battle After Another (2025)",
},
"url": "https://api.rgshows.ru/main/movie/1054867",
},
]
`;
exports[`RgShows handle stranger things s05e01 1`] = `
[
{
"meta": {
"countryCodes": [
"multi",
],
"title": "Stranger Things S05E01",
},
"url": "https://api.rgshows.ru/main/tv/66732/5/1",
},
]
`;

View file

@ -13,6 +13,7 @@ import { MegaKino } from './MegaKino';
import { MeineCloud } from './MeineCloud';
import { MostraGuarda } from './MostraGuarda';
import { Movix } from './Movix';
import { RgShows } from './RgShows';
import { Source } from './Source';
import { StreamKiste } from './StreamKiste';
import { VerHdLink } from './VerHdLink';
@ -26,6 +27,7 @@ export const createSources = (fetcher: Fetcher): Source[] => {
return [
// multi
new FourKHDHub(fetcher),
new RgShows(fetcher),
new VixSrc(fetcher),
// AL
new Kokoshka(fetcher),

View file

@ -42,6 +42,7 @@ type ProxyConfig = Pick<AxiosRequestConfig, 'httpAgent' | 'httpsAgent' | 'proxy'
export type CustomRequestConfig = AxiosRequestConfig & {
minCacheTtl?: number;
noProxy?: boolean;
noProxyHeaders?: boolean;
queueLimit?: number;
queueTimeout?: number;
@ -122,7 +123,7 @@ export class Fetcher {
}
protected async fetchWithTimeout(ctx: Context, url: URL, requestConfig?: CustomRequestConfig, tryCount = 0): Promise<AxiosResponse> {
const proxyUrl = this.getProxyForUrl(ctx, url);
const proxyUrl = requestConfig?.noProxy ? null : this.getProxyForUrl(ctx, url);
let message = `Fetch ${requestConfig?.method ?? 'GET'} ${url}`;
/* istanbul ignore if */

View file

@ -24,6 +24,11 @@ exports[`buildManifest default manifest 1`] = `
"title": "German 🇩🇪 (Einschalten, KinoGer, MegaKino, MeineCloud, StreamKiste)",
"type": "checkbox",
},
{
"key": "en",
"title": "English 🇺🇸 (RgShows)",
"type": "checkbox",
},
{
"key": "es",
"title": "Castilian Spanish 🇪🇸 (CineHDPlus, Cuevana, HomeCine, VerHdLink)",
@ -74,9 +79,9 @@ exports[`buildManifest default manifest 1`] = `
],
"description": "Provides HTTP URLs from streaming websites. Configure add-on for additional languages. Add MediaFlow proxy for more URLs.
Supported languages: Albanian, German, Castilian Spanish, French, Hindi, Italian, Latin American Spanish
Supported languages: Albanian, German, English, Castilian Spanish, French, Hindi, Italian, Latin American Spanish
Supported sources: 4KHDHub, VixSrc, Kokoshka, CineHDPlus, Cuevana, HomeCine, VerHdLink, Einschalten, KinoGer, MegaKino, MeineCloud, StreamKiste, Frembed, FrenchCloud, Movix, Eurostreaming, MostraGuarda
Supported sources: 4KHDHub, RgShows, VixSrc, Kokoshka, CineHDPlus, Cuevana, HomeCine, VerHdLink, Einschalten, KinoGer, MegaKino, MeineCloud, StreamKiste, Frembed, FrenchCloud, Movix, Eurostreaming, MostraGuarda
Supported extractors: ",
"id": "webstreamr",