diff --git a/src/embed-extractor/Dropload.ts b/src/embed-extractor/Dropload.ts index 2455e8e..fc3ab51 100644 --- a/src/embed-extractor/Dropload.ts +++ b/src/embed-extractor/Dropload.ts @@ -1,5 +1,5 @@ import { EmbedExtractor } from './types'; -import { extractUrlFromPacked, Fetcher, iso2ToFlag, scanFromResolution } from '../utils'; +import { extractUrlFromPacked, Fetcher } from '../utils'; import { Context } from '../types'; export class Dropload implements EmbedExtractor { @@ -19,20 +19,18 @@ export class Dropload implements EmbedExtractor { const normalizedUrl = url.toString().replace('/e/', '').replace('/embed-', '/'); const html = await this.fetcher.text(ctx, normalizedUrl); - const resolution = scanFromResolution((html.match(/(\d{3,}x\d{3,}),/) as string[])[1] as string); + const height = (html.match(/\d{3,}x(\d{3,}),/) as string[])[1]; const sizeMatch = html.match(/([\d.]+) ?([GM]B)/) as string[]; const size = `${sizeMatch[1]} ${sizeMatch[2]}`; return { - url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]).toString(), - name: `WebStreamr ${resolution}`, - title: `${this.label} | 💾 ${size} | ${iso2ToFlag(language)}`, - behaviorHints: { - group: `webstreamr-${this.id}`, - }, - resolution, + url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]), + label: this.label, + sourceId: this.id, + height, size, + language, }; }; } diff --git a/src/embed-extractor/EmbedExtractors.ts b/src/embed-extractor/EmbedExtractors.ts index 52ad155..3989186 100644 --- a/src/embed-extractor/EmbedExtractors.ts +++ b/src/embed-extractor/EmbedExtractors.ts @@ -1,5 +1,5 @@ import { EmbedExtractor } from './types'; -import { Context, StreamWithMeta } from '../types'; +import { Context, UrlResult } from '../types'; export class EmbedExtractors { private readonly embedExtractors: EmbedExtractor[]; @@ -8,7 +8,7 @@ export class EmbedExtractors { this.embedExtractors = embedExtractors; } - readonly handle = async (ctx: Context, url: URL, language: string): Promise => { + readonly handle = async (ctx: Context, url: URL, language: string): Promise => { const embedExtractor = this.embedExtractors.find(embedExtractor => embedExtractor.supports(url)); if (undefined === embedExtractor) { diff --git a/src/embed-extractor/SuperVideo.ts b/src/embed-extractor/SuperVideo.ts index cd5938d..4809b61 100644 --- a/src/embed-extractor/SuperVideo.ts +++ b/src/embed-extractor/SuperVideo.ts @@ -1,5 +1,5 @@ import { EmbedExtractor } from './types'; -import { extractUrlFromPacked, Fetcher, iso2ToFlag, scanFromResolution } from '../utils'; +import { extractUrlFromPacked, Fetcher } from '../utils'; import { Context } from '../types'; export class SuperVideo implements EmbedExtractor { @@ -19,19 +19,17 @@ export class SuperVideo implements EmbedExtractor { const normalizedUrl = url.toString().replace('/e/', '/').replace('/embed-', '/'); const html = await this.fetcher.text(ctx, normalizedUrl); - const resolutionAndSizeMatch = html.match(/(\d{3,}x\d{3,}), ([\d.]+) ?([GM]B)/) as string[]; - const resolution = scanFromResolution(resolutionAndSizeMatch[1] as string); - const size = `${resolutionAndSizeMatch[2]} ${resolutionAndSizeMatch[3]}`; + const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+) ?([GM]B)/) as string[]; + const height = heightAndSizeMatch[1]; + const size = `${heightAndSizeMatch[2]} ${heightAndSizeMatch[3]}`; return { - url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]).toString(), - name: `WebStreamr ${resolution}`, - title: `${this.label} | 💾 ${size} | ${iso2ToFlag(language)}`, - behaviorHints: { - group: `webstreamr-${this.id}`, - }, - resolution, + url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]), + label: this.label, + sourceId: this.id, + height, size, + language, }; }; } diff --git a/src/embed-extractor/types.ts b/src/embed-extractor/types.ts index 8df24c7..24fd16c 100644 --- a/src/embed-extractor/types.ts +++ b/src/embed-extractor/types.ts @@ -1,4 +1,4 @@ -import { Context, StreamWithMeta } from '../types'; +import { Context, UrlResult } from '../types'; export interface EmbedExtractor { readonly id: string; @@ -7,5 +7,5 @@ export interface EmbedExtractor { readonly supports: (url: URL) => boolean; - readonly extract: (ctx: Context, url: URL, language: string) => Promise; + readonly extract: (ctx: Context, url: URL, language: string) => Promise; } diff --git a/src/handler/KinoKiste.test.ts b/src/handler/KinoKiste.test.ts index 78871c3..0e2d023 100644 --- a/src/handler/KinoKiste.test.ts +++ b/src/handler/KinoKiste.test.ts @@ -27,24 +27,22 @@ describe('KinoKiste', () => { expect(streams).toHaveLength(2); expect(streams[0]).toStrictEqual({ - behaviorHints: { - group: 'webstreamr-supervideo', - }, - name: 'WebStreamr 720p', - resolution: '720p', + url: expect.any(URL), + label: 'SuperVideo', + sourceId: 'supervideo', + height: '720', size: '699.8 MB', - title: 'SuperVideo | 💾 699.8 MB | 🇩🇪', - url: expect.stringMatching(/^https:\/\/.*?.m3u8/), + language: 'de', }); + expect(streams[0]?.url.href).toMatch(/^https:\/\/.*?.m3u8/); expect(streams[1]).toStrictEqual({ - behaviorHints: { - group: 'webstreamr-dropload', - }, - name: 'WebStreamr 720p', - resolution: '720p', + url: expect.any(URL), + label: 'Dropload', + sourceId: 'dropload', + height: '720', size: '699.8 MB', - title: 'Dropload | 💾 699.8 MB | 🇩🇪', - url: expect.stringMatching(/^https:\/\/.*?.m3u8/), + language: 'de', }); + expect(streams[1]?.url.href).toMatch(/^https:\/\/.*?.m3u8/); }); }); diff --git a/src/handler/MeineCloud.test.ts b/src/handler/MeineCloud.test.ts index 03d0418..49bde0a 100644 --- a/src/handler/MeineCloud.test.ts +++ b/src/handler/MeineCloud.test.ts @@ -27,24 +27,22 @@ describe('MeineCloud', () => { expect(streams).toHaveLength(2); expect(streams[0]).toStrictEqual({ - behaviorHints: { - group: 'webstreamr-supervideo', - }, - name: 'WebStreamr 720p', - resolution: '720p', + url: expect.any(URL), + label: 'SuperVideo', + sourceId: 'supervideo', + height: '720', size: '1.0 GB', - title: 'SuperVideo | 💾 1.0 GB | 🇩🇪', - url: expect.stringMatching(/^https:\/\/.*?.m3u8/), + language: 'de', }); + expect(streams[0]?.url.href).toMatch(/^https:\/\/.*?.m3u8/); expect(streams[1]).toStrictEqual({ - behaviorHints: { - group: 'webstreamr-dropload', - }, - name: 'WebStreamr 1080p', - resolution: '1080p', + url: expect.any(URL), + label: 'Dropload', + sourceId: 'dropload', + height: '1080', size: '1.3 GB', - title: 'Dropload | 💾 1.3 GB | 🇩🇪', - url: expect.stringMatching(/^https:\/\/.*?.m3u8/), + language: 'de', }); + expect(streams[1]?.url.href).toMatch(/^https:\/\/.*?.m3u8/); }); }); diff --git a/src/handler/types.ts b/src/handler/types.ts index 22958a2..9887ea2 100644 --- a/src/handler/types.ts +++ b/src/handler/types.ts @@ -1,4 +1,4 @@ -import { Context, StreamWithMeta } from '../types'; +import { Context, UrlResult } from '../types'; export interface Handler { readonly id: string; @@ -9,5 +9,5 @@ export interface Handler { readonly languages: string[]; - readonly handle: (ctx: Context, id: string) => Promise; + readonly handle: (ctx: Context, id: string) => Promise; } diff --git a/src/index.ts b/src/index.ts index 50087bb..287fbc6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,8 @@ import makeFetchHappen from 'make-fetch-happen'; import { landingTemplate } from './landingTemplate'; import { Handler, KinoKiste, MeineCloud } from './handler'; import { Dropload, EmbedExtractors, SuperVideo } from './embed-extractor'; -import { buildManifest, Fetcher, fulfillAllPromises, logInfo } from './utils'; -import { Config, StreamWithMeta } from './types'; +import { buildManifest, Fetcher, fulfillAllPromises, iso2ToFlag, logInfo } from './utils'; +import { Config, UrlResult } from './types'; import fs from 'node:fs'; import * as os from 'node:os'; @@ -95,29 +95,54 @@ addon.get('/:config/stream/:type/:id.json', async function (req: Request, res: R return; } - const streams: StreamWithMeta[] = []; + const urlResults: UrlResult[] = []; const handlerPromises = selectedHandlers.map(async (handler) => { if (!handler.contentTypes.includes(type)) { return; } - const handlerStreams = await handler.handle({ ip: req.ip as string }, id); - logInfo(`${handler.id} returned ${handlerStreams.length} streams`); + const handlerUrlResults = await handler.handle({ ip: req.ip as string }, id); + logInfo(`${handler.id} returned ${handlerUrlResults.length} urls`); - streams.push(...handlerStreams); + urlResults.push(...handlerUrlResults); }); await fulfillAllPromises(handlerPromises); - streams.sort((a, b) => { - const resolutionComparison = parseInt(b.resolution ?? '0') - parseInt(a.resolution ?? '0'); - if (resolutionComparison !== 0) { - return resolutionComparison; + urlResults.sort((a, b) => { + const heightComparison = parseInt(b.height ?? '0') - parseInt(a.height ?? '0'); + if (heightComparison !== 0) { + return heightComparison; } return parseFloat(b.size ?? '0') - parseFloat(a.size ?? '0'); }); - logInfo(`Return ${streams.length} streams`); + logInfo(`Return ${urlResults.length} streams`); + + const streams = urlResults.map((urlResult) => { + let name = 'WebStreamr'; + if (urlResult.height) { + name += ` ${urlResult.height}p`; + } + + let title = urlResult.label; + if (urlResult.size) { + title += ` | 💾 ${urlResult.size}`; + } + if (urlResult.language) { + title += ` | ${iso2ToFlag(urlResult.language)}`; + } + + return { + url: urlResult.url.toString(), + name, + title, + behaviourHints: { + group: `webstreamr-${urlResult.sourceId}`, + }, + }; + }); + res.send(JSON.stringify({ streams })); }); diff --git a/src/types.ts b/src/types.ts index b288266..4a43684 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { Manifest, ManifestConfig, Stream } from 'stremio-addon-sdk'; +import { Manifest, ManifestConfig } from 'stremio-addon-sdk'; export interface Context { ip: string } @@ -6,4 +6,11 @@ export type ManifestWithConfig = Manifest & { config: ManifestConfig[] }; export type Config = Record; -export type StreamWithMeta = Stream & { resolution: string | undefined; size: string | undefined }; +export interface UrlResult { + url: URL; + label: string; + sourceId: string; + height: string | undefined; + size: string | undefined; + language: string | undefined; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index f263404..a4b4c2e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,4 +5,3 @@ export * from './language'; export * from './log'; export * from './manifest'; export * from './promise'; -export * from './resolution'; diff --git a/src/utils/resolution.test.ts b/src/utils/resolution.test.ts deleted file mode 100644 index b8b983a..0000000 --- a/src/utils/resolution.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { scanFromResolution } from './resolution'; - -describe('scanFromResolution', () => { - test.each([ - ['720p', '1280x720'], - ['720p', '1280 x 720 '], - ['240p', '352x240'], - ['240p', '352x240'], - ['480p', '720x480'], - ['1080p', '1280x1080'], - ['2160p', '3840x2160'], - ['4320p', '7680x4320'], - ])('returns %s with %s', (output, input) => { - expect(scanFromResolution(input)).toBe(output); - }); - - test('logs and returns undefined for invalid resolution', () => { - const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); - - expect(scanFromResolution('foo')).toBe(undefined); - - expect(consoleWarnSpy).toHaveBeenCalled(); - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('"foo" is not a valid resolution')); - }); -}); diff --git a/src/utils/resolution.ts b/src/utils/resolution.ts deleted file mode 100644 index 06b94ee..0000000 --- a/src/utils/resolution.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { logWarn } from './log'; - -// Returns the scan from a resolution string. E.g. 720p for "1280x720" -// See https://en.wikipedia.org/wiki/List_of_common_display_resolutions#Digital_standards -export const scanFromResolution = (resolution: string): string | undefined => { - const height = parseInt(resolution.split('x')[1]?.trim() || ''); - - if (isNaN(height)) { - logWarn(`"${resolution}" is not a valid resolution`); - return undefined; - } - - return `${height}p`; -};