refactor: extract stream resolving and add tests
This commit is contained in:
parent
d4db80789a
commit
5665c6a6ff
6 changed files with 191 additions and 84 deletions
|
|
@ -1,22 +1,22 @@
|
|||
import { Request, Response, Router } from 'express';
|
||||
import { Stream } from 'stremio-addon-sdk';
|
||||
import { Handler } from '../handler';
|
||||
import { Config, UrlResult } from '../types';
|
||||
import bytes from 'bytes';
|
||||
import { flag } from 'country-emoji';
|
||||
import winston from 'winston';
|
||||
import { Handler } from '../handler';
|
||||
import { Config } from '../types';
|
||||
import { StreamResolver } from '../utils';
|
||||
|
||||
export class StreamController {
|
||||
public readonly router: Router;
|
||||
|
||||
private readonly logger: winston.Logger;
|
||||
private readonly handlers: Handler[];
|
||||
private readonly streamResolver: StreamResolver;
|
||||
|
||||
constructor(logger: winston.Logger, handlers: Handler[]) {
|
||||
constructor(logger: winston.Logger, handlers: Handler[], streams: StreamResolver) {
|
||||
this.router = Router();
|
||||
|
||||
this.logger = logger;
|
||||
this.handlers = handlers;
|
||||
this.streamResolver = streams;
|
||||
|
||||
this.router.get('/:config/stream/:type/:id.json', this.getStream.bind(this));
|
||||
}
|
||||
|
|
@ -28,83 +28,11 @@ export class StreamController {
|
|||
|
||||
this.logger.info(`Search stream for type "${type}" and id "${id}"`);
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
const selectedHandlers = this.handlers.filter(handler => handler.id in config);
|
||||
if (selectedHandlers.length === 0) {
|
||||
this.logger.info('No handlers configured, bail out');
|
||||
|
||||
res.send(JSON.stringify({
|
||||
streams: [{
|
||||
name: 'WebStreamr',
|
||||
title: '⚠️ No handlers found. Please re-configure the plugin.',
|
||||
ytId: 'E4WlUXrJgy4',
|
||||
}],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const streams: Stream[] = [];
|
||||
|
||||
const urlResults: UrlResult[] = [];
|
||||
const handlerPromises = selectedHandlers.map(async (handler) => {
|
||||
if (!handler.contentTypes.includes(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const handlerUrlResults = await handler.handle({ ip: req.ip as string }, id);
|
||||
this.logger.info(`${handler.id} returned ${handlerUrlResults.length} urls`);
|
||||
|
||||
urlResults.push(...(handlerUrlResults.filter(handlerUrlResult => handlerUrlResult !== undefined)));
|
||||
} catch (err) {
|
||||
streams.push({
|
||||
name: 'WebStreamr',
|
||||
title: `❌ Error with handler "${handler.id}". Please check the logs or create an issue if this persists.`,
|
||||
ytId: 'E4WlUXrJgy4',
|
||||
});
|
||||
this.logger.error(`${handler.id} error: ` + err);
|
||||
}
|
||||
});
|
||||
await Promise.all(handlerPromises);
|
||||
|
||||
urlResults.sort((a, b) => {
|
||||
const heightComparison = (b.height ?? 0) - (a.height ?? 0);
|
||||
if (heightComparison !== 0) {
|
||||
return heightComparison;
|
||||
}
|
||||
|
||||
return (b.bytes ?? 0) - (a.bytes ?? 0);
|
||||
});
|
||||
|
||||
this.logger.info(`Return ${urlResults.length} streams`);
|
||||
|
||||
streams.push(
|
||||
...urlResults.map((urlResult) => {
|
||||
let name = 'WebStreamr';
|
||||
if (urlResult.height) {
|
||||
name += ` ${urlResult.height}p`;
|
||||
}
|
||||
|
||||
let title = urlResult.label;
|
||||
if (urlResult.bytes) {
|
||||
title += ` | 💾 ${bytes.format(urlResult.bytes, { unitSeparator: ' ' })}`;
|
||||
}
|
||||
if (urlResult.countryCode) {
|
||||
title += ` | ${flag(urlResult.countryCode)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
url: urlResult.url.toString(),
|
||||
name,
|
||||
title,
|
||||
behaviourHints: {
|
||||
group: `webstreamr-${urlResult.sourceId}`,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
const streams = await this.streamResolver.resolve({ ip: (req.ip as string) }, selectedHandlers, type, id);
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(JSON.stringify({ streams }));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import winston from 'winston';
|
|||
import { FrenchCloud, Handler, KinoKiste, MeineCloud, MostraGuarda, VerHdLink } from './handler';
|
||||
import { EmbedExtractorRegistry } from './embed-extractor';
|
||||
import { ConfigureController, ManifestController, StreamController } from './controller';
|
||||
import { Fetcher } from './utils';
|
||||
import { Fetcher, StreamResolver } from './utils';
|
||||
import fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ addon.use((_req: Request, res: Response, next: NextFunction) => {
|
|||
|
||||
addon.use('/', (new ConfigureController(handlers)).router);
|
||||
addon.use('/', (new ManifestController(handlers)).router);
|
||||
addon.use('/', (new StreamController(logger, handlers)).router);
|
||||
addon.use('/', (new StreamController(logger, handlers, new StreamResolver(logger))).router);
|
||||
|
||||
addon.get('/', (_req: Request, res: Response) => {
|
||||
res.redirect('/configure');
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export interface UrlResult {
|
|||
url: URL;
|
||||
label: string;
|
||||
sourceId: string;
|
||||
height: number | undefined;
|
||||
bytes: number | undefined;
|
||||
height: number;
|
||||
bytes: number;
|
||||
countryCode: string | undefined;
|
||||
}
|
||||
|
|
|
|||
90
src/utils/StreamResolver.test.ts
Normal file
90
src/utils/StreamResolver.test.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import winston from 'winston';
|
||||
import { EmbedExtractorRegistry } from '../embed-extractor';
|
||||
import { StreamResolver } from './StreamResolver';
|
||||
import { MeineCloud, MostraGuarda } from '../handler';
|
||||
import { Fetcher } from './Fetcher';
|
||||
import { Context } from '../types';
|
||||
jest.mock('../utils/Fetcher');
|
||||
const streamResolver = new StreamResolver(winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }));
|
||||
const ctx: Context = { ip: '127.0.0.1' };
|
||||
|
||||
// @ts-expect-error No constructor args needed
|
||||
const fetcher = new Fetcher();
|
||||
const embedExtractorRegistry = new EmbedExtractorRegistry(fetcher);
|
||||
const meineCloud = new MeineCloud(fetcher, embedExtractorRegistry);
|
||||
const mostraGuarda = new MostraGuarda(fetcher, embedExtractorRegistry);
|
||||
|
||||
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',
|
||||
}]);
|
||||
});
|
||||
|
||||
test('returns handler errors as stream', async () => {
|
||||
jest.spyOn(fetcher, 'text').mockRejectedValue('ups, an error occurred.');
|
||||
|
||||
const streams = await streamResolver.resolve(ctx, [meineCloud], 'movie', 'tt123456789');
|
||||
|
||||
expect(streams).toStrictEqual([{
|
||||
name: 'WebStreamr',
|
||||
title: '❌ Error with handler "meinecloud". Please check the logs or create an issue if this persists.',
|
||||
ytId: 'E4WlUXrJgy4',
|
||||
}]);
|
||||
});
|
||||
|
||||
test('returns empty array if no handler found anything', async () => {
|
||||
const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'movie', 'tt12345678');
|
||||
|
||||
expect(streams).toStrictEqual([]);
|
||||
});
|
||||
|
||||
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([]);
|
||||
});
|
||||
|
||||
test('returns sorted results', async () => {
|
||||
const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'movie', 'tt29141112');
|
||||
|
||||
expect(streams).toStrictEqual([
|
||||
{
|
||||
url: expect.any(String),
|
||||
name: 'WebStreamr 1080p',
|
||||
title: 'Dropload | 💾 1.3 GB | 🇩🇪',
|
||||
behaviourHints: {
|
||||
group: 'webstreamr-dropload_de',
|
||||
},
|
||||
},
|
||||
{
|
||||
url: expect.any(String),
|
||||
name: 'WebStreamr 720p',
|
||||
title: 'SuperVideo | 💾 1.1 GB | 🇮🇹',
|
||||
behaviourHints: {
|
||||
group: 'webstreamr-supervideo_it',
|
||||
},
|
||||
},
|
||||
{
|
||||
url: expect.any(String),
|
||||
name: 'WebStreamr 720p',
|
||||
title: 'Dropload | 💾 1.1 GB | 🇮🇹',
|
||||
behaviourHints: {
|
||||
group: 'webstreamr-dropload_it',
|
||||
},
|
||||
},
|
||||
{
|
||||
url: expect.any(String),
|
||||
name: 'WebStreamr 720p',
|
||||
title: 'SuperVideo | 💾 1 GB | 🇩🇪',
|
||||
behaviourHints: {
|
||||
group: 'webstreamr-supervideo_de',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
88
src/utils/StreamResolver.ts
Normal file
88
src/utils/StreamResolver.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { Stream } from 'stremio-addon-sdk';
|
||||
import { flag } from 'country-emoji';
|
||||
import winston from 'winston';
|
||||
import bytes from 'bytes';
|
||||
import { Context, UrlResult } from '../types';
|
||||
import { Handler } from '../handler';
|
||||
|
||||
export class StreamResolver {
|
||||
private readonly logger: winston.Logger;
|
||||
|
||||
constructor(logger: winston.Logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
readonly resolve = async (ctx: Context, handlers: Handler[], type: string, id: string): Promise<Stream[]> => {
|
||||
if (handlers.length === 0) {
|
||||
return [{
|
||||
name: 'WebStreamr',
|
||||
title: '⚠️ No handlers found. Please re-configure the plugin.',
|
||||
ytId: 'E4WlUXrJgy4',
|
||||
}];
|
||||
}
|
||||
|
||||
const streams: Stream[] = [];
|
||||
|
||||
const urlResults: UrlResult[] = [];
|
||||
const handlerPromises = handlers.map(async (handler) => {
|
||||
if (!handler.contentTypes.includes(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const handlerUrlResults = await handler.handle(ctx, id);
|
||||
this.logger.info(`${handler.id} returned ${handlerUrlResults.length} urls`);
|
||||
|
||||
urlResults.push(...(handlerUrlResults.filter(handlerUrlResult => handlerUrlResult !== undefined)));
|
||||
} catch (err) {
|
||||
streams.push({
|
||||
name: 'WebStreamr',
|
||||
title: `❌ Error with handler "${handler.id}". Please check the logs or create an issue if this persists.`,
|
||||
ytId: 'E4WlUXrJgy4',
|
||||
});
|
||||
|
||||
this.logger.error(`${handler.id} error: ` + err);
|
||||
}
|
||||
});
|
||||
await Promise.all(handlerPromises);
|
||||
|
||||
urlResults.sort((a, b) => {
|
||||
const heightComparison = b.height - a.height;
|
||||
if (heightComparison !== 0) {
|
||||
return heightComparison;
|
||||
}
|
||||
|
||||
return b.bytes - a.bytes;
|
||||
});
|
||||
|
||||
this.logger.info(`Return ${urlResults.length} streams`);
|
||||
|
||||
streams.push(
|
||||
...urlResults.map((urlResult) => {
|
||||
let name = 'WebStreamr';
|
||||
if (urlResult.height) {
|
||||
name += ` ${urlResult.height}p`;
|
||||
}
|
||||
|
||||
let title = urlResult.label;
|
||||
if (urlResult.bytes) {
|
||||
title += ` | 💾 ${bytes.format(urlResult.bytes, { unitSeparator: ' ' })}`;
|
||||
}
|
||||
if (urlResult.countryCode) {
|
||||
title += ` | ${flag(urlResult.countryCode)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
url: urlResult.url.toString(),
|
||||
name,
|
||||
title,
|
||||
behaviourHints: {
|
||||
group: `webstreamr-${urlResult.sourceId}`,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return streams;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export * from './Fetcher';
|
||||
export * from './StreamResolver';
|
||||
export * from './embed';
|
||||
export * from './imdb';
|
||||
export * from './manifest';
|
||||
|
|
|
|||
Loading…
Reference in a new issue