refactor: use proper prototype class methods

This commit is contained in:
WebStreamr 2025-06-13 21:26:26 +02:00
parent 0e6558e0b1
commit 4bc25692a8
No known key found for this signature in database
27 changed files with 83 additions and 59 deletions

View file

@ -18,7 +18,7 @@ export class ConfigureController {
this.router.get('/:config/configure', this.getConfigure.bind(this));
}
private readonly getConfigure = (req: Request, res: Response) => {
private getConfigure(req: Request, res: Response) {
const config: Config = JSON.parse(req.params['config'] || JSON.stringify(getDefaultConfig()));
const manifest = buildManifest(this.sources, config);

View file

@ -17,7 +17,7 @@ export class ManifestController {
this.router.get('/:config/manifest.json', this.getManifest.bind(this));
}
private readonly getManifest = (req: Request, res: Response) => {
private getManifest(req: Request, res: Response) {
const config: Config = JSON.parse(req.params['config'] || JSON.stringify(getDefaultConfig()));
const manifest = buildManifest(this.sources, config);

View file

@ -22,7 +22,7 @@ export class StreamController {
this.router.get('/:config/stream/:type/:id.json', this.getStream.bind(this));
}
private readonly getStream = async (req: Request, res: Response) => {
private async getStream(req: Request, res: Response) {
const config: Config = req.params['config'] ? JSON.parse(req.params['config']) : getDefaultConfig();
const type: ContentType = (req.params['type'] || '') as ContentType;
const id: string = req.params['id'] || '';

View file

@ -18,15 +18,17 @@ export class DoodStream extends Extractor {
this.fetcher = fetcher;
}
public readonly supports = (_ctx: Context, url: URL): boolean => null !== url.host.match(/dood|do[0-9]go|dooodster|dooood/);
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/dood|do[0-9]go|dooodster|dooood/);
};
public override readonly normalize = (url: URL): URL => {
public override normalize(url: URL): URL {
const videoId = url.pathname.split('/').slice(-1)[0] as string;
return new URL(`http://dood.to/e/${videoId}`);
};
protected readonly extractInternal = async (ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> => {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, new URL(url));
const passMd5Match = html.match(/\/pass_md5\/[\w-]+\/([\w-]+)/);

View file

@ -18,11 +18,13 @@ export class Dropload extends Extractor {
this.fetcher = fetcher;
}
public readonly supports = (_ctx: Context, url: URL): boolean => null !== url.host.match(/dropload/);
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/dropload/);
}
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/e/', '/').replace('/embed-', '/'));
protected readonly extractInternal = async (ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> => {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
if (html.includes('File Not Found')) {

View file

@ -17,9 +17,11 @@ export class ExternalUrl extends Extractor {
this.fetcher = fetcher;
}
public readonly supports = (ctx: Context, url: URL): boolean => showExternalUrls(ctx.config) && null !== url.host.match(/.*/);
public supports(ctx: Context, url: URL): boolean {
return showExternalUrls(ctx.config) && null !== url.host.match(/.*/);
}
protected readonly extractInternal = async (ctx: Context, url: URL, countryCode: CountryCode, title: string | undefined): Promise<UrlResult[]> => {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title: string | undefined): Promise<UrlResult[]> {
try {
// Make sure the URL is accessible, but avoid causing noise and delays doing this
await this.fetcher.head(ctx, url, { noFlareSolverr: true, timeout: 1000 });

View file

@ -8,13 +8,15 @@ export abstract class Extractor {
public readonly ttl: number = 900000; // 15m
public abstract readonly supports: (ctx: Context, url: URL) => boolean;
public abstract supports(ctx: Context, url: URL): boolean;
public readonly normalize = (url: URL): URL => url;
public normalize(url: URL): URL {
return url;
};
protected abstract readonly extractInternal: (ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined) => Promise<UrlResult[]>;
protected abstract extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]>;
public readonly extract = async (ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> => {
public async extract(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> {
try {
return await this.extractInternal(ctx, url, countryCode, title);
} catch (error) {

View file

@ -14,7 +14,7 @@ export class ExtractorRegistry {
this.urlResultCache = new TTLCache({ max: 1024 });
}
public readonly handle = async (ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> => {
public async handle(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> {
const extractor = this.extractors.find(extractor => extractor.supports(ctx, url));
if (!extractor) {
return [];

View file

@ -16,9 +16,11 @@ export class Fsst extends Extractor {
this.fetcher = fetcher;
}
public readonly supports = (_ctx: Context, url: URL): boolean => null !== url.host.match(/fsst/);
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/fsst/);
};
protected readonly extractInternal = async (ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> => {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const $ = cheerio.load(html);

View file

@ -17,11 +17,15 @@ export class KinoGer extends Extractor {
this.fetcher = fetcher;
}
public readonly supports = (_ctx: Context, url: URL): boolean => null !== url.host.match(/kinoger\.re|shiid4u\.upn\.one|moflix\.upns\.xyz|player\.upn\.one|wasuytm\.store|ultrastream\.online/);
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/kinoger\.re|shiid4u\.upn\.one|moflix\.upns\.xyz|player\.upn\.one|wasuytm\.store|ultrastream\.online/);
}
public override readonly normalize = (url: URL): URL => new URL(`${url.origin}/api/v1/video?id=${url.hash.slice(1)}`);
public override normalize(url: URL): URL {
return new URL(`${url.origin}/api/v1/video?id=${url.hash.slice(1)}`);
}
protected readonly extractInternal = async (ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> => {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const hexData = await this.fetcher.text(ctx, url, { headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' } });
const encrypted = Buffer.from(hexData, 'hex');

View file

@ -20,9 +20,11 @@ export class Soaper extends Extractor {
this.fetcher = fetcher;
}
public readonly supports = (_ctx: Context, url: URL): boolean => null !== url.host.match(/soaper/) && null !== url.pathname.match(/^\/(episode|movie)_/);
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/soaper/) && null !== url.pathname.match(/^\/(episode|movie)_/);
}
protected readonly extractInternal = async (ctx: Context, url: URL, countryCode: CountryCode, title: string | undefined): Promise<UrlResult[]> => {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title: string | undefined): Promise<UrlResult[]> {
const movieOrEpisodeId = (url.pathname.match(/\/\w+_(\w+)/) as string[])[1] as string;
const form = new URLSearchParams();

View file

@ -18,11 +18,15 @@ export class SuperVideo extends Extractor {
this.fetcher = fetcher;
}
public readonly supports = (_ctx: Context, url: URL): boolean => null !== url.host.match(/supervideo/);
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/supervideo/);
}
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/e/', '/').replace('/embed-', '/'));
public override normalize(url: URL): URL {
return new URL(url.href.replace('/e/', '/').replace('/embed-', '/'));
}
protected readonly extractInternal = async (ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> => {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
if (html.includes('This video can be watched as embed only')) {

View file

@ -18,9 +18,11 @@ export class VidSrc extends Extractor {
this.fetcher = fetcher;
}
public readonly supports = (_ctx: Context, url: URL): boolean => null !== url.host.match(/vidsrc/);
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/vidsrc/);
}
protected readonly extractInternal = async (ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> => {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const $ = cheerio.load(html);

View file

@ -19,7 +19,7 @@ export class CineHDPlus implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(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

@ -19,7 +19,7 @@ export class Eurostreaming implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(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

@ -18,7 +18,7 @@ export class Frembed implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const apiUrl = new URL(`https://frembed.space/api/series?id=${tmdbId.id}&sa=${tmdbId.season}&epi=${tmdbId.episode}&idType=tmdb`);

View file

@ -19,7 +19,7 @@ export class FrenchCloud implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`https://frenchcloud.cam/movie/${imdbId.id}`);

View file

@ -21,7 +21,7 @@ export class KinoGer implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const [keyword, year] = await this.getKeywordAndYear(ctx, tmdbId);

View file

@ -19,7 +19,7 @@ export class MeineCloud implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`https://meinecloud.click/movie/${imdbId.id}`);

View file

@ -19,7 +19,7 @@ export class MostraGuarda implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`https://mostraguarda.stream/movie/${imdbId.id}`);

View file

@ -21,7 +21,7 @@ export class Soaper implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const [keyword, year, hrefPrefix] = await this.getKeywordYearAndHrefPrefix(ctx, tmdbId);

View file

@ -19,7 +19,7 @@ export class StreamKiste implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(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

@ -19,7 +19,7 @@ export class VerHdLink implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`https://verhdlink.cam/movie/${imdbId.id}`);

View file

@ -20,7 +20,7 @@ export class VidSrc implements Source {
this.fetcher = fetcher;
}
public readonly handle = async (ctx: Context, _type: string, id: Id): Promise<SourceResult[]> => {
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const url = imdbId.season

View file

@ -18,5 +18,5 @@ export interface Source {
readonly countryCodes: CountryCode[];
readonly handle: (ctx: Context, type: ContentType, id: Id) => Promise<(SourceResult[])>;
handle(ctx: Context, type: ContentType, id: Id): Promise<(SourceResult[])>;
}

View file

@ -64,19 +64,19 @@ export class Fetcher {
this.httpCache = new TTLCache();
}
public readonly text = async (ctx: Context, url: URL, init?: CustomRequestInit): Promise<string> => {
public async text(ctx: Context, url: URL, init?: CustomRequestInit): Promise<string> {
return (await this.cachedFetch(ctx, url, init)).body;
};
public readonly textPost = async (ctx: Context, url: URL, body: string, init?: CustomRequestInit): Promise<string> => {
public async textPost(ctx: Context, url: URL, body: string, init?: CustomRequestInit): Promise<string> {
return (await this.cachedFetch(ctx, url, { ...init, method: 'POST', body })).body;
};
public readonly head = async (ctx: Context, url: URL, init?: CustomRequestInit): Promise<CachePolicy.Headers> => {
public async head(ctx: Context, url: URL, init?: CustomRequestInit): Promise<CachePolicy.Headers> {
return (await this.cachedFetch(ctx, url, { ...init, method: 'HEAD' })).policy.responseHeaders();
};
public readonly getInit = (ctx: Context, url: URL, init?: CustomRequestInit): CustomRequestInit => {
public getInit(ctx: Context, url: URL, init?: CustomRequestInit): CustomRequestInit {
const cookieString = this.cookieJar.getCookieStringSync(url.href);
const noReferer = init?.noReferer ?? false;
@ -99,7 +99,7 @@ export class Fetcher {
};
};
private readonly handleHttpCacheItem = async (ctx: Context, httpCacheItem: HttpCacheItem, url: URL, init?: CustomRequestInit): Promise<HttpCacheItem> => {
private async handleHttpCacheItem(ctx: Context, httpCacheItem: HttpCacheItem, url: URL, init?: CustomRequestInit): Promise<HttpCacheItem> {
if (httpCacheItem.status && httpCacheItem.status >= 200 && httpCacheItem.status <= 299) {
return httpCacheItem;
}
@ -162,7 +162,7 @@ export class Fetcher {
throw new HttpError(httpCacheItem.status, httpCacheItem.statusText, responseHeaders);
};
private readonly determineTtl = (httpCacheItem: HttpCacheItem): number => {
private determineTtl(httpCacheItem: HttpCacheItem): number {
if (httpCacheItem.status === 200) {
return Math.max(httpCacheItem.policy.timeToLive(), 900000); // 15m at least
}
@ -170,7 +170,7 @@ export class Fetcher {
return httpCacheItem.policy.timeToLive();
};
private readonly headersToObject = (headers: Headers): Record<string, string> => {
private headersToObject(headers: Headers): Record<string, string> {
const obj: Record<string, string> = {};
headers.forEach((value, name) => {
@ -180,7 +180,7 @@ export class Fetcher {
return obj;
};
private readonly cachedFetch = async (ctx: Context, url: URL, init?: CustomRequestInit): Promise<HttpCacheItem> => {
private async cachedFetch(ctx: Context, url: URL, init?: CustomRequestInit): Promise<HttpCacheItem> {
const newInit = this.getInit(ctx, url, init);
const request: CachePolicy.Request = { url: url.href, method: newInit.method ?? 'GET', headers: {} };
@ -206,7 +206,7 @@ export class Fetcher {
return this.handleHttpCacheItem(ctx, httpCacheItem, url, init);
};
private readonly fetchWithTimeout = async (ctx: Context, url: URL, init?: CustomRequestInit): Promise<Response> => {
private async fetchWithTimeout(ctx: Context, url: URL, init?: CustomRequestInit): Promise<Response> {
this.logger.info(`Fetch ${init?.method ?? 'GET'} ${url}`, ctx);
const controller = new AbortController();
@ -222,9 +222,11 @@ export class Fetcher {
return response;
};
private readonly getHostQueueCount = (host: string): number => this.hostQueueCount.get(host) ?? 0;
private getHostQueueCount(host: string): number {
return this.hostQueueCount.get(host) ?? 0;
}
private readonly lockFetchSlot = async (host: string, queueErrorLimit: number) => {
private async lockFetchSlot(host: string, queueErrorLimit: number) {
await this.countMutex.runExclusive(() => {
if (this.getHostQueueCount(host) > queueErrorLimit) {
throw new QueueIsFullError();
@ -234,13 +236,13 @@ export class Fetcher {
});
};
private readonly unlockFetchSlot = async (host: string) => {
private async unlockFetchSlot(host: string) {
await this.countMutex.runExclusive(() => {
this.hostQueueCount.set(host, Math.max(0, this.getHostQueueCount(host) - 1));
});
};
private readonly waitForHostQueueCount = async (host: string, queueLimit: number, queueErrorLimit: number): Promise<void> => {
private async waitForHostQueueCount(host: string, queueLimit: number, queueErrorLimit: number): Promise<void> {
while (this.getHostQueueCount(host) > queueLimit) {
if (this.getHostQueueCount(host) > queueErrorLimit) {
// Very unlikely to happen..
@ -251,7 +253,7 @@ export class Fetcher {
}
};
private readonly queuedFetch = async (ctx: Context, url: URL, init?: CustomRequestInit): Promise<Response> => {
private async queuedFetch(ctx: Context, url: URL, init?: CustomRequestInit): Promise<Response> {
const queueLimit = init?.queueLimit ?? 5;
const queueErrorLimit = init?.queueErrorLimit ?? 10;

View file

@ -24,7 +24,7 @@ export class StreamResolver {
this.extractorRegistry = extractorRegistry;
}
public readonly resolve = async (ctx: Context, sources: Source[], type: ContentType, id: Id): Promise<ResolveResponse> => {
public async resolve(ctx: Context, sources: Source[], type: ContentType, id: Id): Promise<ResolveResponse> {
if (sources.length === 0) {
return {
streams: [
@ -120,7 +120,7 @@ export class StreamResolver {
};
};
private readonly determineTtl = (urlResults: UrlResult[]): number | undefined => {
private determineTtl(urlResults: UrlResult[]): number | undefined {
if (!urlResults.length) {
return 900000; // 15m
}
@ -132,7 +132,7 @@ export class StreamResolver {
return Math.min(...urlResults.map(urlResult => urlResult.ttl as number));
};
private readonly buildUrl = (ctx: Context, urlResult: UrlResult): { externalUrl: string } | { url: string } | { ytId: string } => {
private buildUrl(ctx: Context, urlResult: UrlResult): { externalUrl: string } | { url: string } | { ytId: string } {
if (!urlResult.isExternal) {
return { url: urlResult.url.href };
}
@ -144,7 +144,7 @@ export class StreamResolver {
return { ytId: 'E4WlUXrJgy4' };
};
private readonly buildName = (ctx: Context, urlResult: UrlResult): string => {
private buildName(ctx: Context, urlResult: UrlResult): string {
let name = envGetAppName();
name += urlResult.meta.height ? ` ${urlResult.meta.height}P` : ' N/A';
@ -156,7 +156,7 @@ export class StreamResolver {
return name;
};
private readonly logErrorAndReturnNiceString = (ctx: Context, source: string, error: unknown): string => {
private logErrorAndReturnNiceString(ctx: Context, source: string, error: unknown): string {
if (error instanceof BlockedError) {
if (error.reason === 'cloudflare_challenge') {
this.logger.warn(`${source}: Request was blocked via Cloudflare challenge.`, ctx);
@ -190,7 +190,7 @@ export class StreamResolver {
return `❌ Request failed. Request-id: ${ctx.id}.`;
};
private readonly buildTitle = (ctx: Context, urlResult: UrlResult): string => {
private buildTitle(ctx: Context, urlResult: UrlResult): string {
const titleLines = [];
if (urlResult.meta.title) {