diff --git a/jest.config.ts b/jest.config.ts index 488fe94..47815d3 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -9,6 +9,9 @@ const config: Config = { '!/src/landingTemplate.ts', ], coverageDirectory: '/coverage', + coveragePathIgnorePatterns: [ + '/src/controller/', + ], coverageProvider: 'babel', coverageThreshold: { global: { diff --git a/src/controller/ConfigureController.ts b/src/controller/ConfigureController.ts new file mode 100644 index 0000000..6826eb3 --- /dev/null +++ b/src/controller/ConfigureController.ts @@ -0,0 +1,29 @@ +import { Request, Response, Router } from 'express'; +import { landingTemplate } from '../landingTemplate'; +import { buildManifest } from '../utils'; +import { Handler } from '../handler'; +import { Config } from '../types'; + +export class ConfigureController { + public readonly router: Router; + + private readonly handlers: Handler[]; + + constructor(handlers: Handler[]) { + this.router = Router(); + + this.handlers = handlers; + + this.router.get('/configure', this.getConfigure.bind(this)); + this.router.get('/:config/configure', this.getConfigure.bind(this)); + } + + private readonly getConfigure = (req: Request, res: Response) => { + const config: Config = JSON.parse(req.params['config'] || '{}'); + + const manifest = buildManifest(this.handlers, config); + + res.setHeader('content-type', 'text/html'); + res.send(landingTemplate(manifest)); + }; +} diff --git a/src/controller/ManifestController.ts b/src/controller/ManifestController.ts new file mode 100644 index 0000000..1edd1dc --- /dev/null +++ b/src/controller/ManifestController.ts @@ -0,0 +1,28 @@ +import { Request, Response, Router } from 'express'; +import { buildManifest } from '../utils'; +import { Handler } from '../handler'; +import { Config } from '../types'; + +export class ManifestController { + public readonly router: Router; + + private readonly handlers: Handler[]; + + constructor(handlers: Handler[]) { + this.router = Router(); + + this.handlers = handlers; + + this.router.get('/manifest.json', this.getManifest.bind(this)); + this.router.get('/:config/manifest.json', this.getManifest.bind(this)); + } + + private readonly getManifest = (req: Request, res: Response) => { + const config: Config = JSON.parse(req.params['config'] || '{}'); + + const manifest = buildManifest(this.handlers, config); + + res.setHeader('Content-Type', 'application/json'); + res.send(manifest); + }; +} diff --git a/src/controller/StreamController.ts b/src/controller/StreamController.ts new file mode 100644 index 0000000..c0322ee --- /dev/null +++ b/src/controller/StreamController.ts @@ -0,0 +1,100 @@ +import { Request, Response, Router } from 'express'; +import { Handler } from '../handler'; +import { Config, UrlResult } from '../types'; +import bytes from 'bytes'; +import { flag } from 'country-emoji'; +import winston from 'winston'; + +export class StreamController { + public readonly router: Router; + + private readonly logger: winston.Logger; + private readonly handlers: Handler[]; + + constructor(logger: winston.Logger, handlers: Handler[]) { + this.router = Router(); + + this.logger = logger; + this.handlers = handlers; + + this.router.get('/:config/stream/:type/:id.json', this.getStream.bind(this)); + } + + private readonly getStream = async (req: Request, res: Response) => { + const config: Config = JSON.parse(req.params['config'] || '{}'); + const type: string = req.params['type'] || ''; + const id: string = req.params['id'] || ''; + + 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 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); + } catch (err) { + 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`); + + const streams = 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}`, + }, + }; + }); + + res.send(JSON.stringify({ streams })); + }; +} diff --git a/src/controller/index.ts b/src/controller/index.ts new file mode 100644 index 0000000..95a75d3 --- /dev/null +++ b/src/controller/index.ts @@ -0,0 +1,3 @@ +export * from './ConfigureController'; +export * from './ManifestController'; +export * from './StreamController'; diff --git a/src/index.ts b/src/index.ts index bc457ff..0711ef1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,12 @@ import express, { NextFunction, Request, Response } from 'express'; import makeFetchHappen from 'make-fetch-happen'; -import { flag } from 'country-emoji'; import winston from 'winston'; -import { landingTemplate } from './landingTemplate'; import { FrenchCloud, Handler, KinoKiste, MeineCloud, MostraGuarda, VerHdLink } from './handler'; import { Dropload, EmbedExtractors, SuperVideo } from './embed-extractor'; -import { buildManifest, Fetcher } from './utils'; -import { Config, UrlResult } from './types'; +import { ConfigureController, ManifestController, StreamController } from './controller'; +import { Fetcher } from './utils'; import fs from 'node:fs'; import * as os from 'node:os'; -import bytes from 'bytes'; - -const addon = express(); -addon.set('trust proxy', true); const logger = winston.createLogger({ transports: [ @@ -47,6 +41,9 @@ const handlers: Handler[] = [ new VerHdLink(fetcher, embedExtractors), ]; +const addon = express(); +addon.set('trust proxy', true); + addon.use((_req: Request, res: Response, next: NextFunction) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', '*'); @@ -54,120 +51,14 @@ addon.use((_req: Request, res: Response, next: NextFunction) => { next(); }); +addon.use('/', (new ConfigureController(handlers)).router); +addon.use('/', (new ManifestController(handlers)).router); +addon.use('/', (new StreamController(logger, handlers)).router); + addon.get('/', (_req: Request, res: Response) => { res.redirect('/configure'); }); -addon.get('/configure', (_req: Request, res: Response) => { - const manifest = buildManifest(handlers, {}); - - res.setHeader('content-type', 'text/html'); - res.send(landingTemplate(manifest)); -}); - -addon.get('/:config/configure', (req: Request, res: Response) => { - const config: Config = JSON.parse(req.params['config'] || '{}'); - - const manifest = buildManifest(handlers, config); - - res.setHeader('content-type', 'text/html'); - res.send(landingTemplate(manifest)); -}); - -addon.get('/manifest.json', (_req: Request, res: Response) => { - const manifest = buildManifest(handlers, {}); - - res.setHeader('Content-Type', 'application/json'); - res.send(manifest); -}); - -addon.get('/:config/manifest.json', (req: Request, res: Response) => { - const config: Config = JSON.parse(req.params['config'] || '{}'); - - const manifest = buildManifest(handlers, config); - - res.setHeader('Content-Type', 'application/json'); - res.send(manifest); -}); - -addon.get('/:config/stream/:type/:id.json', async function (req: Request, res: Response) { - const config: Config = JSON.parse(req.params['config'] || '{}'); - const type: string = req.params['type'] || ''; - const id: string = req.params['id'] || ''; - - logger.info(`Search stream for type "${type}" and id "${id}"`); - - res.setHeader('Content-Type', 'application/json'); - - const selectedHandlers = handlers.filter(handler => handler.id in config); - if (selectedHandlers.length === 0) { - 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 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); - logger.info(`${handler.id} returned ${handlerUrlResults.length} urls`); - - urlResults.push(...handlerUrlResults); - } catch (err) { - 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); - }); - - logger.info(`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.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}`, - }, - }; - }); - - res.send(JSON.stringify({ streams })); -}); - const port = parseInt(process.env['PORT'] || '51546'); addon.listen(port, () => { logger.info(`Add-on Repository URL: http://127.0.0.1:${port}/manifest.json`);