diff --git a/src/controller/StreamController.ts b/src/controller/StreamController.ts index 669ae4d..84bc41b 100644 --- a/src/controller/StreamController.ts +++ b/src/controller/StreamController.ts @@ -36,7 +36,11 @@ export class StreamController { const handlers = this.handlers.filter(handler => handler.countryCodes.filter(countryCode => countryCode in ctx.config).length); - const streams = await this.streamResolver.resolve(ctx, handlers, type, id); + const { streams, ttl } = await this.streamResolver.resolve(ctx, handlers, type, id); + + if (ttl && process.env['NODE_ENV'] === 'production') { + res.setHeader('Cache-Control', `max-age=${ttl / 1000}, public`); + } res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify({ streams })); diff --git a/src/extractor/DoodStream.ts b/src/extractor/DoodStream.ts index 16b9f85..dc88281 100644 --- a/src/extractor/DoodStream.ts +++ b/src/extractor/DoodStream.ts @@ -42,6 +42,7 @@ export class DoodStream implements Extractor { url: new URL(`${baseUrl}${randomstring.generate(10)}?token=${token}&expiry=${Date.now()}`), label: this.label, sourceId: `${this.id}_${meta.countryCode.toLowerCase()}`, + ttl: this.ttl, meta: { title, ...meta, diff --git a/src/extractor/Dropload.ts b/src/extractor/Dropload.ts index 7e8c741..2ebff31 100644 --- a/src/extractor/Dropload.ts +++ b/src/extractor/Dropload.ts @@ -39,6 +39,7 @@ export class Dropload implements Extractor { url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]), label: this.label, sourceId: `${this.id}_${meta.countryCode.toLowerCase()}`, + ttl: this.ttl, meta: { bytes: bytes.parse(sizeMatch[1] as string) as number, height: parseInt(heightMatch[1] as string) as number, diff --git a/src/extractor/ExternalUrl.ts b/src/extractor/ExternalUrl.ts index a4f0b05..5fc7708 100644 --- a/src/extractor/ExternalUrl.ts +++ b/src/extractor/ExternalUrl.ts @@ -26,6 +26,7 @@ export class ExternalUrl implements Extractor { isExternal: true, label: `${url.host}`, sourceId: `${this.id}_${meta.countryCode.toLowerCase()}`, + ttl: this.ttl, meta, }; }; diff --git a/src/extractor/ExtractorRegistry.test.ts b/src/extractor/ExtractorRegistry.test.ts index 0bd187d..066ad42 100644 --- a/src/extractor/ExtractorRegistry.test.ts +++ b/src/extractor/ExtractorRegistry.test.ts @@ -1,6 +1,6 @@ import winston from 'winston'; import { ExtractorRegistry } from './ExtractorRegistry'; -import { Context } from '../types'; +import { Context, UrlResult } from '../types'; import { Fetcher } from '../utils'; jest.mock('../utils/Fetcher'); @@ -31,22 +31,21 @@ describe('ExtractorRegistry', () => { }); test('returns from memory cache if possible', async () => { - const urlResult1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), { countryCode: 'de' }); - const urlResult2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), { countryCode: 'de' }); + const { ttl: ttl1, ...urlResultRest1 } = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), { countryCode: 'de' }) as UrlResult; + const { ttl: ttl2, ...urlResultRest2 } = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), { countryCode: 'de' }) as UrlResult; - expect(urlResult2).toBe(urlResult1); + expect(urlResultRest1).not.toBeUndefined(); + expect(urlResultRest2).toStrictEqual(urlResultRest1); + + expect(ttl1).not.toBe(undefined); + expect(ttl2).not.toBe(undefined); }); - test('returns from memory cache if possible', async () => { - const urlResult1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), { countryCode: 'de' }); - const urlResult2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), { countryCode: 'de' }); + test('ignores not found errors but caches them', async () => { + const urlResult1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), { countryCode: 'de' }); + const urlResult2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), { countryCode: 'de' }); - expect(urlResult2).toBe(urlResult1); - }); - - test('ignores not found errors', async () => { - const urlResult = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), { countryCode: 'de' }); - - expect(urlResult).toBeUndefined(); + expect(urlResult1).toBeUndefined(); + expect(urlResult2).toBeUndefined(); }); }); diff --git a/src/extractor/ExtractorRegistry.ts b/src/extractor/ExtractorRegistry.ts index b9291a7..5302060 100644 --- a/src/extractor/ExtractorRegistry.ts +++ b/src/extractor/ExtractorRegistry.ts @@ -28,7 +28,7 @@ export class ExtractorRegistry { readonly handle = async (ctx: Context, url: URL, meta: Meta): Promise => { let urlResult = this.urlResultCache.get(url.href); if (this.urlResultCache.has(url.href)) { - return urlResult; + return urlResult ? { ...urlResult, ttl: this.urlResultCache.getRemainingTTL(url.href) } : undefined; } const extractor = this.extractors.find(extractor => extractor.supports(ctx, url)); @@ -53,6 +53,7 @@ export class ExtractorRegistry { error, label: url.host, sourceId: `${extractor.id}`, + ttl: extractor.ttl, meta, }; } diff --git a/src/extractor/SuperVideo.ts b/src/extractor/SuperVideo.ts index d398ce0..a672f0d 100644 --- a/src/extractor/SuperVideo.ts +++ b/src/extractor/SuperVideo.ts @@ -32,6 +32,7 @@ export class SuperVideo implements Extractor { url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]), label: this.label, sourceId: `${this.id}_${meta.countryCode.toLowerCase()}`, + ttl: this.ttl, meta: { bytes: bytes.parse(heightAndSizeMatch[2] as string) as number, height: parseInt(heightAndSizeMatch[1] as string) as number, diff --git a/src/extractor/__snapshots__/ExtractorRegistry.test.ts.snap b/src/extractor/__snapshots__/ExtractorRegistry.test.ts.snap index e4cda16..172e9df 100644 --- a/src/extractor/__snapshots__/ExtractorRegistry.test.ts.snap +++ b/src/extractor/__snapshots__/ExtractorRegistry.test.ts.snap @@ -8,6 +8,7 @@ exports[`ExtractorRegistry return external URLs by default 1`] = ` "countryCode": "de", }, "sourceId": "external_de", + "ttl": 3600000, "url": "https://mixdrop.ag/e/3nzwveprim63or6", } `; @@ -21,6 +22,7 @@ exports[`ExtractorRegistry returns error result from extractor 1`] = ` "countryCode": "en", }, "sourceId": "external", + "ttl": 3600000, "url": "https://some-url.test/", } `; diff --git a/src/handler/__snapshots__/CineHDPlus.test.ts.snap b/src/handler/__snapshots__/CineHDPlus.test.ts.snap index 48572ea..b4845f9 100644 --- a/src/handler/__snapshots__/CineHDPlus.test.ts.snap +++ b/src/handler/__snapshots__/CineHDPlus.test.ts.snap @@ -11,6 +11,7 @@ exports[`CineHDPlus handle imdb babylon 5 s2e3 (es) 1`] = ` "title": "Babylon 5 2x3", }, "sourceId": "supervideo_es", + "ttl": 900000, "url": "https://hfs294.serversicuro.cc/hls/,dnzpealv5xg4a3gyvavh52jryms77x3wwakzvgy2ly3yfusnblczjnvvweua,.urlset/master.m3u8", }, { @@ -22,6 +23,7 @@ exports[`CineHDPlus handle imdb babylon 5 s2e3 (es) 1`] = ` "title": "Babylon 5 2x3", }, "sourceId": "dropload_es", + "ttl": 900000, "url": "https://srv29.dropload.io/hls2/01/00295/957hny8mzrvh_h/master.m3u8?t=VpcZJYlXpp0FJb0Nh0yJR0nyI8-SKB2tGFwpcTx9v2w&s=1747213958&e=14400&f=1476559&i=0.0&sp=0&ii=130.61.236.204", }, ] @@ -38,6 +40,7 @@ exports[`CineHDPlus handle imdb black mirror s2e3 (mx) 1`] = ` "title": "Black Mirror 2x3", }, "sourceId": "supervideo_mx", + "ttl": 900000, "url": "https://hfs299.serversicuro.cc/hls/,dnzpff262hg4a3gyvcex5ojxt23vrieljzut7xhjydwidlbp4g7nqswkpfwq,.urlset/master.m3u8", }, { @@ -49,6 +52,7 @@ exports[`CineHDPlus handle imdb black mirror s2e3 (mx) 1`] = ` "title": "Black Mirror 2x3", }, "sourceId": "dropload_mx", + "ttl": 900000, "url": "https://srv27.dropload.io/hls2/01/00075/t058ulwwkwn6_h/master.m3u8?t=GkktV2WgIMrvrgNLyjn6Bxeo2fArEAz6y9nSi_6dVAs&s=1747213564&e=14400&f=376684&i=0.0&sp=0&ii=130.61.236.204", }, ] diff --git a/src/handler/__snapshots__/Eurostreaming.test.ts.snap b/src/handler/__snapshots__/Eurostreaming.test.ts.snap index 7206842..e36b2c4 100644 --- a/src/handler/__snapshots__/Eurostreaming.test.ts.snap +++ b/src/handler/__snapshots__/Eurostreaming.test.ts.snap @@ -11,6 +11,7 @@ exports[`Eurostreaming handle imdb black mirror s2e4 1`] = ` "title": "Black Mirror 2x4", }, "sourceId": "supervideo_it", + "ttl": 900000, "url": "https://hfs309.serversicuro.cc/hls/dnzpdeoe5xg4a3gyvaqx5ojpswtxjd5a22rklgah7,khdhascykjrixs7huqq,obtfascykj3vna7rk5a,.urlset/master.m3u8", }, { @@ -22,6 +23,7 @@ exports[`Eurostreaming handle imdb black mirror s2e4 1`] = ` "title": "Black Mirror 2x4", }, "sourceId": "supervideo_it", + "ttl": 900000, "url": "https://hfs309.serversicuro.cc/hls/dnzpdeoe5xg4a3gyvaqx5ojpswtxjd5a22rklgah7,khdhascykjrixs7huqq,obtfascykj3vna7rk5a,.urlset/master.m3u8", }, { @@ -33,6 +35,7 @@ exports[`Eurostreaming handle imdb black mirror s2e4 1`] = ` "title": "Black Mirror 2x4", }, "sourceId": "dropload_it", + "ttl": 900000, "url": "https://srv24.dropload.io/hls2/01/00289/978xyyxi3ldq_h/master.m3u8?t=unq9qxjYC-zgcmoXiSQ6rkMu7qHrSpN_pWOc95IPYwk&s=1747217151&e=14400&f=1448116&i=0.0&sp=0&ii=130.61.236.204", }, ] diff --git a/src/handler/__snapshots__/Frembed.test.ts.snap b/src/handler/__snapshots__/Frembed.test.ts.snap index 5d40009..2b66510 100644 --- a/src/handler/__snapshots__/Frembed.test.ts.snap +++ b/src/handler/__snapshots__/Frembed.test.ts.snap @@ -11,6 +11,7 @@ exports[`Frembed handle imdb black mirror s4e2 1`] = ` "title": "Black Mirror 4x2", }, "sourceId": "external", + "ttl": 3600000, "url": "https://ahvsh.com/e/83izf7qzwpae,https://netu.frembed.fun/e/0DFgfkcXOsDP,https://likessb.com/e/7yjgl1x56n08.html,https://ds2play.com/e/fzfvfq3ngig0", }, { @@ -23,6 +24,7 @@ exports[`Frembed handle imdb black mirror s4e2 1`] = ` "Referer": "http://dood.to/", }, "sourceId": "doodstream_fr", + "ttl": 900000, "url": "https://ee317r.cloudatacdn.com/u5kj63yvxddlsdgge7qremqqka27irwiupc4k7o5cikbh7fdq4j4mwzraf7q/1y4sn4ndoi~mocked-random-string?token=fh3rdlhjvao7chgxndsi3ra7&expiry=639837296000", }, { @@ -33,6 +35,7 @@ exports[`Frembed handle imdb black mirror s4e2 1`] = ` "title": "Black Mirror 4x2", }, "sourceId": "external_fr", + "ttl": 3600000, "url": "https://netu.fanstream.us/e/0DFgfkcXOsDP", }, { @@ -43,6 +46,7 @@ exports[`Frembed handle imdb black mirror s4e2 1`] = ` "title": "Black Mirror 4x2", }, "sourceId": "external_fr", + "ttl": 3600000, "url": "https://johnalwayssame.com/e/cqy9oue7sv0g", }, { @@ -54,6 +58,7 @@ exports[`Frembed handle imdb black mirror s4e2 1`] = ` "title": "Black Mirror 4x2", }, "sourceId": "external", + "ttl": 3600000, "url": "https://ds2play.com/e/fzfvfq3ngig0", }, ] diff --git a/src/handler/__snapshots__/FrenchCloud.test.ts.snap b/src/handler/__snapshots__/FrenchCloud.test.ts.snap index d72bc76..e43aa17 100644 --- a/src/handler/__snapshots__/FrenchCloud.test.ts.snap +++ b/src/handler/__snapshots__/FrenchCloud.test.ts.snap @@ -11,6 +11,7 @@ exports[`FrenchCloud handle imdb the devil's bath 1`] = ` "title": "the devils bath enfant pour diable 2024 cam", }, "sourceId": "supervideo_fr", + "ttl": 900000, "url": "https://hfs292.serversicuro.cc/hls/,dnzpfwdj5dg4a3gyvbwx5lbvtu65tuwysrizwl7puv6u7dkpym3afmw5hnga,.urlset/master.m3u8", }, { @@ -22,6 +23,7 @@ exports[`FrenchCloud handle imdb the devil's bath 1`] = ` "title": "the-devils-bath-un-enfant-pour-le-diable-2024-cam", }, "sourceId": "dropload_fr", + "ttl": 900000, "url": "https://srv28.dropload.io/hls2/01/00215/w4yit5wiia9y_h/master.m3u8?t=ujTsd4AYOdgaA6EhuMJDrC-xYzMVEbqrrd3qsoZ8iAY&s=1747054860&e=14400&f=1076096&i=0.0&sp=0&ii=130.61.236.204", }, { @@ -31,6 +33,7 @@ exports[`FrenchCloud handle imdb the devil's bath 1`] = ` "countryCode": "fr", }, "sourceId": "external_fr", + "ttl": 3600000, "url": "https://mixdrop.ag/e/l7v73zqrfdj19z", }, { @@ -41,6 +44,7 @@ exports[`FrenchCloud handle imdb the devil's bath 1`] = ` "countryCode": "fr", }, "sourceId": "external", + "ttl": 3600000, "url": "https://streamtape.com/e/gjA1OQ4klyHxgJ", }, { @@ -53,6 +57,7 @@ exports[`FrenchCloud handle imdb the devil's bath 1`] = ` "Referer": "http://dood.to/", }, "sourceId": "doodstream_fr", + "ttl": 900000, "url": "https://ty1053vs.cloudatacdn.com/u5kjz2cvh7blsdgge4mmgoifjigorrf6o6mlfwqbndqkhkcvu43dcrk7wfga/6zwayqucuf~mocked-random-string?token=y0n5z6g48kft9apq8imziegv&expiry=639837296000", }, ] diff --git a/src/handler/__snapshots__/KinoKiste.test.ts.snap b/src/handler/__snapshots__/KinoKiste.test.ts.snap index 8007b34..0073a1c 100644 --- a/src/handler/__snapshots__/KinoKiste.test.ts.snap +++ b/src/handler/__snapshots__/KinoKiste.test.ts.snap @@ -11,6 +11,7 @@ exports[`KinoKiste handle imdb black mirror s2e4 1`] = ` "title": "Black Mirror 2x4", }, "sourceId": "supervideo_de", + "ttl": 900000, "url": "https://hfs279.serversicuro.cc/hls/,dnzpervt3xg4a3gyvdix52ttsqpj5jgxs7keazdmhvru2uyin5g7f67a5u4a,.urlset/master.m3u8", }, { @@ -22,6 +23,7 @@ exports[`KinoKiste handle imdb black mirror s2e4 1`] = ` "title": "Black Mirror 2x4", }, "sourceId": "dropload_de", + "ttl": 900000, "url": "https://srv34.dropload.io/hls2/01/00007/jvjwrkpijr0f_h/master.m3u8?t=9B5k5sYgDD2kS_549tQ4WYh1o74hO1QR9zzyXrVRqv4&s=1746903685&e=14400&f=35986&srv=srv27&i=0.0&sp=0&ii=130.61.236.204&p1=srv27&p2=srv27", }, ] diff --git a/src/handler/__snapshots__/MeineCloud.test.ts.snap b/src/handler/__snapshots__/MeineCloud.test.ts.snap index ccd05d2..34becc8 100644 --- a/src/handler/__snapshots__/MeineCloud.test.ts.snap +++ b/src/handler/__snapshots__/MeineCloud.test.ts.snap @@ -11,6 +11,7 @@ exports[`MeineCloud handle imdb the devil's bath 1`] = ` "title": "des teufels bad 2024", }, "sourceId": "supervideo_de", + "ttl": 900000, "url": "https://hfs290.serversicuro.cc/hls/,dnzpfi3d27g4a3gyvbmh5klwtl65qh654hyjtgupd6p56thhyqu6ra3olbfa,.urlset/master.m3u8", }, { @@ -22,6 +23,7 @@ exports[`MeineCloud handle imdb the devil's bath 1`] = ` "title": "des-teufels-bad-2024", }, "sourceId": "dropload_de", + "ttl": 900000, "url": "https://srv27.dropload.io/hls2/01/00197/lyo2h1snpe5c_h/master.m3u8?t=qTZsRGIMnUjXsQ4o2LRLlsT4tGG1AdJrRBRDGttNLPM&s=1746903744&e=14400&f=987607&i=0.0&sp=0&ii=130.61.236.204", }, { @@ -31,6 +33,7 @@ exports[`MeineCloud handle imdb the devil's bath 1`] = ` "countryCode": "de", }, "sourceId": "external_de", + "ttl": 3600000, "url": "https://mixdrop.ag/e/3nzwveprim63or6", }, { @@ -43,6 +46,7 @@ exports[`MeineCloud handle imdb the devil's bath 1`] = ` "Referer": "http://dood.to/", }, "sourceId": "doodstream_de", + "ttl": 900000, "url": "https://aa360cc.cloudatacdn.com/u5kjv4jxytd3sdgge5uogji5dg4huat2pxrm2qibdbm5rtpmbzgb5tornooa/k9wk0js17e~mocked-random-string?token=7uebebipnnhusa4xnyea1er4&expiry=639837296000", }, ] diff --git a/src/handler/__snapshots__/MostraGuarda.test.ts.snap b/src/handler/__snapshots__/MostraGuarda.test.ts.snap index d66fd85..6910f0d 100644 --- a/src/handler/__snapshots__/MostraGuarda.test.ts.snap +++ b/src/handler/__snapshots__/MostraGuarda.test.ts.snap @@ -11,6 +11,7 @@ exports[`MostraGuarda handle imdb the devil's bath 1`] = ` "title": "the devils bath 2024 sub ita", }, "sourceId": "supervideo_it", + "ttl": 900000, "url": "https://hfs279.serversicuro.cc/hls/,dnzpejj427g4a3gyvbth53rxqewqgqqzp3wr6zwutysvud7zroar3cb75d3q,.urlset/master.m3u8", }, { @@ -22,6 +23,7 @@ exports[`MostraGuarda handle imdb the devil's bath 1`] = ` "title": "the-devils-bath-2024-sd-sub-ita", }, "sourceId": "dropload_it", + "ttl": 900000, "url": "https://srv34.dropload.io/hls2/01/00200/xsr90y2ltdyx_h/master.m3u8?t=avFOZ-10hDxo6__iaoSUrq7o4yY3ldLw6XDlXxpXF8E&s=1747059001&e=14400&f=1004116&srv=srv24&i=0.0&sp=0&ii=130.61.236.204&p1=srv24&p2=srv24", }, { @@ -31,6 +33,7 @@ exports[`MostraGuarda handle imdb the devil's bath 1`] = ` "countryCode": "it", }, "sourceId": "external_it", + "ttl": 3600000, "url": "https://mixdrop.ag/e/vk196d6xfzwwo1", }, { @@ -43,6 +46,7 @@ exports[`MostraGuarda handle imdb the devil's bath 1`] = ` "Referer": "http://dood.to/", }, "sourceId": "doodstream_it", + "ttl": 900000, "url": "https://kk892as.cloudatacdn.com/u5kj7j7s27d3sdgge5woezkbi4pu672wktq3aqujw47rbpl3xog2gtqnw2xa/awn9rgotuu~mocked-random-string?token=aw2v2d8uhbrj2ky54d573ujk&expiry=639837296000", }, ] diff --git a/src/handler/__snapshots__/VerHdLink.test.ts.snap b/src/handler/__snapshots__/VerHdLink.test.ts.snap index 3e1571d..a795eda 100644 --- a/src/handler/__snapshots__/VerHdLink.test.ts.snap +++ b/src/handler/__snapshots__/VerHdLink.test.ts.snap @@ -11,6 +11,7 @@ exports[`VerHdLink handle titanic 1`] = ` "title": "titanic 1997 [latino]", }, "sourceId": "supervideo_mx", + "ttl": 900000, "url": "https://hfs292.serversicuro.cc/hls/,dnzpejlw37g4a3gyvdzh5p3xttjqd3ccheao6qexhjbv4ecdpod7hxlh4a7a,.urlset/master.m3u8", }, { @@ -22,6 +23,7 @@ exports[`VerHdLink handle titanic 1`] = ` "title": "titanic-1997-[latino]", }, "sourceId": "dropload_mx", + "ttl": 900000, "url": "https://srv22.dropload.io/hls2/01/00046/jjjx9j5joiz8_h/master.m3u8?t=_sOFVDjKUqBAK_gJtH7YVQnCm6nj7kr4df3PNsKeKAA&s=1747060128&e=14400&f=230703&i=0.0&sp=0&ii=130.61.236.204", }, { @@ -31,6 +33,7 @@ exports[`VerHdLink handle titanic 1`] = ` "countryCode": "mx", }, "sourceId": "external_mx", + "ttl": 3600000, "url": "https://mixdrop.ag/e/vn0wx308fq984q", }, { @@ -41,6 +44,7 @@ exports[`VerHdLink handle titanic 1`] = ` "countryCode": "mx", }, "sourceId": "external", + "ttl": 3600000, "url": "https://streamtape.com/e/Bjp2vjrdBxsK82", }, { @@ -52,6 +56,7 @@ exports[`VerHdLink handle titanic 1`] = ` "title": "titanic 1997 2[castellano]", }, "sourceId": "supervideo_es", + "ttl": 900000, "url": "https://hfs295.serversicuro.cc/hls/,dnzpfv3w37g4a3gyvdzh5kjeyi4vtoyw4x4it5gxl4zzwiv4t3fxkvagrhea,.urlset/master.m3u8", }, { @@ -63,6 +68,7 @@ exports[`VerHdLink handle titanic 1`] = ` "title": "titanic-1997-2[castellano]", }, "sourceId": "dropload_es", + "ttl": 900000, "url": "https://srv23.dropload.io/hls2/01/00046/ick4ti66vt6s_h/master.m3u8?t=OUbNiE0qRNUy69JP4AIj1bGcV5kaljyNrAUlCYWGVOs&s=1747060128&e=14400&f=230700&i=0.0&sp=0&ii=130.61.236.204", }, { @@ -72,6 +78,7 @@ exports[`VerHdLink handle titanic 1`] = ` "countryCode": "es", }, "sourceId": "external_es", + "ttl": 3600000, "url": "https://mixdrop.ag/e/xokzmv61hrwe8o", }, ] diff --git a/src/types.ts b/src/types.ts index 190b617..43795f2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,6 +30,7 @@ export interface UrlResult { error?: unknown; label: string; sourceId: string; + ttl?: number; meta: Meta; requestHeaders?: Record; } diff --git a/src/utils/StreamResolver.test.ts b/src/utils/StreamResolver.test.ts index 940fcb2..b04a5ef 100644 --- a/src/utils/StreamResolver.test.ts +++ b/src/utils/StreamResolver.test.ts @@ -21,11 +21,7 @@ describe('resolve', () => { test('returns info as stream if no handlers were configured', async () => { const streams = await streamResolver.resolve(ctx, [], 'movie', 'tt123456789'); - expect(streams).toStrictEqual([{ - name: 'WebStreamr', - title: 'āš ļø No handlers found. Please re-configure the plugin.', - ytId: 'E4WlUXrJgy4', - }]); + expect(streams).toMatchSnapshot(); }); test('returns handler errors as stream', async () => { @@ -33,23 +29,21 @@ describe('resolve', () => { const streams = await streamResolver.resolve(ctx, [meineCloud], 'movie', 'tt123456789'); - expect(streams).toStrictEqual([{ - name: 'WebStreamr', - title: 'šŸ”— MeineCloud\nāŒ Request failed. Request-id: id.', - ytId: 'E4WlUXrJgy4', - }]); + expect(streams).toMatchSnapshot(); fetcherSpy.mockRestore(); }); test('returns empty array if no handler found anything', async () => { const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'movie', 'tt12345678'); - expect(streams).toStrictEqual([]); + + expect(streams).toMatchSnapshot(); }); test('returns empty array if no handler supported the type', async () => { const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'series', 'tt12345678:1:1'); - expect(streams).toStrictEqual([]); + + expect(streams).toMatchSnapshot(); }); test('returns sorted results', async () => { @@ -130,6 +124,7 @@ describe('resolve', () => { }; const streams = await streamResolver.resolve(ctx, [mockHandler], 'movie', 'tt12345678'); - expect(streams).toStrictEqual([]); + + expect(streams).toMatchSnapshot(); }); }); diff --git a/src/utils/StreamResolver.ts b/src/utils/StreamResolver.ts index 1896226..e0d563d 100644 --- a/src/utils/StreamResolver.ts +++ b/src/utils/StreamResolver.ts @@ -7,6 +7,11 @@ import { Handler } from '../handler'; import { BlockedError, NotFoundError, QueueIsFullError } from '../error'; import { languageFromCountryCode } from './languageFromCountryCode'; +interface ResolveResponse { + streams: Stream[]; + ttl?: number; +} + export class StreamResolver { private readonly logger: winston.Logger; @@ -14,17 +19,22 @@ export class StreamResolver { this.logger = logger; } - readonly resolve = async (ctx: Context, handlers: Handler[], type: string, id: string): Promise => { + readonly resolve = async (ctx: Context, handlers: Handler[], type: string, id: string): Promise => { if (handlers.length === 0) { - return [{ - name: 'WebStreamr', - title: 'āš ļø No handlers found. Please re-configure the plugin.', - ytId: 'E4WlUXrJgy4', - }]; + return { + streams: [ + { + name: 'WebStreamr', + title: 'āš ļø No handlers found. Please re-configure the plugin.', + ytId: 'E4WlUXrJgy4', + }, + ], + }; } const streams: Stream[] = []; + let handlerErrorOccurred = false; const urlResults: UrlResult[] = []; const handlerPromises = handlers.map(async (handler) => { if (!handler.contentTypes.includes(type)) { @@ -41,6 +51,8 @@ export class StreamResolver { return; } + handlerErrorOccurred = true; + streams.push({ name: process.env['MANIFEST_NAME'] || 'WebStreamr', title: [`šŸ”— ${handler.label}`, this.logErrorAndReturnNiceString(ctx, handler.id, error)].join('\n'), @@ -83,7 +95,24 @@ export class StreamResolver { })), ); - return streams; + const ttl = !handlerErrorOccurred ? this.determineTtl(urlResults) : undefined; + + return { + streams, + ...(ttl && { ttl }), + }; + }; + + private readonly determineTtl = (urlResults: UrlResult[]): number | undefined => { + if (!urlResults.length) { + return 900000; // 15m + } + + if (urlResults.some(urlResult => urlResult.ttl === undefined)) { + return undefined; + } + + return Math.min(...urlResults.map(urlResult => urlResult.ttl as number)); }; private readonly showExternalUrls = (ctx: Context): boolean => !('excludeExternalUrls' in ctx.config); diff --git a/src/utils/__snapshots__/StreamResolver.test.ts.snap b/src/utils/__snapshots__/StreamResolver.test.ts.snap index 30811df..fd96650 100644 --- a/src/utils/__snapshots__/StreamResolver.test.ts.snap +++ b/src/utils/__snapshots__/StreamResolver.test.ts.snap @@ -1,178 +1,231 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`resolve adds error info 1`] = ` -[ - { - "behaviorHints": {}, - "externalUrl": "https://example.com/", - "name": "WebStreamr N/A āš ļø external", - "title": "🌐 German šŸ‡©šŸ‡Ŗ +{ + "streams": [ + { + "behaviorHints": {}, + "externalUrl": "https://example.com/", + "name": "WebStreamr N/A āš ļø external", + "title": "🌐 German šŸ‡©šŸ‡Ŗ šŸ”— hoster.com āš ļø Request was blocked.", - }, - { - "behaviorHints": {}, - "externalUrl": "https://example2.com/", - "name": "WebStreamr N/A āš ļø external", - "title": "🌐 German šŸ‡©šŸ‡Ŗ + }, + { + "behaviorHints": {}, + "externalUrl": "https://example2.com/", + "name": "WebStreamr N/A āš ļø external", + "title": "🌐 German šŸ‡©šŸ‡Ŗ šŸ”— hoster.com āŒ Request failed. Request-id: id.", - }, - { - "behaviorHints": {}, - "externalUrl": "https://example2.com/", - "name": "WebStreamr N/A āš ļø external", - "title": "🌐 German šŸ‡©šŸ‡Ŗ + }, + { + "behaviorHints": {}, + "externalUrl": "https://example2.com/", + "name": "WebStreamr N/A āš ļø external", + "title": "🌐 German šŸ‡©šŸ‡Ŗ šŸ”— hoster.com 🐢 Request timed out.", - }, - { - "behaviorHints": {}, - "externalUrl": "https://example3.com/", - "name": "WebStreamr N/A āš ļø external", - "title": "🌐 German šŸ‡©šŸ‡Ŗ + }, + { + "behaviorHints": {}, + "externalUrl": "https://example3.com/", + "name": "WebStreamr N/A āš ļø external", + "title": "🌐 German šŸ‡©šŸ‡Ŗ šŸ”— hoster.com ā³ Request queue is full. Please try again later or consider self-hosting.", - }, -] + }, + ], +} `; exports[`resolve adds error info 2`] = ` -[ - { - "behaviorHints": {}, - "name": "WebStreamr N/A", - "title": "🌐 German šŸ‡©šŸ‡Ŗ +{ + "streams": [ + { + "behaviorHints": {}, + "name": "WebStreamr N/A", + "title": "🌐 German šŸ‡©šŸ‡Ŗ šŸ”— hoster.com āš ļø Request was blocked.", - "ytId": "E4WlUXrJgy4", - }, - { - "behaviorHints": {}, - "name": "WebStreamr N/A", - "title": "🌐 German šŸ‡©šŸ‡Ŗ + "ytId": "E4WlUXrJgy4", + }, + { + "behaviorHints": {}, + "name": "WebStreamr N/A", + "title": "🌐 German šŸ‡©šŸ‡Ŗ šŸ”— hoster.com āŒ Request failed. Request-id: id.", - "ytId": "E4WlUXrJgy4", - }, - { - "behaviorHints": {}, - "name": "WebStreamr N/A", - "title": "🌐 German šŸ‡©šŸ‡Ŗ + "ytId": "E4WlUXrJgy4", + }, + { + "behaviorHints": {}, + "name": "WebStreamr N/A", + "title": "🌐 German šŸ‡©šŸ‡Ŗ šŸ”— hoster.com 🐢 Request timed out.", - "ytId": "E4WlUXrJgy4", - }, - { - "behaviorHints": {}, - "name": "WebStreamr N/A", - "title": "🌐 German šŸ‡©šŸ‡Ŗ + "ytId": "E4WlUXrJgy4", + }, + { + "behaviorHints": {}, + "name": "WebStreamr N/A", + "title": "🌐 German šŸ‡©šŸ‡Ŗ šŸ”— hoster.com ā³ Request queue is full. Please try again later or consider self-hosting.", - "ytId": "E4WlUXrJgy4", - }, -] + "ytId": "E4WlUXrJgy4", + }, + ], +} +`; + +exports[`resolve ignores not found errors 1`] = ` +{ + "streams": [], + "ttl": 900000, +} +`; + +exports[`resolve returns empty array if no handler found anything 1`] = ` +{ + "streams": [], + "ttl": 900000, +} +`; + +exports[`resolve returns empty array if no handler supported the type 1`] = ` +{ + "streams": [], + "ttl": 900000, +} +`; + +exports[`resolve returns handler errors as stream 1`] = ` +{ + "streams": [ + { + "name": "WebStreamr", + "title": "šŸ”— MeineCloud +āŒ Request failed. Request-id: id.", + "ytId": "E4WlUXrJgy4", + }, + ], +} +`; + +exports[`resolve returns info as stream if no handlers were configured 1`] = ` +{ + "streams": [ + { + "name": "WebStreamr", + "title": "āš ļø No handlers found. Please re-configure the plugin.", + "ytId": "E4WlUXrJgy4", + }, + ], +} `; exports[`resolve returns sorted results 1`] = ` -[ - { - "behaviorHints": { - "bingeGroup": "webstreamr-dropload_de", - "videoSize": 1395864371, - }, - "name": "WebStreamr 1080P", - "title": "šŸ“‚ des-teufels-bad-2024 +{ + "streams": [ + { + "behaviorHints": { + "bingeGroup": "webstreamr-dropload_de", + "videoSize": 1395864371, + }, + "name": "WebStreamr 1080P", + "title": "šŸ“‚ des-teufels-bad-2024 šŸ’¾ 1.3 GB 🌐 German šŸ‡©šŸ‡Ŗ šŸ”— Dropload", - "url": "https://srv27.dropload.io/hls2/01/00197/lyo2h1snpe5c_h/master.m3u8?t=qTZsRGIMnUjXsQ4o2LRLlsT4tGG1AdJrRBRDGttNLPM&s=1746903744&e=14400&f=987607&i=0.0&sp=0&ii=130.61.236.204", - }, - { - "behaviorHints": { - "bingeGroup": "webstreamr-dropload_it", - "videoSize": 1181116006, + "url": "https://srv27.dropload.io/hls2/01/00197/lyo2h1snpe5c_h/master.m3u8?t=qTZsRGIMnUjXsQ4o2LRLlsT4tGG1AdJrRBRDGttNLPM&s=1746903744&e=14400&f=987607&i=0.0&sp=0&ii=130.61.236.204", }, - "name": "WebStreamr 720P", - "title": "šŸ“‚ the-devils-bath-2024-sd-sub-ita + { + "behaviorHints": { + "bingeGroup": "webstreamr-dropload_it", + "videoSize": 1181116006, + }, + "name": "WebStreamr 720P", + "title": "šŸ“‚ the-devils-bath-2024-sd-sub-ita šŸ’¾ 1.1 GB 🌐 Italian šŸ‡®šŸ‡¹ šŸ”— Dropload", - "url": "https://srv34.dropload.io/hls2/01/00200/xsr90y2ltdyx_h/master.m3u8?t=avFOZ-10hDxo6__iaoSUrq7o4yY3ldLw6XDlXxpXF8E&s=1747059001&e=14400&f=1004116&srv=srv24&i=0.0&sp=0&ii=130.61.236.204&p1=srv24&p2=srv24", - }, - { - "behaviorHints": { - "bingeGroup": "webstreamr-supervideo_it", - "videoSize": 1181116006, + "url": "https://srv34.dropload.io/hls2/01/00200/xsr90y2ltdyx_h/master.m3u8?t=avFOZ-10hDxo6__iaoSUrq7o4yY3ldLw6XDlXxpXF8E&s=1747059001&e=14400&f=1004116&srv=srv24&i=0.0&sp=0&ii=130.61.236.204&p1=srv24&p2=srv24", }, - "name": "WebStreamr 720P", - "title": "šŸ“‚ the devils bath 2024 sub ita + { + "behaviorHints": { + "bingeGroup": "webstreamr-supervideo_it", + "videoSize": 1181116006, + }, + "name": "WebStreamr 720P", + "title": "šŸ“‚ the devils bath 2024 sub ita šŸ’¾ 1.1 GB 🌐 Italian šŸ‡®šŸ‡¹ šŸ”— SuperVideo", - "url": "https://hfs279.serversicuro.cc/hls/,dnzpejj427g4a3gyvbth53rxqewqgqqzp3wr6zwutysvud7zroar3cb75d3q,.urlset/master.m3u8", - }, - { - "behaviorHints": { - "bingeGroup": "webstreamr-supervideo_de", - "videoSize": 1073741824, + "url": "https://hfs279.serversicuro.cc/hls/,dnzpejj427g4a3gyvbth53rxqewqgqqzp3wr6zwutysvud7zroar3cb75d3q,.urlset/master.m3u8", }, - "name": "WebStreamr 720P", - "title": "šŸ“‚ des teufels bad 2024 + { + "behaviorHints": { + "bingeGroup": "webstreamr-supervideo_de", + "videoSize": 1073741824, + }, + "name": "WebStreamr 720P", + "title": "šŸ“‚ des teufels bad 2024 šŸ’¾ 1 GB 🌐 German šŸ‡©šŸ‡Ŗ šŸ”— SuperVideo", - "url": "https://hfs290.serversicuro.cc/hls/,dnzpfi3d27g4a3gyvbmh5klwtl65qh654hyjtgupd6p56thhyqu6ra3olbfa,.urlset/master.m3u8", - }, - { - "behaviorHints": { - "bingeGroup": "webstreamr-doodstream_de", - "notWebReady": true, - "proxyHeaders": { - "request": { - "Referer": "http://dood.to/", + "url": "https://hfs290.serversicuro.cc/hls/,dnzpfi3d27g4a3gyvbmh5klwtl65qh654hyjtgupd6p56thhyqu6ra3olbfa,.urlset/master.m3u8", + }, + { + "behaviorHints": { + "bingeGroup": "webstreamr-doodstream_de", + "notWebReady": true, + "proxyHeaders": { + "request": { + "Referer": "http://dood.to/", + }, }, }, - }, - "name": "WebStreamr N/A", - "title": "šŸ“‚ des-teufels-bad-2024 + "name": "WebStreamr N/A", + "title": "šŸ“‚ des-teufels-bad-2024 🌐 German šŸ‡©šŸ‡Ŗ šŸ”— DoodStream", - "url": "https://aa360cc.cloudatacdn.com/u5kjv4jxytd3sdgge5uogji5dg4huat2pxrm2qibdbm5rtpmbzgb5tornooa/k9wk0js17e~mocked-random-string?token=7uebebipnnhusa4xnyea1er4&expiry=639837296000", - }, - { - "behaviorHints": { - "bingeGroup": "webstreamr-doodstream_it", - "notWebReady": true, - "proxyHeaders": { - "request": { - "Referer": "http://dood.to/", + "url": "https://aa360cc.cloudatacdn.com/u5kjv4jxytd3sdgge5uogji5dg4huat2pxrm2qibdbm5rtpmbzgb5tornooa/k9wk0js17e~mocked-random-string?token=7uebebipnnhusa4xnyea1er4&expiry=639837296000", + }, + { + "behaviorHints": { + "bingeGroup": "webstreamr-doodstream_it", + "notWebReady": true, + "proxyHeaders": { + "request": { + "Referer": "http://dood.to/", + }, }, }, - }, - "name": "WebStreamr N/A", - "title": "šŸ“‚ the-devils-bath-2024-sd-sub-ita + "name": "WebStreamr N/A", + "title": "šŸ“‚ the-devils-bath-2024-sd-sub-ita 🌐 Italian šŸ‡®šŸ‡¹ šŸ”— DoodStream", - "url": "https://kk892as.cloudatacdn.com/u5kj7j7s27d3sdgge5woezkbi4pu672wktq3aqujw47rbpl3xog2gtqnw2xa/awn9rgotuu~mocked-random-string?token=aw2v2d8uhbrj2ky54d573ujk&expiry=639837296000", - }, - { - "behaviorHints": { - "bingeGroup": "webstreamr-external_de", + "url": "https://kk892as.cloudatacdn.com/u5kj7j7s27d3sdgge5woezkbi4pu672wktq3aqujw47rbpl3xog2gtqnw2xa/awn9rgotuu~mocked-random-string?token=aw2v2d8uhbrj2ky54d573ujk&expiry=639837296000", }, - "externalUrl": "https://mixdrop.ag/e/3nzwveprim63or6", - "name": "WebStreamr N/A āš ļø external", - "title": "🌐 German šŸ‡©šŸ‡Ŗ + { + "behaviorHints": { + "bingeGroup": "webstreamr-external_de", + }, + "externalUrl": "https://mixdrop.ag/e/3nzwveprim63or6", + "name": "WebStreamr N/A āš ļø external", + "title": "🌐 German šŸ‡©šŸ‡Ŗ šŸ”— mixdrop.ag", - }, - { - "behaviorHints": { - "bingeGroup": "webstreamr-external_it", }, - "externalUrl": "https://mixdrop.ag/e/vk196d6xfzwwo1", - "name": "WebStreamr N/A āš ļø external", - "title": "🌐 Italian šŸ‡®šŸ‡¹ + { + "behaviorHints": { + "bingeGroup": "webstreamr-external_it", + }, + "externalUrl": "https://mixdrop.ag/e/vk196d6xfzwwo1", + "name": "WebStreamr N/A āš ļø external", + "title": "🌐 Italian šŸ‡®šŸ‡¹ šŸ”— mixdrop.ag", - }, -] + }, + ], + "ttl": 900000, +} `;