refactor: make resolution and size optional and extract resolution helper

This should come in handy later when adding more providers
This commit is contained in:
WebStreamr 2025-05-08 18:39:23 +00:00
parent e063d71710
commit 3a87ddb9f0
No known key found for this signature in database
8 changed files with 67 additions and 14 deletions

View file

@ -43,12 +43,12 @@ type RequestConfig = Record<string, boolean | string | number>;
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');
});
};

View file

@ -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;

View file

@ -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('<html><body>Something to stream<script>(eval(function(p,a,c,k,e,d){...}))</script></body></html>'),
@ -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,
});
});
});

View file

@ -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<ParsePackedEmbedResult> => {

View file

@ -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);

View file

@ -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}`);
};

View file

@ -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'));
});
});

14
src/utils/resolution.ts Normal file
View file

@ -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`;
};