refactor(source): introduce abstract parent class

This commit is contained in:
WebStreamr 2025-08-31 18:52:42 +00:00
parent 3044e41d9b
commit 9da7c07dc6
No known key found for this signature in database
23 changed files with 143 additions and 93 deletions

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getImdbId, Id, ImdbId } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class CineHDPlus implements Source {
export class CineHDPlus extends Source {
public readonly id = 'cinehdplus';
public readonly label = 'CineHDPlus';
@ -18,10 +18,12 @@ export class CineHDPlus implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const seriesPageUrl = await this.fetchSeriesPageUrl(ctx, imdbId);

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, getTmdbNameAndYear, Id, TmdbId } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class Cuevana implements Source {
export class Cuevana extends Source {
public readonly id = 'cuevana';
public readonly label = 'Cuevana';
@ -18,10 +18,12 @@ export class Cuevana implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const [name] = await getTmdbNameAndYear(ctx, this.fetcher, tmdbId, 'es');

View file

@ -1,14 +1,14 @@
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
interface EinschaltenResponse {
releaseName: string;
streamUrl: string;
}
export class Einschalten implements Source {
export class Einschalten extends Source {
public readonly id = 'einschalten';
public readonly label = 'Einschalten';
@ -22,10 +22,12 @@ export class Einschalten implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const { releaseName, streamUrl } = JSON.parse(await this.fetcher.text(ctx, new URL(`/api/movies/${tmdbId.id}/watch`, this.baseUrl))) as EinschaltenResponse;

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, getTmdbNameAndYear, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class Eurostreaming implements Source {
export class Eurostreaming extends Source {
public readonly id = 'eurostreaming';
public readonly label = 'Eurostreaming';
@ -18,10 +18,12 @@ export class Eurostreaming implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const [name] = await getTmdbNameAndYear(ctx, this.fetcher, tmdbId, 'it');

View file

@ -1,9 +1,9 @@
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class Frembed implements Source {
export class Frembed extends Source {
public readonly id = 'frembed';
public readonly label = 'Frembed';
@ -17,10 +17,12 @@ export class Frembed implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const apiUrl = tmdbId.season

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getImdbId, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class FrenchCloud implements Source {
export class FrenchCloud extends Source {
public readonly id = 'frenchcloud';
public readonly label = 'FrenchCloud';
@ -18,10 +18,12 @@ export class FrenchCloud implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`/movie/${imdbId.id}`, this.baseUrl);

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, getTmdbNameAndYear, Id, TmdbId } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class HomeCine implements Source {
export class HomeCine extends Source {
public readonly id = 'homecine';
public readonly label = 'HomeCine';
@ -18,10 +18,12 @@ export class HomeCine implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
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, 'es');

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, getTmdbNameAndYear, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class KinoGer implements Source {
export class KinoGer extends Source {
public readonly id = 'kinoger';
public readonly label = 'KinoGer';
@ -18,10 +18,12 @@ export class KinoGer implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
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, 'de');

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getImdbId, Id, ImdbId } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class MegaKino implements Source {
export class MegaKino extends Source {
public readonly id = 'megakino';
public readonly label = 'MegaKino';
@ -18,10 +18,12 @@ export class MegaKino implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = await this.fetchPageUrl(ctx, imdbId);

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getImdbId, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class MeineCloud implements Source {
export class MeineCloud extends Source {
public readonly id = 'meinecloud';
public readonly label = 'MeineCloud';
@ -18,10 +18,12 @@ export class MeineCloud implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`/movie/${imdbId.id}`, this.baseUrl);

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getImdbId, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class MostraGuarda implements Source {
export class MostraGuarda extends Source {
public readonly id = 'mostraguarda';
public readonly label = 'MostraGuarda';
@ -18,10 +18,12 @@ export class MostraGuarda implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`/movie/${imdbId.id}`, this.baseUrl);

View file

@ -1,13 +1,13 @@
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
interface MovixApiData {
player_links?: { decoded_url: string }[];
}
export class Movix implements Source {
export class Movix extends Source {
public readonly id = 'movix';
public readonly label = 'Movix';
@ -21,10 +21,12 @@ export class Movix implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const apiUrl = tmdbId.season

View file

@ -4,9 +4,9 @@ import { JSDOM } from 'jsdom';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getImdbId, Id, ImdbId } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class PrimeWire implements Source {
export class PrimeWire extends Source {
public readonly id = 'primewire';
public readonly label = 'PrimeWire';
@ -22,10 +22,12 @@ export class PrimeWire implements Source {
private readonly redirectUrlCache = new Map<string, URL>();
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
// We need any non-protected entrypoint to grab the app.js

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, getTmdbNameAndYear, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class Soaper implements Source {
export class Soaper extends Source {
public readonly id = 'soaper';
public readonly label = 'Soaper';
@ -18,10 +18,12 @@ export class Soaper implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
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);

27
src/source/Source.ts Normal file
View file

@ -0,0 +1,27 @@
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Id } from '../utils';
export interface SourceResult {
countryCode: CountryCode;
title?: string;
url: URL;
}
export abstract class Source {
public abstract readonly id: string;
public abstract readonly label: string;
public abstract readonly contentTypes: ContentType[];
public abstract readonly countryCodes: CountryCode[];
public abstract readonly baseUrl: string;
protected abstract handleInternal(ctx: Context, type: ContentType, id: Id): Promise<(SourceResult[])>;
public async handle(ctx: Context, type: ContentType, id: Id): Promise<(SourceResult[])> {
return await this.handleInternal(ctx, type, id);
}
}

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getImdbId, Id, ImdbId } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class StreamKiste implements Source {
export class StreamKiste extends Source {
public readonly id = 'streamkiste';
public readonly label = 'StreamKiste';
@ -18,10 +18,12 @@ export class StreamKiste implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const seriesPageUrl = await this.fetchSeriesPageUrl(ctx, imdbId);

View file

@ -2,9 +2,9 @@ import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getImdbId, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class VerHdLink implements Source {
export class VerHdLink extends Source {
public readonly id = 'verhdlink';
public readonly label = 'VerHdLink';
@ -18,10 +18,12 @@ export class VerHdLink implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`/movie/${imdbId.id}`, this.baseUrl);

View file

@ -1,9 +1,9 @@
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getImdbId, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class VidSrc implements Source {
export class VidSrc extends Source {
public readonly id = 'vidsrc';
public readonly label = 'VidSrc';
@ -17,10 +17,12 @@ export class VidSrc implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const url = imdbId.season

View file

@ -1,9 +1,9 @@
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class VixSrc implements Source {
export class VixSrc extends Source {
public readonly id = 'vixsrc';
public readonly label = 'VixSrc';
@ -17,10 +17,12 @@ export class VixSrc implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const url = tmdbId.season

View file

@ -1,9 +1,9 @@
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, getTmdbNameAndYear, Id } from '../utils';
import { Source, SourceResult } from './types';
import { Source, SourceResult } from './Source';
export class XPrime implements Source {
export class XPrime extends Source {
public readonly id = 'xprime';
public readonly label = 'XPrime';
@ -17,10 +17,12 @@ export class XPrime implements Source {
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
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);

View file

@ -13,14 +13,14 @@ import { MostraGuarda } from './MostraGuarda';
import { Movix } from './Movix';
import { PrimeWire } from './PrimeWire';
import { Soaper } from './Soaper';
import { Source } from './Source';
import { StreamKiste } from './StreamKiste';
import { Source } from './types';
import { VerHdLink } from './VerHdLink';
import { VidSrc } from './VidSrc';
import { VixSrc } from './VixSrc';
import { XPrime } from './XPrime';
export * from './types';
export * from './Source';
export const createSources = (fetcher: Fetcher): Source[] => [
// multi

View file

@ -1,23 +0,0 @@
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Id } from '../utils';
export interface SourceResult {
countryCode: CountryCode;
title?: string;
url: URL;
}
export interface Source {
readonly id: string;
readonly label: string;
readonly contentTypes: ContentType[];
readonly countryCodes: CountryCode[];
readonly baseUrl: string;
handle(ctx: Context, type: ContentType, id: Id): Promise<(SourceResult[])>;
}

View file

@ -79,7 +79,7 @@ describe('resolve', () => {
});
test('adds error info', async () => {
class MockSource implements Source {
class MockSource extends Source {
public readonly id = 'mocksource';
public readonly label = 'MockSource';
@ -90,7 +90,7 @@ describe('resolve', () => {
public readonly baseUrl = 'https://example.com';
public readonly handle = async (): Promise<SourceResult[]> => {
public readonly handleInternal = async (): Promise<SourceResult[]> => {
return [{ countryCode: CountryCode.de, url: new URL('https://example.com') }];
};
}
@ -258,17 +258,25 @@ describe('resolve', () => {
});
test('ignores not found errors', async () => {
const mockHandler: Source = {
id: 'mocksource',
label: 'MockSource',
contentTypes: ['movie'],
countryCodes: [CountryCode.de],
baseUrl: 'https://example.com',
handle: jest.fn().mockRejectedValue(new NotFoundError()),
};
class MockSource extends Source {
public readonly id = 'mocksource';
public readonly label = 'MockSource';
public readonly contentTypes: ContentType[] = ['movie'];
public readonly countryCodes: CountryCode[] = [CountryCode.de];
public readonly baseUrl = 'https://example.com';
public readonly handleInternal = async (): Promise<SourceResult[]> => {
throw new NotFoundError();
};
}
const streamResolver = new StreamResolver(logger, new ExtractorRegistry(logger, createExtractors(fetcher)));
const streams = await streamResolver.resolve(ctx, [mockHandler], 'movie', new ImdbId('tt12345678', undefined, undefined));
const streams = await streamResolver.resolve(ctx, [new MockSource()], 'movie', new ImdbId('tt12345678', undefined, undefined));
expect(streams).toMatchSnapshot();
});