import { randomUUID } from 'node:crypto'; import express, { NextFunction, Request, Response } from 'express'; import winston from 'winston'; import { ConfigureController, ManifestController, StreamController } from './controller'; import { BlockedError, logErrorAndReturnNiceString } from './error'; import { createExtractors, ExtractorRegistry } from './extractor'; import { createSources, Source } from './source'; import { HomeCine } from './source/HomeCine'; import { MeineCloud } from './source/MeineCloud'; import { MostraGuarda } from './source/MostraGuarda'; import { contextFromRequestAndResponse, envGet, envIsProd, Fetcher, StreamResolver } from './utils'; if (envIsProd()) { console.log = console.warn = console.error = console.info = console.debug = () => { /* disable in favor of logger */ }; } const logger = winston.createLogger({ transports: [ new winston.transports.Console({ format: winston.format.combine( winston.format.cli(), winston.format.timestamp(), winston.format.printf(({ level, message, timestamp, id }) => `${timestamp} ${level} ${id}: ${message}`)), }), ], }); process.on('uncaughtException', (error: Error) => { logger.error(`Uncaught exception caught: ${error}, cause: ${error.cause}, stack: ${error.stack}`); }); process.on('unhandledRejection', (error: Error) => { logger.error(`Unhandled rejection: ${error}, cause: ${error.cause}, stack: ${error.stack}`); }); const fetcher = new Fetcher(logger); const sources = createSources(fetcher); const extractors = createExtractors(fetcher); const addon = express(); addon.set('trust proxy', true); addon.use((_req: Request, res: Response, next: NextFunction) => { res.setHeader('X-Request-ID', randomUUID()); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', '*'); if (envIsProd()) { res.setHeader('Cache-Control', 'max-age=10, public'); } next(); }); addon.use('/', (new ConfigureController(sources, extractors)).router); addon.use('/', (new ManifestController(sources, extractors)).router); const extractorRegistry = new ExtractorRegistry(logger, extractors); const streamResolver = new StreamResolver(logger, extractorRegistry); addon.use('/', (new StreamController(logger, sources, streamResolver)).router); addon.get('/', (_req: Request, res: Response) => { res.redirect('/configure'); }); addon.get('/startup', async (_req: Request, res: Response) => { res.json({ status: 'ok' }); }); addon.get('/ready', async (_req: Request, res: Response) => { res.json({ status: 'ok' }); }); let lastLiveProbeRequestsTimestamp = 0; addon.get('/live', async (req: Request, res: Response) => { const ctx = contextFromRequestAndResponse(req, res); const sources: Source[] = [ new HomeCine(fetcher), new MeineCloud(fetcher), new MostraGuarda(fetcher), ]; const hrefs = [ ...sources.map(source => source.baseUrl), 'https://cloudnestra.com', ]; const results = new Map(); let blockedCount = 0; let errorCount = 0; const fetchFactories = hrefs.map(href => async () => { const url = new URL(href); try { await fetcher.head(ctx, url, { noCache: true }); results.set(url.host, 'ok'); } catch (error) { if (error instanceof BlockedError) { results.set(url.host, 'blocked'); blockedCount++; } else { results.set(url.host, 'error'); errorCount++; } logErrorAndReturnNiceString(ctx, logger, href, error); } }); if (Date.now() - lastLiveProbeRequestsTimestamp > 60000 || 'force' in req.query) { // every minute await Promise.all(fetchFactories.map(fn => fn())); lastLiveProbeRequestsTimestamp = Date.now(); } const details = Object.fromEntries(results); if (blockedCount > 0) { // TODO: fail health check and try to get a clean IP if infra is ready logger.warn('IP might be not clean and leading to blocking.', ctx); res.json({ status: 'ok', details }); } else if (errorCount === sources.length) { res.status(503).json({ status: 'error', details }); } else { res.json({ status: 'ok', ipStatus: 'ok', details }); } }); addon.get('/stats', async (_req: Request, res: Response) => { res.json({ extractorRegistry: extractorRegistry.stats(), fetcher: fetcher.stats(), sources: Source.stats(), }); }); const port = parseInt(envGet('PORT') || '51546'); addon.listen(port, () => { logger.info(`Add-on Repository URL: http://127.0.0.1:${port}/manifest.json`); });