diff --git a/src/controller/StreamController.ts b/src/controller/StreamController.ts index c0322ee..5d30f03 100644 --- a/src/controller/StreamController.ts +++ b/src/controller/StreamController.ts @@ -53,7 +53,7 @@ export class StreamController { const handlerUrlResults = await handler.handle({ ip: req.ip as string }, id); this.logger.info(`${handler.id} returned ${handlerUrlResults.length} urls`); - urlResults.push(...handlerUrlResults); + urlResults.push(...(handlerUrlResults.filter(handlerUrlResult => handlerUrlResult !== undefined))); } catch (err) { this.logger.error(`${handler.id} error: ` + err); } diff --git a/src/embed-extractor/EmbedExtractorRegistry.test.ts b/src/embed-extractor/EmbedExtractorRegistry.test.ts new file mode 100644 index 0000000..08286d3 --- /dev/null +++ b/src/embed-extractor/EmbedExtractorRegistry.test.ts @@ -0,0 +1,25 @@ +import { EmbedExtractorRegistry } from './EmbedExtractorRegistry'; +import { Context } from '../types'; +import { Fetcher } from '../utils'; +jest.mock('../utils/Fetcher'); + +// @ts-expect-error No constructor args needed +const fetcher = new Fetcher(); +const embedExtractors = new EmbedExtractorRegistry(fetcher); + +describe('EmbedExtractorRegistry', () => { + const ctx: Context = { ip: '127.0.0.1' }; + + test('returns undefined when no embed extractor can be found', async () => { + const urlResult = await embedExtractors.handle(ctx, new URL('https://some-url.test'), 'en'); + + expect(urlResult).toBeUndefined(); + }); + + test('returns from memory cache if possible', async () => { + const urlResult1 = await embedExtractors.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), 'de'); + const urlResult2 = await embedExtractors.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), 'de'); + + expect(urlResult2).toBe(urlResult1); + }); +}); diff --git a/src/embed-extractor/EmbedExtractors.ts b/src/embed-extractor/EmbedExtractorRegistry.ts similarity index 69% rename from src/embed-extractor/EmbedExtractors.ts rename to src/embed-extractor/EmbedExtractorRegistry.ts index e95d623..d256726 100644 --- a/src/embed-extractor/EmbedExtractors.ts +++ b/src/embed-extractor/EmbedExtractorRegistry.ts @@ -1,18 +1,24 @@ import TTLCache from '@isaacs/ttlcache'; import { EmbedExtractor } from './types'; import { Context, UrlResult } from '../types'; +import { Fetcher } from '../utils'; +import { Dropload } from './Dropload'; +import { SuperVideo } from './SuperVideo'; -export class EmbedExtractors { +export class EmbedExtractorRegistry { private readonly embedExtractors: EmbedExtractor[]; private readonly urlResultCache: TTLCache; - constructor(embedExtractors: EmbedExtractor[]) { - this.embedExtractors = embedExtractors; + constructor(fetcher: Fetcher) { + this.embedExtractors = [ + new Dropload(fetcher), + new SuperVideo(fetcher), + ]; this.urlResultCache = new TTLCache({ max: 1024 }); } - readonly handle = async (ctx: Context, url: URL, countryCode: string): Promise => { + readonly handle = async (ctx: Context, url: URL, countryCode: string): Promise => { let urlResult = this.urlResultCache.get(url.href); if (urlResult) { return urlResult; @@ -20,7 +26,7 @@ export class EmbedExtractors { const embedExtractor = this.embedExtractors.find(embedExtractor => embedExtractor.supports(url)); if (undefined === embedExtractor) { - throw new Error(`No embed extractor found that supports url ${url}`); + return undefined; } urlResult = await embedExtractor.extract(ctx, url, countryCode); diff --git a/src/embed-extractor/EmbedExtractors.test.ts b/src/embed-extractor/EmbedExtractors.test.ts deleted file mode 100644 index 502c169..0000000 --- a/src/embed-extractor/EmbedExtractors.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { EmbedExtractors } from './EmbedExtractors'; -import { Context } from '../types'; -import { Fetcher } from '../utils'; -import { Dropload } from './Dropload'; -jest.mock('../utils/Fetcher'); - -describe('EmbedExtractors', () => { - const ctx: Context = { ip: '127.0.0.1' }; - - test('throws when no embed extractor can be found', () => { - const embedExtractors = new EmbedExtractors([]); - - expect(embedExtractors.handle(ctx, new URL('https://some-url.test'), 'en')) - .rejects.toThrow('No embed extractor found that supports url https://some-url.test'); - }); - - test('returns from memory cache if possible', async () => { - // @ts-expect-error No constructor args needed - const fetcher = new Fetcher(); - const embedExtractors = new EmbedExtractors([new Dropload(fetcher)]); - - const urlResult1 = await embedExtractors.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), 'de'); - const urlResult2 = await embedExtractors.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), 'de'); - - expect(urlResult2).toBe(urlResult1); - }); -}); diff --git a/src/embed-extractor/index.ts b/src/embed-extractor/index.ts index 2630e13..b057ae4 100644 --- a/src/embed-extractor/index.ts +++ b/src/embed-extractor/index.ts @@ -1,4 +1 @@ -export * from './EmbedExtractors'; -export * from './Dropload'; -export * from './SuperVideo'; -export * from './types'; +export * from './EmbedExtractorRegistry'; diff --git a/src/handler/FrenchCloud.test.ts b/src/handler/FrenchCloud.test.ts index 3f7754d..00cb239 100644 --- a/src/handler/FrenchCloud.test.ts +++ b/src/handler/FrenchCloud.test.ts @@ -1,29 +1,29 @@ import { FrenchCloud } from './FrenchCloud'; import { Fetcher } from '../utils'; -import { Dropload, EmbedExtractors, SuperVideo } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; import { Context } from '../types'; jest.mock('../utils/Fetcher'); // @ts-expect-error No constructor args needed const fetcher = new Fetcher(); -const frenchcloud = new FrenchCloud(fetcher, new EmbedExtractors([new Dropload(fetcher), new SuperVideo(fetcher)])); +const handler = new FrenchCloud(fetcher, new EmbedExtractorRegistry(fetcher)); const ctx: Context = { ip: '127.0.0.1' }; describe('FrenchCloud', () => { test('does not handle non imdb movies', async () => { - const streams = await frenchcloud.handle(ctx, 'kitsu:123'); + const streams = await handler.handle(ctx, 'kitsu:123'); expect(streams).toHaveLength(0); }); test('handles non-existent movies gracefully', async () => { - const streams = await frenchcloud.handle(ctx, 'tt12345678'); + const streams = await handler.handle(ctx, 'tt12345678'); expect(streams).toHaveLength(0); }); test('handle imdb the devil\'s bath', async () => { - const streams = await frenchcloud.handle(ctx, 'tt29141112'); + const streams = (await handler.handle(ctx, 'tt29141112')).filter(stream => stream !== undefined); expect(streams).toHaveLength(2); expect(streams[0]).toStrictEqual({ diff --git a/src/handler/FrenchCloud.ts b/src/handler/FrenchCloud.ts index e47b9e9..47e1c53 100644 --- a/src/handler/FrenchCloud.ts +++ b/src/handler/FrenchCloud.ts @@ -1,7 +1,7 @@ import * as cheerio from 'cheerio'; import { Handler } from './types'; import { Fetcher, parseImdbId } from '../utils'; -import { EmbedExtractors } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; import { Context } from '../types'; export class FrenchCloud implements Handler { @@ -14,9 +14,9 @@ export class FrenchCloud implements Handler { readonly languages = ['fr']; private readonly fetcher: Fetcher; - private readonly embedExtractors: EmbedExtractors; + private readonly embedExtractors: EmbedExtractorRegistry; - constructor(fetcher: Fetcher, embedExtractors: EmbedExtractors) { + constructor(fetcher: Fetcher, embedExtractors: EmbedExtractorRegistry) { this.fetcher = fetcher; this.embedExtractors = embedExtractors; } @@ -34,7 +34,7 @@ export class FrenchCloud implements Handler { $('[data-link!=""]') .map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://'))) .toArray() - .filter(embedUrl => embedUrl.host.match(/(dropload|supervideo)/)) + .filter(embedUrl => !embedUrl.host.match(/frenchcloud/)) .map(embedUrl => this.embedExtractors.handle(ctx, embedUrl, 'fr')), ); }; diff --git a/src/handler/KinoKiste.test.ts b/src/handler/KinoKiste.test.ts index ae9bd97..f804216 100644 --- a/src/handler/KinoKiste.test.ts +++ b/src/handler/KinoKiste.test.ts @@ -1,29 +1,29 @@ import { KinoKiste } from './KinoKiste'; -import { Dropload, EmbedExtractors, SuperVideo } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; import { Fetcher } from '../utils'; import { Context } from '../types'; jest.mock('../utils/Fetcher'); // @ts-expect-error No constructor args needed const fetcher = new Fetcher(); -const kinokiste = new KinoKiste(fetcher, new EmbedExtractors([new Dropload(fetcher), new SuperVideo(fetcher)])); +const handler = new KinoKiste(fetcher, new EmbedExtractorRegistry(fetcher)); const ctx: Context = { ip: '127.0.0.1' }; describe('KinoKiste', () => { test('does not handle non imdb series', async () => { - const streams = await kinokiste.handle(ctx, 'kitsu:123'); + const streams = await handler.handle(ctx, 'kitsu:123'); expect(streams).toHaveLength(0); }); test('handles non-existent series gracefully', async () => { - const streams = await kinokiste.handle(ctx, 'tt12345678:1:1'); + const streams = await handler.handle(ctx, 'tt12345678:1:1'); expect(streams).toHaveLength(0); }); test('handle imdb black mirror s2e4', async () => { - const streams = await kinokiste.handle(ctx, 'tt2085059:2:4'); + const streams = (await handler.handle(ctx, 'tt2085059:2:4')).filter(stream => stream !== undefined); expect(streams).toHaveLength(2); expect(streams[0]).toStrictEqual({ diff --git a/src/handler/KinoKiste.ts b/src/handler/KinoKiste.ts index 21d1eea..83ac7f1 100644 --- a/src/handler/KinoKiste.ts +++ b/src/handler/KinoKiste.ts @@ -1,7 +1,7 @@ import * as cheerio from 'cheerio'; import { Handler } from './types'; import { ImdbId, parseImdbId, Fetcher } from '../utils'; -import { EmbedExtractors } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; import { Context } from '../types'; export class KinoKiste implements Handler { @@ -14,9 +14,9 @@ export class KinoKiste implements Handler { readonly languages = ['de']; private readonly fetcher: Fetcher; - private readonly embedExtractors: EmbedExtractors; + private readonly embedExtractors: EmbedExtractorRegistry; - constructor(fetcher: Fetcher, embedExtractors: EmbedExtractors) { + constructor(fetcher: Fetcher, embedExtractors: EmbedExtractorRegistry) { this.fetcher = fetcher; this.embedExtractors = embedExtractors; } @@ -43,7 +43,7 @@ export class KinoKiste implements Handler { .children('[data-link]') .map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://'))) .toArray() - .filter(embedUrl => embedUrl.host.match(/(dropload|supervideo)/)) + .filter(embedUrl => !embedUrl.host.match(/kinokiste/)) .map(embedUrl => this.embedExtractors.handle(ctx, embedUrl, 'de')), ); }; diff --git a/src/handler/MeineCloud.test.ts b/src/handler/MeineCloud.test.ts index 2864dce..61b569d 100644 --- a/src/handler/MeineCloud.test.ts +++ b/src/handler/MeineCloud.test.ts @@ -1,29 +1,29 @@ import { MeineCloud } from './MeineCloud'; import { Fetcher } from '../utils'; -import { Dropload, EmbedExtractors, SuperVideo } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; import { Context } from '../types'; jest.mock('../utils/Fetcher'); // @ts-expect-error No constructor args needed const fetcher = new Fetcher(); -const meinecloud = new MeineCloud(fetcher, new EmbedExtractors([new Dropload(fetcher), new SuperVideo(fetcher)])); +const handler = new MeineCloud(fetcher, new EmbedExtractorRegistry(fetcher)); const ctx: Context = { ip: '127.0.0.1' }; describe('MeineCloud', () => { test('does not handle non imdb movies', async () => { - const streams = await meinecloud.handle(ctx, 'kitsu:123'); + const streams = await handler.handle(ctx, 'kitsu:123'); expect(streams).toHaveLength(0); }); test('handles non-existent movies gracefully', async () => { - const streams = await meinecloud.handle(ctx, 'tt12345678'); + const streams = await handler.handle(ctx, 'tt12345678'); expect(streams).toHaveLength(0); }); test('handle imdb the devil\'s bath', async () => { - const streams = await meinecloud.handle(ctx, 'tt29141112'); + const streams = (await handler.handle(ctx, 'tt29141112')).filter(stream => stream !== undefined); expect(streams).toHaveLength(2); expect(streams[0]).toStrictEqual({ diff --git a/src/handler/MeineCloud.ts b/src/handler/MeineCloud.ts index 07617d2..a1c5bd1 100644 --- a/src/handler/MeineCloud.ts +++ b/src/handler/MeineCloud.ts @@ -1,7 +1,7 @@ import * as cheerio from 'cheerio'; import { Handler } from './types'; import { Fetcher, parseImdbId } from '../utils'; -import { EmbedExtractors } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; import { Context } from '../types'; export class MeineCloud implements Handler { @@ -14,9 +14,9 @@ export class MeineCloud implements Handler { readonly languages = ['de']; private readonly fetcher: Fetcher; - private readonly embedExtractors: EmbedExtractors; + private readonly embedExtractors: EmbedExtractorRegistry; - constructor(fetcher: Fetcher, embedExtractors: EmbedExtractors) { + constructor(fetcher: Fetcher, embedExtractors: EmbedExtractorRegistry) { this.fetcher = fetcher; this.embedExtractors = embedExtractors; } @@ -34,7 +34,7 @@ export class MeineCloud implements Handler { $('[data-link!=""]') .map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://'))) .toArray() - .filter(embedUrl => embedUrl.host.match(/(dropload|supervideo)/)) + .filter(embedUrl => !embedUrl.host.match(/meinecloud/)) .map(embedUrl => this.embedExtractors.handle(ctx, embedUrl, 'de')), ); }; diff --git a/src/handler/MostraGuarda.test.ts b/src/handler/MostraGuarda.test.ts index f95514e..6ef9192 100644 --- a/src/handler/MostraGuarda.test.ts +++ b/src/handler/MostraGuarda.test.ts @@ -1,29 +1,29 @@ import { MostraGuarda } from './MostraGuarda'; import { Fetcher } from '../utils'; -import { Dropload, EmbedExtractors, SuperVideo } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; import { Context } from '../types'; jest.mock('../utils/Fetcher'); // @ts-expect-error No constructor args needed const fetcher = new Fetcher(); -const mostraguarda = new MostraGuarda(fetcher, new EmbedExtractors([new Dropload(fetcher), new SuperVideo(fetcher)])); +const handler = new MostraGuarda(fetcher, new EmbedExtractorRegistry(fetcher)); const ctx: Context = { ip: '127.0.0.1' }; describe('MostraGuarda', () => { test('does not handle non imdb movies', async () => { - const streams = await mostraguarda.handle(ctx, 'kitsu:123'); + const streams = await handler.handle(ctx, 'kitsu:123'); expect(streams).toHaveLength(0); }); test('handles non-existent movies gracefully', async () => { - const streams = await mostraguarda.handle(ctx, 'tt12345678'); + const streams = await handler.handle(ctx, 'tt12345678'); expect(streams).toHaveLength(0); }); test('handle imdb the devil\'s bath', async () => { - const streams = await mostraguarda.handle(ctx, 'tt29141112'); + const streams = (await handler.handle(ctx, 'tt29141112')).filter(stream => stream !== undefined); expect(streams).toHaveLength(2); expect(streams[0]).toStrictEqual({ diff --git a/src/handler/MostraGuarda.ts b/src/handler/MostraGuarda.ts index e2f6017..1ce604a 100644 --- a/src/handler/MostraGuarda.ts +++ b/src/handler/MostraGuarda.ts @@ -1,7 +1,7 @@ import * as cheerio from 'cheerio'; import { Handler } from './types'; import { Fetcher, parseImdbId } from '../utils'; -import { EmbedExtractors } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; import { Context } from '../types'; export class MostraGuarda implements Handler { @@ -14,9 +14,9 @@ export class MostraGuarda implements Handler { readonly languages = ['it']; private readonly fetcher: Fetcher; - private readonly embedExtractors: EmbedExtractors; + private readonly embedExtractors: EmbedExtractorRegistry; - constructor(fetcher: Fetcher, embedExtractors: EmbedExtractors) { + constructor(fetcher: Fetcher, embedExtractors: EmbedExtractorRegistry) { this.fetcher = fetcher; this.embedExtractors = embedExtractors; } @@ -34,7 +34,7 @@ export class MostraGuarda implements Handler { $('[data-link!=""]') .map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://'))) .toArray() - .filter(embedUrl => embedUrl.host.match(/(dropload|supervideo)/)) + .filter(embedUrl => !embedUrl.host.match(/mostraguarda/)) .map(embedUrl => this.embedExtractors.handle(ctx, embedUrl, 'it')), ); }; diff --git a/src/handler/VerHdLink.test.ts b/src/handler/VerHdLink.test.ts index de63e08..8c3ae19 100644 --- a/src/handler/VerHdLink.test.ts +++ b/src/handler/VerHdLink.test.ts @@ -1,29 +1,29 @@ import { VerHdLink } from './VerHdLink'; import { Fetcher } from '../utils'; -import { Dropload, EmbedExtractors, SuperVideo } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; import { Context } from '../types'; jest.mock('../utils/Fetcher'); // @ts-expect-error No constructor args needed const fetcher = new Fetcher(); -const mostraguarda = new VerHdLink(fetcher, new EmbedExtractors([new Dropload(fetcher), new SuperVideo(fetcher)])); +const handler = new VerHdLink(fetcher, new EmbedExtractorRegistry(fetcher)); const ctx: Context = { ip: '127.0.0.1' }; describe('VerHdLink', () => { test('does not handle non imdb movies', async () => { - const streams = await mostraguarda.handle(ctx, 'kitsu:123'); + const streams = await handler.handle(ctx, 'kitsu:123'); expect(streams).toHaveLength(0); }); test('handles non-existent movies gracefully', async () => { - const streams = await mostraguarda.handle(ctx, 'tt12345678'); + const streams = await handler.handle(ctx, 'tt12345678'); expect(streams).toHaveLength(0); }); test('handle titanic', async () => { - const streams = await mostraguarda.handle(ctx, 'tt0120338'); + const streams = (await handler.handle(ctx, 'tt0120338')).filter(stream => stream !== undefined); expect(streams).toHaveLength(4); expect(streams[0]).toStrictEqual({ diff --git a/src/handler/VerHdLink.ts b/src/handler/VerHdLink.ts index 93408d6..f40e0a5 100644 --- a/src/handler/VerHdLink.ts +++ b/src/handler/VerHdLink.ts @@ -1,7 +1,7 @@ import * as cheerio from 'cheerio'; import { Handler } from './types'; import { Fetcher, parseImdbId } from '../utils'; -import { EmbedExtractors } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; import { Context } from '../types'; export class VerHdLink implements Handler { @@ -14,9 +14,9 @@ export class VerHdLink implements Handler { readonly languages = ['es', 'mx']; private readonly fetcher: Fetcher; - private readonly embedExtractors: EmbedExtractors; + private readonly embedExtractors: EmbedExtractorRegistry; - constructor(fetcher: Fetcher, embedExtractors: EmbedExtractors) { + constructor(fetcher: Fetcher, embedExtractors: EmbedExtractorRegistry) { this.fetcher = fetcher; this.embedExtractors = embedExtractors; } @@ -45,7 +45,7 @@ export class VerHdLink implements Handler { return $('[data-link!=""]', el) .map((_i, el) => new URL(($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://'))) .toArray() - .filter(embedUrl => embedUrl.host.match(/(dropload|supervideo)/)) + .filter(embedUrl => !embedUrl.host.match(/verhdlink/)) .map(embedUrl => this.embedExtractors.handle(ctx, embedUrl, countryCode)); }), ); diff --git a/src/handler/types.ts b/src/handler/types.ts index 9887ea2..cf9bb42 100644 --- a/src/handler/types.ts +++ b/src/handler/types.ts @@ -9,5 +9,5 @@ export interface Handler { readonly languages: string[]; - readonly handle: (ctx: Context, id: string) => Promise; + readonly handle: (ctx: Context, id: string) => Promise<(UrlResult | undefined)[]>; } diff --git a/src/index.ts b/src/index.ts index 0711ef1..47b5653 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import express, { NextFunction, Request, Response } from 'express'; import makeFetchHappen from 'make-fetch-happen'; import winston from 'winston'; import { FrenchCloud, Handler, KinoKiste, MeineCloud, MostraGuarda, VerHdLink } from './handler'; -import { Dropload, EmbedExtractors, SuperVideo } from './embed-extractor'; +import { EmbedExtractorRegistry } from './embed-extractor'; import { ConfigureController, ManifestController, StreamController } from './controller'; import { Fetcher } from './utils'; import fs from 'node:fs'; @@ -28,10 +28,7 @@ const fetcher = new Fetcher( logger, ); -const embedExtractors = new EmbedExtractors([ - new Dropload(fetcher), - new SuperVideo(fetcher), -]); +const embedExtractors = new EmbedExtractorRegistry(fetcher); const handlers: Handler[] = [ new FrenchCloud(fetcher, embedExtractors), diff --git a/src/utils/manifest.test.ts b/src/utils/manifest.test.ts index 6fa37b2..3f5ed7a 100644 --- a/src/utils/manifest.test.ts +++ b/src/utils/manifest.test.ts @@ -1,7 +1,7 @@ import { buildManifest } from './manifest'; import { KinoKiste } from '../handler'; import { Fetcher } from './Fetcher'; -import { EmbedExtractors } from '../embed-extractor'; +import { EmbedExtractorRegistry } from '../embed-extractor'; // @ts-expect-error No constructor args needed const fetcher = new Fetcher(); @@ -14,14 +14,14 @@ describe('buildManifest', () => { }); test('has unchecked handler without a config', () => { - const manifest = buildManifest([new KinoKiste(fetcher, new EmbedExtractors([]))], {}); + const manifest = buildManifest([new KinoKiste(fetcher, new EmbedExtractorRegistry(fetcher))], {}); expect(manifest.config).toHaveLength(1); expect(manifest.config[0]?.default).toBeUndefined(); }); test('has checked handler with appropriate config', () => { - const kinokiste = new KinoKiste(fetcher, new EmbedExtractors([])); + const kinokiste = new KinoKiste(fetcher, new EmbedExtractorRegistry(fetcher)); const manifest = buildManifest([kinokiste], { [kinokiste.id]: 'on' }); expect(manifest.config).toHaveLength(1);