From e1e8f37862874f3e178b4e4dbd2f44bfcc47a0ab Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:20:12 +0000 Subject: [PATCH] feat(extractor): bring back Fsst --- src/extractor/Fsst.test.ts | 16 ++ src/extractor/Fsst.ts | 43 ++++ ...c93e1a94d9c7611948000948429948429_720p.mp4 | 1 + ...8a2d20cb476bf49948000948429948429_360p.mp4 | 1 + ...7986a6f5e1c7e4ef6d10948000948429948429.mp4 | 1 + .../Fsst/https:fsst.onlineembed948429 | 211 ++++++++++++++++++ src/extractor/__snapshots__/Fsst.test.ts.snap | 42 ++++ src/extractor/index.ts | 2 + src/utils/Fetcher.ts | 11 +- src/utils/FetcherMock.ts | 8 +- 10 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 src/extractor/Fsst.test.ts create mode 100644 src/extractor/Fsst.ts create mode 100644 src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file182b29f2b5e347fae9289b47644f3c93e1a94d9c7611948000948429948429_720p.mp4 create mode 100644 src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file185f4c7a9db5fbdfe139db2e22c9c8a2d20cb476bf49948000948429948429_360p.mp4 create mode 100644 src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file18f4595a22a9b081291cd4107986a6f5e1c7e4ef6d10948000948429948429.mp4 create mode 100644 src/extractor/__fixtures__/Fsst/https:fsst.onlineembed948429 create mode 100644 src/extractor/__snapshots__/Fsst.test.ts.snap diff --git a/src/extractor/Fsst.test.ts b/src/extractor/Fsst.test.ts new file mode 100644 index 0000000..9583f16 --- /dev/null +++ b/src/extractor/Fsst.test.ts @@ -0,0 +1,16 @@ +import winston from 'winston'; +import { createTestContext } from '../test'; +import { FetcherMock } from '../utils'; +import { ExtractorRegistry } from './ExtractorRegistry'; +import { Fsst } from './Fsst'; + +const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }); +const extractorRegistry = new ExtractorRegistry(logger, [new Fsst(new FetcherMock(`${__dirname}/__fixtures__/Fsst`))]); + +const ctx = createTestContext(); + +describe('Fsst', () => { + test('Wake up Dead Man', async () => { + expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/948429/'))).toMatchSnapshot(); + }); +}); diff --git a/src/extractor/Fsst.ts b/src/extractor/Fsst.ts new file mode 100644 index 0000000..36bea44 --- /dev/null +++ b/src/extractor/Fsst.ts @@ -0,0 +1,43 @@ +import * as cheerio from 'cheerio'; +import { Context, Format, Meta, UrlResult } from '../types'; +import { Extractor } from './Extractor'; + +export class Fsst extends Extractor { + public readonly id = 'fsst'; + + public readonly label = 'Fsst'; + + public supports(_ctx: Context, url: URL): boolean { + return null !== url.host.match(/fsst/); + }; + + protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise { + const headers = { Referer: meta.referer ?? url.href }; + + const html = await this.fetcher.text(ctx, url, { headers, noProxyHeaders: true }); + + const $ = cheerio.load(html); + const title = $('title').text().trim(); + + const filesMatch = html.match(/file:"(.*)"/) as string[]; + + return Promise.all( + (filesMatch[1] as string).split(',').map(async (fileString) => { + const heightAndUrlMatch = fileString.match(/\[?([\d]*)p?]?(.*)/) as string[]; + const fileHref = heightAndUrlMatch[2] as string; + + return { + url: await this.fetcher.getFinalRedirectUrl(ctx, new URL(fileHref), { headers, noProxyHeaders: true }, 1), + format: Format.mp4, + label: this.label, + sourceId: `${this.id}_${meta.countryCodes?.join('_')}`, + meta: { + ...meta, + height: parseInt(heightAndUrlMatch[1] as string), + title, + }, + }; + }), + ); + }; +} diff --git a/src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file182b29f2b5e347fae9289b47644f3c93e1a94d9c7611948000948429948429_720p.mp4 b/src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file182b29f2b5e347fae9289b47644f3c93e1a94d9c7611948000948429948429_720p.mp4 new file mode 100644 index 0000000..1129c2f --- /dev/null +++ b/src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file182b29f2b5e347fae9289b47644f3c93e1a94d9c7611948000948429948429_720p.mp4 @@ -0,0 +1 @@ +{"server":"nginx","date":"Tue, 06 Jan 2026 15:18:15 GMT","content-type":"text/html; charset=UTF-8","connection":"close","referrer-policy":"strict-origin-when-cross-origin","location":"https://a41.filevideo1.com/remote_control.php?time=1767712695&cv=a5df8582e6558fcf6bc25d7f02e5e646&lr=0&cv2=81173262e5b3e34fa9ebf67326ca98a5&file=%2Fvideos%2F948000%2F948429%2F948429_720p.mp4&cv3=2d8ee1a66c0ca6ce75d84c67a50a70e9&cv4=6974d60c75ea0485c4f2a71b164a21ce"} \ No newline at end of file diff --git a/src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file185f4c7a9db5fbdfe139db2e22c9c8a2d20cb476bf49948000948429948429_360p.mp4 b/src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file185f4c7a9db5fbdfe139db2e22c9c8a2d20cb476bf49948000948429948429_360p.mp4 new file mode 100644 index 0000000..1642353 --- /dev/null +++ b/src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file185f4c7a9db5fbdfe139db2e22c9c8a2d20cb476bf49948000948429948429_360p.mp4 @@ -0,0 +1 @@ +{"server":"nginx","date":"Tue, 06 Jan 2026 15:18:15 GMT","content-type":"text/html; charset=UTF-8","connection":"close","referrer-policy":"strict-origin-when-cross-origin","location":"https://a41.filevideo1.com/remote_control.php?time=1767712695&cv=a5df8582e6558fcf6bc25d7f02e5e646&lr=0&cv2=81173262e5b3e34fa9ebf67326ca98a5&file=%2Fvideos%2F948000%2F948429%2F948429_360p.mp4&cv3=2d8ee1a66c0ca6ce75d84c67a50a70e9&cv4=3ea2409b52380eb78c22c143fa6e27db"} \ No newline at end of file diff --git a/src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file18f4595a22a9b081291cd4107986a6f5e1c7e4ef6d10948000948429948429.mp4 b/src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file18f4595a22a9b081291cd4107986a6f5e1c7e4ef6d10948000948429948429.mp4 new file mode 100644 index 0000000..ddfb3fc --- /dev/null +++ b/src/extractor/__fixtures__/Fsst/head-https:www.secvideo1.onlineget_file18f4595a22a9b081291cd4107986a6f5e1c7e4ef6d10948000948429948429.mp4 @@ -0,0 +1 @@ +{"server":"nginx","date":"Tue, 06 Jan 2026 15:18:15 GMT","content-type":"text/html; charset=UTF-8","connection":"close","referrer-policy":"strict-origin-when-cross-origin","location":"https://a41.filevideo1.com/remote_control.php?time=1767712695&cv=a5df8582e6558fcf6bc25d7f02e5e646&lr=0&cv2=81173262e5b3e34fa9ebf67326ca98a5&file=%2Fvideos%2F948000%2F948429%2F948429.mp4&cv3=2d8ee1a66c0ca6ce75d84c67a50a70e9&cv4=7cf7fccae0c48291e3452c8ca2277f70"} \ No newline at end of file diff --git a/src/extractor/__fixtures__/Fsst/https:fsst.onlineembed948429 b/src/extractor/__fixtures__/Fsst/https:fsst.onlineembed948429 new file mode 100644 index 0000000..9f336c5 --- /dev/null +++ b/src/extractor/__fixtures__/Fsst/https:fsst.onlineembed948429 @@ -0,0 +1,211 @@ + + + + Wake.Up.Dead.Man.A.Knives.Out.Mystery.2025.mkv + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/extractor/__snapshots__/Fsst.test.ts.snap b/src/extractor/__snapshots__/Fsst.test.ts.snap new file mode 100644 index 0000000..0953e88 --- /dev/null +++ b/src/extractor/__snapshots__/Fsst.test.ts.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Fsst Wake up Dead Man 1`] = ` +[ + { + "format": "mp4", + "label": "Fsst", + "meta": { + "countryCodes": [], + "extractorId": "fsst", + "height": 360, + "title": "Wake.Up.Dead.Man.A.Knives.Out.Mystery.2025.mkv", + }, + "sourceId": "fsst_", + "url": "https://a41.filevideo1.com/remote_control.php?time=1767712695&cv=a5df8582e6558fcf6bc25d7f02e5e646&lr=0&cv2=81173262e5b3e34fa9ebf67326ca98a5&file=%2Fvideos%2F948000%2F948429%2F948429_360p.mp4&cv3=2d8ee1a66c0ca6ce75d84c67a50a70e9&cv4=3ea2409b52380eb78c22c143fa6e27db", + }, + { + "format": "mp4", + "label": "Fsst", + "meta": { + "countryCodes": [], + "extractorId": "fsst", + "height": 720, + "title": "Wake.Up.Dead.Man.A.Knives.Out.Mystery.2025.mkv", + }, + "sourceId": "fsst_", + "url": "https://a41.filevideo1.com/remote_control.php?time=1767712695&cv=a5df8582e6558fcf6bc25d7f02e5e646&lr=0&cv2=81173262e5b3e34fa9ebf67326ca98a5&file=%2Fvideos%2F948000%2F948429%2F948429_720p.mp4&cv3=2d8ee1a66c0ca6ce75d84c67a50a70e9&cv4=6974d60c75ea0485c4f2a71b164a21ce", + }, + { + "format": "mp4", + "label": "Fsst", + "meta": { + "countryCodes": [], + "extractorId": "fsst", + "height": 1080, + "title": "Wake.Up.Dead.Man.A.Knives.Out.Mystery.2025.mkv", + }, + "sourceId": "fsst_", + "url": "https://a41.filevideo1.com/remote_control.php?time=1767712695&cv=a5df8582e6558fcf6bc25d7f02e5e646&lr=0&cv2=81173262e5b3e34fa9ebf67326ca98a5&file=%2Fvideos%2F948000%2F948429%2F948429.mp4&cv3=2d8ee1a66c0ca6ce75d84c67a50a70e9&cv4=7cf7fccae0c48291e3452c8ca2277f70", + }, +] +`; diff --git a/src/extractor/index.ts b/src/extractor/index.ts index 0e9bb29..738f415 100644 --- a/src/extractor/index.ts +++ b/src/extractor/index.ts @@ -6,6 +6,7 @@ import { Extractor } from './Extractor'; import { Fastream } from './Fastream'; import { FileLions } from './FileLions'; import { FileMoon } from './FileMoon'; +import { Fsst } from './Fsst'; import { HubCloud } from './HubCloud'; import { KinoGer } from './KinoGer'; import { LuluStream } from './LuluStream'; @@ -32,6 +33,7 @@ export const createExtractors = (fetcher: Fetcher): Extractor[] => { new Fastream(fetcher), new FileLions(fetcher), new FileMoon(fetcher), + new Fsst(fetcher), new HubCloud(fetcher), new KinoGer(fetcher), new LuluStream(fetcher), diff --git a/src/utils/Fetcher.ts b/src/utils/Fetcher.ts index 30a0d8d..ec562d1 100644 --- a/src/utils/Fetcher.ts +++ b/src/utils/Fetcher.ts @@ -41,6 +41,7 @@ type ProxyConfig = Pick { + public async getFinalRedirectUrl(ctx: Context, url: URL, requestConfig?: CustomRequestConfig, maxCount?: number, count?: number): Promise { const newRequestConfig = { ...requestConfig, method: 'HEAD', maxRedirects: 0 }; + if (count && maxCount && count >= maxCount) { + return url; + } + const response = await this.queuedFetch(ctx, url, newRequestConfig); if (response.status >= 300 && response.status < 400) { - return await this.getFinalRedirectUrl(ctx, new URL(response.headers['location']), newRequestConfig); + return await this.getFinalRedirectUrl(ctx, new URL(response.headers['location']), newRequestConfig, maxCount, (count ?? 0) + 1); } return url; @@ -171,7 +176,7 @@ export class Fetcher { 'User-Agent': this.hostUserAgentMap.get(url.host) ?? 'Mozilla', ...(hostUserAgent && { 'User-Agent': hostUserAgent }), ...(cookieString && { Cookie: cookieString }), - ...(ctx.ip && { + ...(ctx.ip && !requestConfig?.noProxyHeaders && { 'Forwarded': `by=unknown;for=${ctx.ip};host=${url.host};proto=${forwardedProto}`, 'X-Forwarded-For': ctx.ip, 'X-Forwarded-Host': url.host, diff --git a/src/utils/FetcherMock.ts b/src/utils/FetcherMock.ts index 348ee27..6b77de5 100644 --- a/src/utils/FetcherMock.ts +++ b/src/utils/FetcherMock.ts @@ -51,12 +51,16 @@ export class FetcherMock extends Fetcher { return (await this.fetchInternal(path, ctx, url, { ...init, method: 'HEAD' })).headers; }; - public override async getFinalRedirectUrl(ctx: Context, url: URL, requestConfig?: CustomRequestConfig): Promise { + public override async getFinalRedirectUrl(ctx: Context, url: URL, requestConfig?: CustomRequestConfig, maxCount?: number, count?: number): Promise { const newRequestConfig = { ...requestConfig, method: 'HEAD', maxRedirects: 0 }; + if (count && maxCount && count >= maxCount) { + return url; + } + const response = await this.fetch(ctx, url, newRequestConfig); if (response.headers['location']) { - return await this.getFinalRedirectUrl(ctx, new URL(response.headers['location']), newRequestConfig); + return await this.getFinalRedirectUrl(ctx, new URL(response.headers['location']), newRequestConfig, maxCount, (count ?? 0) + 1); } return url;