From 3a87ddb9f0072a1478d7d4d9fcf27be697530b2c Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Thu, 8 May 2025 18:39:23 +0000 Subject: [PATCH] refactor: make resolution and size optional and extract resolution helper This should come in handy later when adding more providers --- src/addon.ts | 4 ++-- src/handler/types.ts | 2 +- src/utils/embed.test.ts | 6 +++--- src/utils/embed.ts | 15 ++++++++------- src/utils/log.test.ts | 11 ++++++++++- src/utils/log.ts | 4 ++++ src/utils/resolution.test.ts | 25 +++++++++++++++++++++++++ src/utils/resolution.ts | 14 ++++++++++++++ 8 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 src/utils/resolution.test.ts create mode 100644 src/utils/resolution.ts diff --git a/src/addon.ts b/src/addon.ts index ec8a67e..ee78905 100644 --- a/src/addon.ts +++ b/src/addon.ts @@ -43,12 +43,12 @@ type RequestConfig = Record; const sortStreams = (streams: HandlerStream[]): void => { streams.sort((a, b) => { - const resolutionComparison = parseInt(b.resolution) - parseInt(a.resolution); + const resolutionComparison = parseInt(b.resolution ?? '0') - parseInt(a.resolution ?? '0'); if (resolutionComparison !== 0) { return resolutionComparison; } - return parseFloat(b.size) - parseFloat(a.size); + return parseFloat(b.size ?? '0') - parseFloat(a.size ?? '0'); }); }; diff --git a/src/handler/types.ts b/src/handler/types.ts index 7678f7a..3b1ff0f 100644 --- a/src/handler/types.ts +++ b/src/handler/types.ts @@ -1,6 +1,6 @@ import { Stream } from 'stremio-addon-sdk'; -export type HandlerStream = Stream & { resolution: string; size: string }; +export type HandlerStream = Stream & { resolution: string | undefined; size: string | undefined }; export interface Handler { readonly id: string; diff --git a/src/utils/embed.test.ts b/src/utils/embed.test.ts index 0bd2817..bf942c4 100644 --- a/src/utils/embed.test.ts +++ b/src/utils/embed.test.ts @@ -42,7 +42,7 @@ describe('unpackEmbedUrl', () => { }); }); - test('resolution and size return "?" if not found', async () => { + test('resolution and size return undefined if not found', async () => { (unpack as jest.Mock).mockReturnValue('{sources:[{file:"https://streaming-url.mp4"}'); (cachedFetchText as jest.Mock).mockReturnValue( Promise.resolve('Something to stream'), @@ -51,8 +51,8 @@ describe('unpackEmbedUrl', () => { const parsePackedEmbedResult = await parsePackedEmbed('https://some-url.test'); expect(parsePackedEmbedResult).toStrictEqual({ url: 'https://streaming-url.mp4', - resolution: '?', - size: '?', + resolution: undefined, + size: undefined, }); }); }); diff --git a/src/utils/embed.ts b/src/utils/embed.ts index 9196942..d733604 100644 --- a/src/utils/embed.ts +++ b/src/utils/embed.ts @@ -1,5 +1,6 @@ import { unpack } from 'unpacker'; import { cachedFetchText } from './fetch'; +import { scanFromResolution } from './resolution'; const LINK_REGEXPS = [ /sources:\[{file:"(.*?)"/, // dropload, supervideo @@ -7,20 +8,20 @@ const LINK_REGEXPS = [ interface ParsePackedEmbedResult { url: string; - resolution: string; - size: string; + resolution: string | undefined; + size: string | undefined; } -const findResolution = (html: string): string => { - const match = html.match(/\d{3,}x(\d{3,}),/); +const findResolution = (html: string): string | undefined => { + const match = html.match(/(\d{3,}x\d{3,}),/); - return match && match[1] ? `${match[1]}p` : '?'; + return match && match[1] ? scanFromResolution(match[1]) : undefined; }; -const findSize = (html: string): string => { +const findSize = (html: string): string | undefined => { const sizeMatch = html.match(/([\d.]+) ?([GM]B)/); - return sizeMatch && sizeMatch[1] && sizeMatch[2] ? `${sizeMatch[1]} ${sizeMatch[2]}` : '?'; + return sizeMatch && sizeMatch[1] && sizeMatch[2] ? `${sizeMatch[1]} ${sizeMatch[2]}` : undefined; }; export const parsePackedEmbed = async (url: string): Promise => { diff --git a/src/utils/log.test.ts b/src/utils/log.test.ts index 804e19f..0a9020a 100644 --- a/src/utils/log.test.ts +++ b/src/utils/log.test.ts @@ -1,4 +1,4 @@ -import { logError, logInfo } from './log'; +import { logError, logInfo, logWarn } from './log'; describe('log', () => { test('logError logs via console.error', () => { @@ -10,6 +10,15 @@ describe('log', () => { expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('some error occurred')); }); + test('logWarn logs via console.info', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + logWarn('something happened'); + + expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('something happened')); + }); + test('logInfo logs via console.info', () => { const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => undefined); diff --git a/src/utils/log.ts b/src/utils/log.ts index 28592d3..07dbc9e 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -2,6 +2,10 @@ export const logError = (message: string) => { console.error(`${new Date().toISOString()}, Error: ${message}`); }; +export const logWarn = (message: string) => { + console.warn(`${new Date().toISOString()}, Warn: ${message}`); +}; + export const logInfo = (message: string) => { console.info(`${new Date().toISOString()}, Info: ${message}`); }; diff --git a/src/utils/resolution.test.ts b/src/utils/resolution.test.ts new file mode 100644 index 0000000..b8b983a --- /dev/null +++ b/src/utils/resolution.test.ts @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000..06b94ee --- /dev/null +++ b/src/utils/resolution.ts @@ -0,0 +1,14 @@ +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`; +};