From 22ca35890f9bd6b5e74c39fccd44a2987bc399e0 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Fri, 21 Feb 2025 18:01:21 +0000 Subject: [PATCH 01/10] fix!(crypto): use full 256-bit entropy for AES-256 key closes #87 --- packages/utils/src/crypto.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/utils/src/crypto.ts b/packages/utils/src/crypto.ts index 1c001ca2..783b5439 100644 --- a/packages/utils/src/crypto.ts +++ b/packages/utils/src/crypto.ts @@ -7,6 +7,22 @@ import { import { deflateSync, inflateSync } from 'zlib'; import { Settings } from './settings'; import JSONCrush from 'jsoncrush'; +export const loadSecretKey = (): Buffer => { + const secretKey = Settings.SECRET_KEY; + if (!secretKey) { + throw new Error('No secret key provided'); + } + // must be 64 characters long and hex + if (secretKey.length !== 64) { + throw new Error('Secret key must be 64 characters long'); + } + + if (!/^[0-9a-fA-F]+$/.test(secretKey)) { + throw new Error('Secret key must be a hex string (0-9, a-f)'); + } + + return Buffer.from(secretKey, 'hex'); +}; const pad = (data: Buffer, blockSize: number): Buffer => { const padding = blockSize - (data.length % blockSize); @@ -37,11 +53,7 @@ export const decompressData = (data: Buffer): string => { }; export const encryptData = (data: Buffer): { iv: string; data: string } => { - const secretKey = Settings.SECRET_KEY; - if (!secretKey) { - console.error('|ERR| crypto > compressAndEncrypt > No secret key provided'); - throw new Error('No secret key provided'); - } + const secretKey = loadSecretKey(); // Then encrypt the compressed data const iv = randomBytes(16); @@ -61,13 +73,7 @@ export const encryptData = (data: Buffer): { iv: string; data: string } => { }; export const decryptData = (encryptedData: Buffer, iv: Buffer): Buffer => { - const secretKey = Settings.SECRET_KEY; - if (!secretKey) { - console.error( - '|ERR| crypto > decryptAndDecompress > No secret key provided' - ); - throw new Error('No secret key provided'); - } + const secretKey = loadSecretKey(); const decipher = createDecipheriv('aes-256-cbc', secretKey, iv); // Decrypt the data From 70265971f910c5304b146005784ae17171b1c1be Mon Sep 17 00:00:00 2001 From: Viren070 Date: Fri, 21 Feb 2025 18:02:59 +0000 Subject: [PATCH 02/10] feat: use winston for logging --- package-lock.json | 225 ++++++++++++++++++++++- packages/addon/src/addon.ts | 211 ++++++++++++++------- packages/addon/src/server.ts | 156 ++++++++-------- packages/cloudflare-worker/package.json | 2 +- packages/frontend/next.config.ts | 8 + packages/utils/package.json | 3 +- packages/utils/src/crypto.ts | 8 +- packages/utils/src/index.ts | 1 + packages/utils/src/logger.ts | 83 +++++++++ packages/utils/src/mediaflow.ts | 41 ++--- packages/utils/src/settings.ts | 8 +- packages/wrappers/src/base.ts | 48 +++-- packages/wrappers/src/comet.ts | 8 +- packages/wrappers/src/debridio.ts | 10 +- packages/wrappers/src/jackettio.ts | 10 +- packages/wrappers/src/mediafusion.ts | 24 ++- packages/wrappers/src/orion.ts | 8 +- packages/wrappers/src/peerflix.ts | 16 +- packages/wrappers/src/stremio-jackett.ts | 16 +- packages/wrappers/src/torbox.ts | 10 +- packages/wrappers/src/torrentio.ts | 10 +- 21 files changed, 655 insertions(+), 251 deletions(-) create mode 100644 packages/utils/src/logger.ts diff --git a/package-lock.json b/package-lock.json index cc58ad7a..fa993483 100644 --- a/package-lock.json +++ b/package-lock.json @@ -329,6 +329,15 @@ "dev": true, "license": "MIT OR Apache-2.0" }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -342,6 +351,17 @@ "node": ">=12" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -2242,6 +2262,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.18.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz", @@ -2903,6 +2929,12 @@ "dev": true, "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/async-file": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/async-file/-/async-file-2.0.2.tgz", @@ -3301,7 +3333,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, "license": "MIT" }, "node_modules/color-string": { @@ -3309,12 +3340,46 @@ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", - "optional": true, "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/colorspace/node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/colorspace/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/colorspace/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, "node_modules/commander": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-6.1.0.tgz", @@ -3706,6 +3771,12 @@ "dev": true, "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -4572,6 +4643,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4675,6 +4752,12 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -5158,8 +5241,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/is-async-function": { "version": "2.0.0", @@ -5443,7 +5525,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5691,6 +5772,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -5754,6 +5841,23 @@ "dev": true, "license": "MIT" }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6299,6 +6403,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6730,6 +6843,20 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", @@ -7034,6 +7161,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7363,7 +7499,6 @@ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "license": "MIT", - "optional": true, "dependencies": { "is-arrayish": "^0.3.1" } @@ -7420,6 +7555,15 @@ "dev": true, "license": "MIT" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -7473,6 +7617,15 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -7673,6 +7826,12 @@ "node": ">=6" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -7739,6 +7898,15 @@ "node": ">=0.6" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -8006,6 +8174,12 @@ } } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8724,6 +8898,42 @@ "node": ">=8" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -9389,7 +9599,8 @@ "name": "@aiostreams/utils", "version": "1.13.8", "dependencies": { - "jsoncrush": "^1.1.8" + "jsoncrush": "^1.1.8", + "winston": "^3.17.0" } }, "packages/wrappers": { diff --git a/packages/addon/src/addon.ts b/packages/addon/src/addon.ts index a03b373e..96dbbd36 100644 --- a/packages/addon/src/addon.ts +++ b/packages/addon/src/addon.ts @@ -33,9 +33,12 @@ import { getMediaFlowPublicIp, getTimeTakenSincePoint, Settings, + createLogger, } from '@aiostreams/utils'; import { errorStream } from './responses'; +const logger = createLogger('addon'); + export class AIOStreams { private config: Config; @@ -74,17 +77,18 @@ export class AIOStreams { } this.config.requestingIp = ip; break; - } catch (error) { - console.error( - `|ERR| addon > getStreams: Failed to get requesting IP: ${error}, retrying ${ipRequestCount + 1}/3` - ); + } catch (error: any) { + logger.error(error, { + func: 'getRequestingIp', + }); ipRequestCount++; + if (ipRequestCount < 3) { + logger.info(`Retrying ${ipRequestCount}/3...`); + } } } if (ipRequestCount === 3) { - console.error( - '|ERR| addon > getStreams: Failed to get requesting IP after 3 attempts' - ); + logger.error('Failed to get requesting IP after 3 attempts'); if (this.config.mediaFlowConfig?.mediaFlowEnabled) { return [ errorStream('Aborted request after failing to get requesting IP'), @@ -94,8 +98,24 @@ export class AIOStreams { const { parsedStreams, errorStreams } = await this.getParsedStreams(streamRequest); - console.log( - `|INF| addon > getStreams: Got ${parsedStreams.length} parsed streams and ${errorStreams.length} error streams in ${getTimeTakenSincePoint(startTime)}` + const skipReasons = { + excludeKeywords: 0, + requiredKeywords: 0, + excludeLanguages: 0, + excludeResolutions: 0, + excludeQualities: 0, + excludeEncodes: 0, + excludeAudioTags: 0, + excludeVisualTags: 0, + excludeStreamTypes: 0, + excludeCached: 0, + sizeFilters: 0, + duplicateStreams: 0, + streamLimiters: 0, + }; + + logger.info( + `Got ${parsedStreams.length} parsed streams and ${errorStreams.length} error streams in ${getTimeTakenSincePoint(startTime)}` ); const filterStartTime = new Date().getTime(); @@ -120,8 +140,8 @@ export class AIOStreams { : null; excludeRegex || strictIncludeRegex - ? console.log( - `|INF| addon > getStreams: Created regex filters: excludeRegex: ${excludeRegex}, strictIncludeRegex: ${strictIncludeRegex}` + ? logger.debug( + `Using keyword regex filters: excludeRegex: ${excludeRegex}, strictIncludeRegex: ${strictIncludeRegex}` ) : null; @@ -129,17 +149,26 @@ export class AIOStreams { const streamTypeFilter = this.config.streamTypes?.find( (streamType) => streamType[parsedStream.type] === false ); - if (this.config.streamTypes && streamTypeFilter) return false; + if (this.config.streamTypes && streamTypeFilter) { + skipReasons.excludeStreamTypes++; + return false; + } const resolutionFilter = this.config.resolutions?.find( (resolution) => resolution[parsedStream.resolution] === false ); - if (resolutionFilter) return false; + if (resolutionFilter) { + skipReasons.excludeResolutions++; + return false; + } const qualityFilter = this.config.qualities?.find( (quality) => quality[parsedStream.quality] === false ); - if (this.config.qualities && qualityFilter) return false; + if (this.config.qualities && qualityFilter) { + skipReasons.excludeQualities++; + return false; + } // Check for HDR and DV tags in the parsed stream const hasHDR = parsedStream.visualTags.some((tag) => @@ -156,6 +185,7 @@ export class AIOStreams { if (hasHDRAndDV) { if (!HDRAndDVEnabled) { + skipReasons.excludeVisualTags++; return false; } } else if (hasHDR) { @@ -166,16 +196,21 @@ export class AIOStreams { (tag) => isTagDisabled(tag) === true ); if (disabledTags.length > 0) { - return false; + skipReasons.excludeVisualTags++; + return; } } else if (hasDV && isTagDisabled('DV')) { + skipReasons.excludeVisualTags++; return false; } // Check other visual tags for explicit disabling for (const tag of parsedStream.visualTags) { if (tag.startsWith('HDR') || tag === 'DV') continue; - if (isTagDisabled(tag)) return false; + if (isTagDisabled(tag)) { + skipReasons.excludeVisualTags++; + return false; + } } // apply excludedLanguages filter @@ -186,6 +221,7 @@ export class AIOStreams { excludedLanguages.includes(lang) ) ) { + skipReasons.excludeLanguages++; return false; } } else if ( @@ -193,74 +229,94 @@ export class AIOStreams { excludedLanguages.includes('Unknown') && parsedStream.languages.length === 0 ) { + skipReasons.excludeLanguages++; return false; } const audioTagFilter = parsedStream.audioTags.find((tag) => this.config.audioTags.some((audioTag) => audioTag[tag] === false) ); - if (audioTagFilter) return false; + if (audioTagFilter) { + skipReasons.excludeAudioTags++; + return false; + } if ( parsedStream.encode && this.config.encodes.some( (encode) => encode[parsedStream.encode] === false ) - ) + ) { + skipReasons.excludeEncodes++; return false; + } if ( this.config.onlyShowCachedStreams && parsedStream.provider && !parsedStream.provider.cached - ) + ) { + skipReasons.excludeCached++; return false; + } if ( this.config.minSize && parsedStream.size && parsedStream.size < this.config.minSize - ) + ) { + skipReasons.sizeFilters++; return false; + } if ( this.config.maxSize && parsedStream.size && parsedStream.size > this.config.maxSize - ) + ) { + skipReasons.sizeFilters++; return false; + } if ( streamRequest.type === 'movie' && this.config.maxMovieSize && parsedStream.size && parsedStream.size > this.config.maxMovieSize - ) + ) { + skipReasons.sizeFilters++; return false; + } if ( streamRequest.type === 'movie' && this.config.minMovieSize && parsedStream.size && parsedStream.size < this.config.minMovieSize - ) + ) { + skipReasons.sizeFilters++; return false; + } if ( streamRequest.type === 'series' && this.config.maxEpisodeSize && parsedStream.size && parsedStream.size > this.config.maxEpisodeSize - ) + ) { + skipReasons.sizeFilters++; return false; + } if ( streamRequest.type === 'series' && this.config.minEpisodeSize && parsedStream.size && parsedStream.size < this.config.minEpisodeSize - ) + ) { + skipReasons.sizeFilters++; return false; + } // apply keyword filters if ( @@ -269,9 +325,11 @@ export class AIOStreams { excludeRegex ) { if (parsedStream.filename && excludeRegex.test(parsedStream.filename)) { + skipReasons.excludeKeywords++; return false; } if (parsedStream.indexers && excludeRegex.test(parsedStream.indexers)) { + skipReasons.excludeKeywords++; return false; } } @@ -285,14 +343,15 @@ export class AIOStreams { parsedStream.filename && !strictIncludeRegex.test(parsedStream.filename) ) { + skipReasons.requiredKeywords++; return false; } } return true; }); - console.log( - `|INF| addon > getStreams: Initial filter to ${filteredResults.length} streams in ${getTimeTakenSincePoint(filterStartTime)}` + logger.info( + `Initial filter to ${filteredResults.length} streams in ${getTimeTakenSincePoint(filterStartTime)}` ); if (this.config.cleanResults) { @@ -332,16 +391,14 @@ export class AIOStreams { const cleanResultsStartTime = new Date().getTime(); // Deduplication by normalised filename const cleanResultsByFilenameStartTime = new Date().getTime(); - console.log( - `|INF| addon > cleaner: Received ${initialStreams.length} streams to clean` - ); + logger.info(`Received ${initialStreams.length} streams to clean`); const streamsGroupedByFilename = groupStreamsByKey( initialStreams, (stream) => normaliseFilename(stream.filename) ); - console.log( - `|INF| addon > cleaner: Found ${Object.keys(streamsGroupedByFilename).length} unique filenames` + logger.info( + `Found ${Object.keys(streamsGroupedByFilename).length} unique filenames` ); // Process grouped streams by filename @@ -349,8 +406,8 @@ export class AIOStreams { streamsGroupedByFilename ); - console.log( - `|INF| addon > cleaner: Deduplicated streams by filename to ${cleanedStreamsByFilename.length} streams in ${getTimeTakenSincePoint(cleanResultsByFilenameStartTime)}` + logger.info( + `Deduplicated streams by filename to ${cleanedStreamsByFilename.length} streams in ${getTimeTakenSincePoint(cleanResultsByFilenameStartTime)}` ); // Deduplication by hash @@ -360,22 +417,24 @@ export class AIOStreams { cleanedStreamsByFilename, (stream) => stream._infoHash ); - console.log( - `|INF| addon > cleaner: Found ${Object.keys(streamsGroupedByHash).length} unique hashes with ${cleanedStreamsByFilename.length - Object.values(streamsGroupedByHash).reduce((sum, group) => sum + group.length, 0)} streams not grouped` + logger.info( + `Found ${Object.keys(streamsGroupedByHash).length} unique hashes with ${cleanedStreamsByFilename.length - Object.values(streamsGroupedByHash).reduce((sum, group) => sum + group.length, 0)} streams not grouped` ); // Process grouped streams by hash const cleanedStreamsByHash = await this.processGroupedStreams(streamsGroupedByHash); - console.log( - `|INF| addon > cleaner: Deduplicated streams by hash to ${cleanedStreamsByHash.length} streams in ${getTimeTakenSincePoint(cleanResultsByHashStartTime)}` + logger.info( + `Deduplicated streams by hash to ${cleanedStreamsByHash.length} streams in ${getTimeTakenSincePoint(cleanResultsByHashStartTime)}` ); cleanedStreams.push(...cleanedStreamsByHash); - console.log( - `|INF| addon > cleaner: Deduplicated streams to ${cleanedStreams.length} streams in ${getTimeTakenSincePoint(cleanResultsStartTime)}` + logger.info( + `Deduplicated streams to ${cleanedStreams.length} streams in ${getTimeTakenSincePoint(cleanResultsStartTime)}` ); + skipReasons.duplicateStreams = + filteredResults.length - cleanedStreams.length; filteredResults = cleanedStreams; } // Apply sorting @@ -403,9 +462,7 @@ export class AIOStreams { return 0; }); - console.log( - `|INF| addon > getStreams: Sorted results in ${getTimeTakenSincePoint(sortStartTime)}` - ); + logger.info(`Sorted results in ${getTimeTakenSincePoint(sortStartTime)}`); // apply config.maxResultsPerResolution if (this.config.maxResultsPerResolution) { @@ -423,14 +480,34 @@ export class AIOStreams { return false; }); - + skipReasons.streamLimiters = + filteredResults.length - limitedResults.length; filteredResults = limitedResults; - console.log( - `|INF| addon > getStreams: Limited results to ${limitedResults.length} streams after applying maxResultsPerResolution in ${new Date().getTime() - startTime}ms` + logger.info( + `Limited results to ${limitedResults.length} streams after applying maxResultsPerResolution in ${new Date().getTime() - startTime}ms` ); } + const totalSkipped = Object.values(skipReasons).reduce( + (acc, val) => acc + val, + 0 + ); + const reportLines = [ + '╔═══════════════════════╤════════════╗', + '║ Skip Reason │ Count ║', + '╟───────────────────────┼────────────╢', + ...Object.entries(skipReasons).map( + ([reason, count]) => + `║ ${reason.padEnd(21)} │ ${String(count).padStart(10)} ║` + ), + '╟───────────────────────┼────────────╢', + `║ Total Skipped │ ${String(totalSkipped).padStart(10)} ║`, + '╚═══════════════════════╧════════════╝', + ]; + + logger.info('\n' + reportLines.join('\n')); + // Create stream objects const streamsStartTime = new Date().getTime(); const streamObjects = await Promise.all( @@ -443,11 +520,11 @@ export class AIOStreams { ...errorStreams.map((e) => errorStream(e.error, e.addon.name)) ); - console.log( - `|INF| addon > getStreams: Created ${streams.length} stream objects in ${getTimeTakenSincePoint(streamsStartTime)}` + logger.info( + `Created ${streams.length} stream objects in ${getTimeTakenSincePoint(streamsStartTime)}` ); - console.log( - `|INF| addon > getStreams: Total time taken to serve streams: ${getTimeTakenSincePoint(startTime)}` + logger.info( + `Total time taken to get streams: ${getTimeTakenSincePoint(startTime)}` ); return streams; } @@ -458,8 +535,9 @@ export class AIOStreams { description: string ): Stream { if (!parsedStream.url) { - console.error( - `|ERR| addon > createMediaFlowStream: Stream URL is missing, cannot proxy a stream without a URL` + logger.error( + `Stream URL is missing, cannot proxy a stream without a URL`, + { func: 'createMediaFlowStream' } ); throw new Error('Stream URL is missing'); } @@ -506,8 +584,8 @@ export class AIOStreams { if (!mediaFlowConfig.mediaFlowEnabled) return false; if (!stream.url) return false; // // now check if mediaFlowConfig.proxiedAddons or mediaFlowConfig.proxiedServices is not null - // console.log(this.config.mediaFlowConfig?.proxiedAddons); - // console.log(stream.addon.id); + // logger.info(this.config.mediaFlowConfig?.proxiedAddons); + // logger.info(stream.addon.id); if ( mediaFlowConfig.proxiedAddons && mediaFlowConfig.proxiedAddons.length > 0 && @@ -603,9 +681,7 @@ export class AIOStreams { } return mediaFlowStream; } catch (error) { - console.error( - `|ERR| addon > createStreamObject: Failed to create MediaFlow stream URL: ${error}` - ); + logger.error(`Failed to create MediaFlow stream URL: ${error}`); return null; } } @@ -892,7 +968,6 @@ export class AIOStreams { addon.options.overrideName || addonDetails.find((addonDetail) => addonDetail.id === addon.id)?.name || addon.id; - console.log(addon.options); const addonId = `${addon.id}-${JSON.stringify(addon.options)}`; try { const startTime = new Date().getTime(); @@ -908,13 +983,11 @@ export class AIOStreams { addon: { id: addonId, name: addonName }, })) ); - console.log( - `|INF| addon > getParsedStreams: Got ${addonStreams.length} streams ${addonErrors.length > 0 ? `and ${addonErrors.length} errors ` : ''}from addon ${addonName} in ${getTimeTakenSincePoint(startTime)}` + logger.info( + `Got ${addonStreams.length} streams ${addonErrors.length > 0 ? `and ${addonErrors.length} errors ` : ''}from addon ${addonName} in ${getTimeTakenSincePoint(startTime)}` ); } catch (error: any) { - console.error( - `|ERR| addon > getParsedStreams: Failed to get streams from ${addonName}: ${error}` - ); + logger.error(`Failed to get streams from ${addonName}: ${error}`); errorStreams.push({ error: formatError(error.message ?? error ?? 'Unknown error'), addon: { @@ -1075,16 +1148,16 @@ export class AIOStreams { return; } - /*console.log( + /*logger.info( `==================\nDetermining unique streams for ${groupedStreams[0].filename} from ${groupedStreams.length} total duplicates` ); - console.log( + logger.info( groupedStreams.map( (stream) => `Addon ID: ${stream.addon.id}, Provider ID: ${stream.provider?.id}, Provider Cached: ${stream.provider?.cached}, type: ${stream.torrent ? 'torrent' : 'usenet'}` ) ); - console.log('==================');*/ + logger.info('==================');*/ // Separate streams into categories const cachedStreams = groupedStreams.filter( (stream) => stream.provider?.cached || (!stream.provider && stream.url) @@ -1119,7 +1192,7 @@ export class AIOStreams { return aIndex - bIndex; })[0]; }); - //selectedUncachedStreams.forEach(stream => console.log(`Selected uncached stream for provider ${stream.provider!.id}: Addon ID: ${stream.addon.id}`)); + //selectedUncachedStreams.forEach(stream => logger.info(`Selected uncached stream for provider ${stream.provider!.id}: Addon ID: ${stream.addon.id}`)); // Select cached streams by provider and addon priority const selectedCachedStream = cachedStreams.sort((a, b) => { @@ -1175,11 +1248,11 @@ export class AIOStreams { // Combine selected streams for this group if (selectedNoProviderStream) { - //console.log(`Selected no provider stream: Addon ID: ${selectedNoProviderStream.addon.id}`); + //logger.info(`Selected no provider stream: Addon ID: ${selectedNoProviderStream.addon.id}`); uniqueStreams.push(selectedNoProviderStream); } if (selectedCachedStream) { - //console.log(`Selected cached stream for provider ${selectedCachedStream.provider!.id} from Addon ID: ${selectedCachedStream.addon.id}`); + //logger.info(`Selected cached stream for provider ${selectedCachedStream.provider!.id} from Addon ID: ${selectedCachedStream.addon.id}`); uniqueStreams.push(selectedCachedStream); } uniqueStreams.push(...selectedUncachedStreams); diff --git a/packages/addon/src/server.ts b/packages/addon/src/server.ts index a710b29e..7d65c581 100644 --- a/packages/addon/src/server.ts +++ b/packages/addon/src/server.ts @@ -19,17 +19,21 @@ import { decompressData, decryptData, uncrushJson, + loadSecretKey, + createLogger, + getTimeTakenSincePoint, } from '@aiostreams/utils'; +const logger = createLogger('server'); + const app = express(); -console.log(`|INF| server > init: Starting server and loading settings...`); +//logger.info(`Starting server and loading settings...`); +logger.info('Starting server and loading settings...', { func: 'init' }); Object.entries(Settings).forEach(([key, value]) => { switch (key) { case 'SECRET_KEY': if (value) { - console.log( - `|INF| server > init: ${key} = ${value.replace(/./g, '*').slice(0, 32)}` - ); + logger.info(`${key} = ${value.replace(/./g, '*').slice(0, 64)}`); } break; @@ -39,30 +43,29 @@ Object.entries(Settings).forEach(([key, value]) => { break; default: - console.log(`|INF| server > init: ${key} = ${value}`); + logger.info(`${key} = ${value}`); } }); -if (!Settings.SECRET_KEY) { - console.warn( - '|WRN| server > init: SECRET_KEY is not set, data encryption is disabled!' - ); -} - let CUSTOM_CONFIGS: Record = {}; if (Settings.CUSTOM_CONFIGS) { try { CUSTOM_CONFIGS = JSON.parse(Settings.CUSTOM_CONFIGS); - console.log( - `|INF| server > init: Loaded ${Object.keys(CUSTOM_CONFIGS).length} custom configs under aliases ${Object.keys(CUSTOM_CONFIGS).join(', ')}` + logger.info( + `Loaded ${Object.keys(CUSTOM_CONFIGS).length} custom configs under aliases ${Object.keys(CUSTOM_CONFIGS).join(', ')}` ); } catch (error: any) { - console.error( - `|ERR| server > init: CUSTOM_CONFIGS is not valid JSON: ${error.message}` - ); + logger.error(`CUSTOM_CONFIGS is not valid JSON: ${error.message}`); } } +// attempt to load the secret key +try { + if (Settings.SECRET_KEY) loadSecretKey(); +} catch (error: any) { + logger.error(`Invalid SECRET_KEY - ${error.message}`); +} + const cache = new Cache(Settings.MAX_CACHE_SIZE); // Built-in middleware for parsing JSON @@ -72,16 +75,19 @@ app.use(express.urlencoded({ extended: true })); // unhandled errors app.use((err: any, req: Request, res: Response, next: any) => { - console.error(`|ERR| server > ${err.message}`); + logger.error(`${err.message}`); res.status(500).send('Internal server error'); }); app.use((req, res, next) => { res.append('Access-Control-Allow-Origin', '*'); res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); - console.log( - `|DBG| server > ${req.method} ${req.path.replace(/\/eyJ[\w\=]+/g, '/*******').replace(/\/E2?-[\w-\%]+/g, '/*******')}` - ); + const start = Date.now(); + res.on('finish', () => { + logger.info( + `${req.method} ${req.path.replace(/\/eyJ[\w\=]+/g, '/*******').replace(/\/(E2?|B)?-[\w-\%]+/g, '/*******')} - ${res.statusCode} - ${getTimeTakenSincePoint(start)}` + ); + }); next(); }); @@ -118,9 +124,7 @@ app.get('/:config/configure', (req, res) => { try { let configJson = extractJsonConfig(config); if (config.startsWith('E-') || config.startsWith('E2-')) { - console.log( - `|DBG| server > Encrypted config detected, encrypting credentials` - ); + logger.info(`Encrypted config detected, encrypting credentials`); configJson = encryptInfoInConfig(configJson); } const base64Config = Buffer.from(JSON.stringify(configJson)).toString( @@ -128,7 +132,7 @@ app.get('/:config/configure', (req, res) => { ); res.redirect(`/${encodeURIComponent(base64Config)}/configure`); } catch (error: any) { - console.error(`|ERR| server > Failed to extract config: ${error.message}`); + logger.error(`Failed to extract config: ${error.message}`); res.status(400).send('Invalid config'); } }); @@ -142,24 +146,22 @@ app.get('/:config/manifest.json', (req, res) => { let configJson: Config; try { configJson = extractJsonConfig(config); - console.log(`|DBG| server > Extracted config for manifest request`); + logger.info(`Extracted config for manifest request`); configJson = decryptEncryptedInfoFromConfig(configJson); if (Settings.LOG_SENSITIVE_INFO) { - console.log(`|DBG| server > Final config: ${JSON.stringify(configJson)}`); + logger.info(`Final config: ${JSON.stringify(configJson)}`); } - console.log( - `|DBG| server > Successfully removed or decrypted sensitive info` - ); + logger.info(`Successfully removed or decrypted sensitive info`); const { valid, errorMessage } = validateConfig(configJson); if (!valid) { - console.error( - `|ERR| server > Received invalid config for manifest request: ${errorMessage}` + logger.error( + `Received invalid config for manifest request: ${errorMessage}` ); res.status(400).json({ error: 'Invalid config', message: errorMessage }); return; } } catch (error: any) { - console.error(`|ERR| server > Failed to extract config: ${error.message}`); + logger.error(`Failed to extract config: ${error.message}`); res.status(400).json({ error: 'Invalid config' }); return; } @@ -184,16 +186,14 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { let configJson: Config; try { configJson = extractJsonConfig(config); - console.log(`|DBG| server > Extracted config for stream request`); + logger.info(`Extracted config for stream request`); configJson = decryptEncryptedInfoFromConfig(configJson); if (Settings.LOG_SENSITIVE_INFO) { - console.log(`|DBG| server > Final config: ${JSON.stringify(configJson)}`); + logger.info(`Final config: ${JSON.stringify(configJson)}`); } - console.log( - `|DBG| server > Successfully removed or decrypted sensitive info` - ); + logger.info(`Successfully removed or decrypted sensitive info`); } catch (error: any) { - console.error(`|ERR| server > Failed to extract config: ${error.message}`); + logger.error(`Failed to extract config: ${error.message}`); res.json( errorResponse( `${error.message}, please check the logs or click this stream to create an issue on GitHub`, @@ -205,10 +205,10 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { return; } - console.log(`|DBG| server > Requesting streams for ${type} ${id}`); + logger.info(`Requesting streams for ${type} ${id}`); if (type !== 'movie' && type !== 'series') { - console.error(`|ERR| server > Invalid type for stream request`); + logger.error(`Invalid type for stream request`); res.json( errorResponse( 'Invalid type for stream request, must be movie or series', @@ -223,9 +223,7 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { try { const { valid, errorCode, errorMessage } = validateConfig(configJson); if (!valid) { - console.error( - `|ERR| server > Received invalid config: ${errorCode} - ${errorMessage}` - ); + logger.error(`Received invalid config: ${errorCode} - ${errorMessage}`); res.json( errorResponse(errorMessage ?? 'Unknown', rootUrl(req), '/configure') ); @@ -244,7 +242,7 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { res.json({ streams: streams }); }) .catch((error: any) => { - console.error(`|ERR| server > Internal addon error: ${error.message}`); + logger.error(`Internal addon error: ${error.message}`); res.json( errorResponse( 'An unexpected error occurred, please check the logs or create an issue on GitHub', @@ -255,7 +253,7 @@ app.get('/:config/stream/:type/:id.json', (req, res: Response): void => { ); }); } catch (error: any) { - console.error(`|ERR| server > Internal addon error: ${error.message}`); + logger.error(`Internal addon error: ${error.message}`); res.json( errorResponse( 'An unexpected error occurred, please check the logs or create an issue on GitHub', @@ -271,7 +269,7 @@ app.post('/encrypt-user-data', (req, res) => { const { data } = req.body; let finalString: string = ''; if (!data) { - console.error('|ERR| server > /encrypt-user-data: No data provided'); + logger.error('/encrypt-user-data: No data provided'); res.json({ success: false, message: 'No data provided' }); return; } @@ -280,16 +278,14 @@ app.post('/encrypt-user-data', (req, res) => { const config = JSON.parse(data); const { valid, errorCode, errorMessage } = validateConfig(config); if (!valid) { - console.error( - `|ERR| server > generateConfig: Invalid config: ${errorCode} - ${errorMessage}` + logger.error( + `generateConfig: Invalid config: ${errorCode} - ${errorMessage}` ); res.json({ success: false, message: errorMessage, error: errorMessage }); return; } } catch (error: any) { - console.error( - `|ERR| server > /encrypt-user-data: Invalid JSON: ${error.message}` - ); + logger.error(`/encrypt-user-data: Invalid JSON: ${error.message}`); res.json({ success: false, message: 'Malformed configuration' }); return; } @@ -306,28 +302,28 @@ app.post('/encrypt-user-data', (req, res) => { finalString = `E2-${encodeURIComponent(iv)}-${encodeURIComponent(data)}`; } - console.log( + logger.info( `|INF| server > /encrypt-user-data: Encrypted user data, compression report:` ); - console.log(`+--------------------------------------------+`); - console.log(`| Original: ${data.length} bytes`); - console.log(`| URL Encoded: ${encodeURIComponent(data).length} bytes`); - console.log(`| Minified: ${JSON.stringify(minified).length} bytes`); - console.log(`| Crushed: ${crushed.length} bytes`); - console.log(`| Compressed: ${compressed.length} bytes`); - console.log(`| Final String: ${finalString.length} bytes`); - console.log( + logger.info(`+--------------------------------------------+`); + logger.info(`| Original: ${data.length} bytes`); + logger.info(`| URL Encoded: ${encodeURIComponent(data).length} bytes`); + logger.info(`| Minified: ${JSON.stringify(minified).length} bytes`); + logger.info(`| Crushed: ${crushed.length} bytes`); + logger.info(`| Compressed: ${compressed.length} bytes`); + logger.info(`| Final String: ${finalString.length} bytes`); + logger.info( `| Ratio: ${((finalString.length / data.length) * 100).toFixed(2)}%` ); - console.log( + logger.info( `| Reduction: ${data.length - finalString.length} bytes (${(((data.length - finalString.length) / data.length) * 100).toFixed(2)}%)` ); - console.log(`+--------------------------------------------+`); + logger.info(`+--------------------------------------------+`); res.json({ success: true, data: finalString }); } catch (error: any) { - console.error(`|ERR| server > /encrypt-user-data: ${error.message}`); - console.error(error); + logger.error(`/encrypt-user-data: ${error.message}`); + logger.error(error); res.json({ success: false, message: error.message }); } }); @@ -351,7 +347,7 @@ app.use((req, res) => { }); app.listen(Settings.PORT, () => { - console.log(`|INF| server > init: Listening on port ${Settings.PORT}`); + logger.info(`Listening on port ${Settings.PORT}`); }); function extractJsonConfig(config: string): Config { @@ -374,8 +370,8 @@ function extractJsonConfig(config: string): Config { function extractCustomConfig(config: string): Config | undefined { const customConfig = CUSTOM_CONFIGS?.[config]; if (!customConfig) return undefined; - console.log( - `|DBG| server > Found custom config for alias ${config}, attempting to extract config` + logger.info( + `Found custom config for alias ${config}, attempting to extract config` ); return extractEncryptedOrEncodedConfig( decodeURIComponent(customConfig), @@ -391,7 +387,7 @@ function extractEncryptedOrEncodedConfig( try { if (config.startsWith('E-')) { // compressed and encrypted (hex) - console.log(`|DBG| server > Extracting encrypted (v1) config`); + logger.info(`Extracting encrypted (v1) config`); const parts = config.split('-'); if (parts.length !== 3) { throw new Error('Invalid encrypted config format'); @@ -401,7 +397,7 @@ function extractEncryptedOrEncodedConfig( decodedConfig = JSON.parse(decompressData(decryptData(data, iv))); } else if (config.startsWith('E2-')) { // minified, crushed, compressed and encrypted (base64) - console.log(`|DBG| server > Extracting encrypted (v2) config`); + logger.info(`Extracting encrypted (v2) config`); const parts = config.split('-'); if (parts.length !== 3) { throw new Error('Invalid encrypted config format'); @@ -414,9 +410,7 @@ function extractEncryptedOrEncodedConfig( decodedConfig = unminifyConfig(JSON.parse(minifiedConfig)); } else if (config.startsWith('B-')) { // minifed, crushed, compressed, base64 encoded - console.log( - `|DBG| server > Extracting base64 encoded and compressed config` - ); + logger.info(`Extracting base64 encoded and compressed config`); decodedConfig = unminifyConfig( JSON.parse( uncrushJson(decompressData(Buffer.from(config.slice(2), 'base64'))) @@ -424,15 +418,17 @@ function extractEncryptedOrEncodedConfig( ); } else { // plain base64 encoded - console.log(`|DBG| server > Extracting plain base64 encoded config`); + logger.info(`Extracting plain base64 encoded config`); decodedConfig = JSON.parse( Buffer.from(config, 'base64').toString('utf-8') ); } return decodedConfig; } catch (error: any) { - console.error(`|ERR| Failed to parse ${label}: ${error.message}`); - console.error(error); + logger.error(`Failed to parse ${label}: ${error.message}`, { + func: 'extractJsonConfig', + }); + logger.error(error, { func: 'extractJsonConfig' }); throw new Error(`Failed to parse ${label}`); } } @@ -556,7 +552,7 @@ function processObjectValues( Object.keys(obj).forEach((key) => { const value = obj[key]; if (condition(key, value)) { - console.log(`|DBG| Processing ${labelPrefix} ${key}`); + logger.debug(`Processing ${labelPrefix} ${key}`); obj[key] = decrypt ? decryptValue(value, `${labelPrefix} ${key}`) : encryptValue(value, `${labelPrefix} ${key}`); @@ -570,7 +566,8 @@ function encryptValue(value: any, label: string): any { const { iv, data } = encryptData(compressData(value)); return `E2-${iv}-${data}`; } catch (error: any) { - console.error(`Failed to encrypt ${label}`); + logger.error(`Failed to encrypt ${label}`, { func: 'encryptValue' }); + logger.error(error, { func: 'encryptValue' }); return ''; } } @@ -584,7 +581,10 @@ function decryptValue(value: any, label: string): any { if (decrypted === null) throw new Error('Decryption failed'); return decrypted; } catch (error: any) { - console.error(`|ERR| Failed to decrypt ${label}: ${error.message}`); + logger.error(`Failed to decrypt ${label}: ${error.message}`, { + func: 'decryptValue', + }); + logger.error(error, { func: 'decryptValue' }); throw new Error('Failed to decrypt config'); } } diff --git a/packages/cloudflare-worker/package.json b/packages/cloudflare-worker/package.json index 89770293..9ad06c78 100644 --- a/packages/cloudflare-worker/package.json +++ b/packages/cloudflare-worker/package.json @@ -17,4 +17,4 @@ "typescript": "^5.5.2", "wrangler": "^3.99.0" } -} +} \ No newline at end of file diff --git a/packages/frontend/next.config.ts b/packages/frontend/next.config.ts index 6515352b..9483e03b 100644 --- a/packages/frontend/next.config.ts +++ b/packages/frontend/next.config.ts @@ -25,6 +25,14 @@ const nextConfig: NextConfig = { env: { NEXT_PUBLIC_BRANDING: branding, }, + webpack(config) { + config.resolve.fallback = { + ...config.resolve.fallback, // if you miss it, all the other options in fallback, specified + // by next.js will be dropped. Doesn't make much sense, but how it is + fs: false, // the solution + }; + return config; + }, }; export default nextConfig; diff --git a/packages/utils/package.json b/packages/utils/package.json index 14249b35..5d03986a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -8,6 +8,7 @@ }, "description": "Library to parse a torrent filename ", "dependencies": { - "jsoncrush": "^1.1.8" + "jsoncrush": "^1.1.8", + "winston": "^3.17.0" } } diff --git a/packages/utils/src/crypto.ts b/packages/utils/src/crypto.ts index 783b5439..fd3f295a 100644 --- a/packages/utils/src/crypto.ts +++ b/packages/utils/src/crypto.ts @@ -7,6 +7,10 @@ import { import { deflateSync, inflateSync } from 'zlib'; import { Settings } from './settings'; import JSONCrush from 'jsoncrush'; +import { createLogger } from './logger'; + +const logger = createLogger('crypto'); + export const loadSecretKey = (): Buffer => { const secretKey = Settings.SECRET_KEY; if (!secretKey) { @@ -106,9 +110,7 @@ export function parseAndDecryptString(data: string): string | null { } return data; } catch (error: any) { - console.error( - `|ERR| crypto > parseAndDecryptString > Failed to decrypt data: ${error.message}` - ); + logger.error(`Failed to decrypt data: ${error.message}`); return null; } } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index a3624606..e9674c35 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -4,3 +4,4 @@ export * from './settings'; export * from './mediaflow'; export * from './cache'; export * from './general'; +export * from './logger'; diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts new file mode 100644 index 00000000..56d876df --- /dev/null +++ b/packages/utils/src/logger.ts @@ -0,0 +1,83 @@ +import winston from 'winston'; +import { Settings } from './settings'; + +// Map log levels to short labels and emojis +const levelMap: { [key: string]: string } = { + error: 'ERR', + warn: 'WRN', + info: 'INF', + debug: 'DBG', +}; + +const moduleMap: { [key: string]: string } = { + server: '🌐 SERVER', + wrappers: '📦 WRAPPERS', + crypto: '🔒 CRYPTO', + addon: '🧩 ADDON', + parser: '🔍 PARSER', + mediaflow: '🌊 MEDIAFLOW', +}; + +// Define colors for each log level +const levelColors: { [key: string]: string } = { + ERR: 'red', + WRN: 'yellow', + INF: 'cyan', + DBG: 'magenta', +}; + +const emojiLevelMap: { [key: string]: string } = { + error: '🛑', + warn: '⚠️', + info: '🟢', + debug: '🐞', +}; + +// Apply colors to Winston +winston.addColors(levelColors); + +const cfWorkerCheck = async () => { + try { + const response = await fetch('disable-winston-logs'); + if (response.status === 200) { + return true; + } + } catch (e) { + return false; + } +}; + +export const createLogger = (module: string) => { + // cloudflare workers do not play nice with winston + // so we disable winston logs in cloudflare workers + try { + __dirname; + } catch (e) { + return console; + } + const isJsonFormat = Settings.LOG_FORMAT === 'json'; + return winston.createLogger({ + level: Settings.LOG_LEVEL || 'info', // Default to 'info' if not set + format: isJsonFormat + ? winston.format.combine( + winston.format.timestamp(), + winston.format.json() // Use JSON format when LOG_FORMAT=json + ) + : winston.format.combine( + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss.SSS', + }), + winston.format.printf(({ timestamp, level, message, func }) => { + const emoji = emojiLevelMap[level] || ''; + const formattedModule = moduleMap[module] || module; + level = levelMap[level] || level.toUpperCase(); + // Apply color to the level + const coloredLevel = winston.format + .colorize() + .colorize(level, `${level}`); + return `${emoji} [${timestamp}] | ${coloredLevel} | ${formattedModule}${func ? ' (' + func + ')' : ''} | ${message}`; + }) + ), + transports: [new winston.transports.Console()], + }); +}; diff --git a/packages/utils/src/mediaflow.ts b/packages/utils/src/mediaflow.ts index cd6d0810..cacbb1b2 100644 --- a/packages/utils/src/mediaflow.ts +++ b/packages/utils/src/mediaflow.ts @@ -3,6 +3,9 @@ import path from 'path'; import { Settings } from './settings'; import { getTextHash } from './crypto'; import { Cache } from './cache'; +import { createLogger } from './logger'; + +const logger = createLogger('mediaflow'); const PRIVATE_CIDR = /^(10\.|127\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/; @@ -15,21 +18,15 @@ export function createProxiedMediaFlowUrl( } ) { if (!url) { - console.error( - '|ERR| mediaflow > createProxiedMediaFlowUrl > streamUrl is missing, could not create proxied URL' - ); + logger.error('streamUrl is missing, could not create proxied URL'); throw new Error('Stream URL is missing'); } if (!mediaFlowConfig) { - console.error( - '|ERR| mediaflow > createProxiedMediaFlowUrl > mediaFlowConfig is missing' - ); + logger.error('mediaFlowConfig is missing'); throw new Error('MediaFlow configuration is missing'); } if (!mediaFlowConfig?.proxyUrl || !mediaFlowConfig?.apiPassword) { - console.error( - '|ERR| mediaflow > createProxiedMediaFlowUrl > mediaFlowUrl or API password is missing' - ); + logger.error('mediaFlowUrl or API password is missing'); throw new Error('MediaFlow URL or API password is missing'); } @@ -69,16 +66,12 @@ export async function getMediaFlowPublicIp( ) { try { if (!mediaFlowConfig) { - console.error( - '|ERR| mediaflow > getMediaFlowPublicIp > mediaFlowConfig is missing' - ); + logger.error('mediaFlowConfig is missing'); throw new Error('MediaFlow configuration is missing'); } if (!mediaFlowConfig?.proxyUrl) { - console.error( - '|ERR| mediaflow > getMediaFlowPublicIp > mediaFlowUrl is missing' - ); + logger.error('mediaFlowUrl is missing'); throw new Error('MediaFlow URL is missing'); } @@ -89,8 +82,8 @@ export async function getMediaFlowPublicIp( const mediaFlowUrl = new URL(mediaFlowConfig.proxyUrl.replace(/\/$/, '')); if (PRIVATE_CIDR.test(mediaFlowUrl.hostname)) { // MediaFlow proxy URL is a private IP address - console.debug( - '|DBG| mediaflow > getMediaFlowPublicIp > MediaFlow proxy URL is a private IP address so returning null' + logger.debug( + 'MediaFlow proxy URL is a private IP address so returning null' ); return null; } @@ -100,9 +93,7 @@ export async function getMediaFlowPublicIp( ); const cachedPublicIp = cache ? cache.get(cacheKey) : null; if (cachedPublicIp) { - console.debug( - `|DBG| mediaflow > getMediaFlowPublicIp > Returning cached public IP` - ); + logger.debug(`Returning cached public IP`); return cachedPublicIp; } @@ -114,13 +105,9 @@ export async function getMediaFlowPublicIp( }).toString(); if (Settings.LOG_SENSITIVE_INFO) { - console.debug( - `|DBG| mediaflow > getMediaFlowPublicIp > GET ${proxyIpUrl.toString()}` - ); + logger.debug(`GET ${proxyIpUrl.toString()}`); } else { - console.debug( - '|DBG| mediaflow > getMediaFlowPublicIp > GET /proxy/ip?api_password=***' - ); + logger.debug('GET /proxy/ip?api_password=***'); } const response = await fetch(proxyIpUrl.toString(), { @@ -142,7 +129,7 @@ export async function getMediaFlowPublicIp( } return publicIp; } catch (error: any) { - console.error(`|ERR| mediaflow > getMediaFlowPublicIp > ${error.message}`); + logger.error(`${error.message}`); return null; } } diff --git a/packages/utils/src/settings.ts b/packages/utils/src/settings.ts index ca69f4f4..b469465d 100644 --- a/packages/utils/src/settings.ts +++ b/packages/utils/src/settings.ts @@ -4,7 +4,7 @@ import path from 'path'; try { dotenv.config({ path: path.resolve(__dirname, '../../../.env') }); } catch (error) { - console.error('Error loading .env file:', error); + console.error('Error loading .env file', error); } export class Settings { @@ -40,6 +40,12 @@ export class Settings { public static readonly DISABLE_TORRENTIO_MESSAGE = process.env.DISABLE_TORRENTIO_MESSAGE || 'The Torrentio addon has been disabled, please remove it to use this addon.'; + public static readonly LOG_LEVEL = process.env.LOG_LEVEL || 'info'; + public static readonly LOG_FORMAT = process.env.LOG_FORMAT + ? process.env.LOG_FORMAT === 'json' + ? 'json' + : 'text' + : 'text'; // Cache settings public static readonly CACHE_STREAM_RESULTS = process.env.CACHE_STREAM_RESULTS diff --git a/packages/wrappers/src/base.ts b/packages/wrappers/src/base.ts index c3cbe797..b00eb180 100644 --- a/packages/wrappers/src/base.ts +++ b/packages/wrappers/src/base.ts @@ -8,10 +8,17 @@ import { ParseResult, } from '@aiostreams/types'; import { parseFilename } from '@aiostreams/parser'; -import { getTextHash, serviceDetails, Settings } from '@aiostreams/utils'; -import { fetch as uFetch, ProxyAgent } from 'undici'; +import { + getTextHash, + serviceDetails, + Settings, + createLogger, +} from '@aiostreams/utils'; +// import { fetch as uFetch, ProxyAgent } from 'undici'; import { emojiToLanguage, codeToLanguage } from '@aiostreams/formatters'; +const logger = createLogger('wrappers'); + export class BaseWrapper { private readonly streamPath: string = 'stream/{type}/{id}.json'; private indexerTimeout: number; @@ -78,8 +85,10 @@ export class BaseWrapper { let hostname: string; try { hostname = new URL(url).hostname; - } catch (e) { - console.error(`|ERR| utils > shouldProxyRequest Error parsing URL`); + } catch (e: any) { + logger.error(`Error parsing URL: ${this.getLoggableUrl(url)}`, { + func: 'shouldProxyRequest', + }); return false; } if (!Settings.ADDON_PROXY) { @@ -90,8 +99,11 @@ export class BaseWrapper { for (const rule of Settings.ADDON_PROXY_CONFIG.split(',')) { const [ruleHost, enabled] = rule.split(':'); if (['true', 'false'].includes(enabled) === false) { - console.error( - `|ERR| utils > shouldProxyRequest > Invalid rule: ${rule}` + logger.error( + `Invalid rule: ${rule}. Rule must be in the format host:enabled`, + { + func: 'shouldProxyRequest', + } ); continue; } @@ -111,7 +123,7 @@ export class BaseWrapper { return useProxy; } - private getLoggableUrl(url: string): string { + protected getLoggableUrl(url: string): string { let urlObj = new URL(url); return `${urlObj.protocol}//${urlObj.hostname}/${urlObj.pathname .split('/') @@ -132,8 +144,8 @@ export class BaseWrapper { let sanitisedUrl = this.getLoggableUrl(url); let useProxy = this.shouldProxyRequest(url); - console.log( - `|DBG| wrappers > base > ${this.addonName}: Making a ${useProxy ? 'proxied' : 'direct'} request to ${sanitisedUrl} with user IP ${ + logger.info( + `Making a ${useProxy ? 'proxied' : 'direct'} request to ${this.addonName} (${sanitisedUrl}) with user IP ${ userIp ? Settings.LOG_SENSITIVE_INFO ? userIp @@ -143,8 +155,8 @@ export class BaseWrapper { ); let response = useProxy - ? uFetch(url, { - dispatcher: new ProxyAgent(Settings.ADDON_PROXY), + ? fetch(url, { + // dispatcher: new ProxyAgent(Settings.ADDON_PROXY), method: 'GET', headers: headers, signal: AbortSignal.timeout(this.indexerTimeout), @@ -163,8 +175,8 @@ export class BaseWrapper { const requestCacheKey = getTextHash(url); const cachedStreams = cache ? cache.get(requestCacheKey) : undefined; if (cachedStreams) { - console.debug( - `|DBG| wrappers > base > ${this.addonName}: Returning cached streams for ${this.getLoggableUrl(url)}` + logger.info( + `Returning cached streams for ${this.addonName} (${this.getLoggableUrl(url)})` ); return cachedStreams; } @@ -195,10 +207,10 @@ export class BaseWrapper { let message = error.message; if (error.name === 'TimeoutError') { message = `The stream request to ${this.addonName} timed out after ${this.indexerTimeout}ms`; - return Promise.reject(new Error(message)); + return Promise.reject(message); } - console.error(`|ERR| wrappers > base > ${this.addonName}: ${message}`); - console.error(error); + logger.error(`Error during fetch for ${this.addonName}: ${message}`); + logger.error(error); return Promise.reject(error.message); } } @@ -274,8 +286,8 @@ export class BaseWrapper { errorRegex.test(stream.title || '') || errorRegex.test(stream.description || '') ) { - console.log( - `|ERR| wrappers > base > ${this.addonName}: ${stream.title || stream.description} was detected as an error` + logger.debug( + `Result from ${this.addonName} (${(stream.title || stream.description).split('\n').join(' ')}) was detected as an error` ); return { type: 'error', diff --git a/packages/wrappers/src/comet.ts b/packages/wrappers/src/comet.ts index 8cb94b90..af63f849 100644 --- a/packages/wrappers/src/comet.ts +++ b/packages/wrappers/src/comet.ts @@ -1,9 +1,11 @@ import { AddonDetail, ParseResult, StreamRequest } from '@aiostreams/types'; import { ParsedStream, Stream, Config } from '@aiostreams/types'; import { BaseWrapper } from './base'; -import { addonDetails } from '@aiostreams/utils'; +import { addonDetails, createLogger } from '@aiostreams/utils'; import { Settings } from '@aiostreams/utils'; +const logger = createLogger('wrappers'); + export class Comet extends BaseWrapper { constructor( configString: string | null, @@ -157,9 +159,7 @@ export async function getCometStreams( } const errorMessages: string[] = []; const streamPromises = servicesToUse.map(async (service) => { - console.log( - `|INF| wrappers > comet: Getting Comet streams for service: ${service.id}` - ); + logger.info(`Getting Comet streams for ${service.id}`, { func: 'comet' }); const cometConfig = getCometConfig(service.id, service.credentials.apiKey); const configString = Buffer.from(JSON.stringify(cometConfig)).toString( 'base64' diff --git a/packages/wrappers/src/debridio.ts b/packages/wrappers/src/debridio.ts index 30486ac5..404f8227 100644 --- a/packages/wrappers/src/debridio.ts +++ b/packages/wrappers/src/debridio.ts @@ -1,9 +1,11 @@ import { AddonDetail, ParseResult, StreamRequest } from '@aiostreams/types'; import { ParsedStream, Stream, Config } from '@aiostreams/types'; import { BaseWrapper } from './base'; -import { addonDetails } from '@aiostreams/utils'; +import { addonDetails, createLogger } from '@aiostreams/utils'; import { Settings } from '@aiostreams/utils'; +const logger = createLogger('wrappers'); + export class Debridio extends BaseWrapper { constructor( configString: string | null, @@ -136,9 +138,9 @@ export async function getDebridioStreams( const addonErrors: string[] = []; const streamPromises = servicesToUse.map(async (service) => { - console.log( - `|INF| wrappers > debridio: Getting Debridio streams for ${service.name}` - ); + logger.info(`Getting Debridio streams for ${service.name}`, { + func: 'debridio', + }); const debridioConfigString = getDebridioConfigString( service.id, service.credentials.apiKey diff --git a/packages/wrappers/src/jackettio.ts b/packages/wrappers/src/jackettio.ts index 889ce135..2aea880f 100644 --- a/packages/wrappers/src/jackettio.ts +++ b/packages/wrappers/src/jackettio.ts @@ -1,9 +1,11 @@ import { AddonDetail, StreamRequest } from '@aiostreams/types'; import { ParsedStream, Config } from '@aiostreams/types'; import { BaseWrapper } from './base'; -import { addonDetails } from '@aiostreams/utils'; +import { addonDetails, createLogger } from '@aiostreams/utils'; import { Settings } from '@aiostreams/utils'; +const logger = createLogger('wrappers'); + // name, title, url export class Jackettio extends BaseWrapper { constructor( @@ -156,9 +158,9 @@ export async function getJackettioStreams( } const streamPromises = servicesToUse.map(async (service) => { - console.log( - `|INF| wrappers > jackettio: Getting Jackettio streams for ${service.name}` - ); + logger.info(`Getting Jackettio streams for ${service.name}`, { + func: 'jackettio', + }); const configString = getJackettioConfigString( service.id, service.credentials.apiKey diff --git a/packages/wrappers/src/mediafusion.ts b/packages/wrappers/src/mediafusion.ts index 2f7964b8..7c5b6bdd 100644 --- a/packages/wrappers/src/mediafusion.ts +++ b/packages/wrappers/src/mediafusion.ts @@ -6,8 +6,11 @@ import { getTextHash, getTimeTakenSincePoint, Settings, + createLogger, } from '@aiostreams/utils'; +const logger = createLogger('wrappers'); + export class MediaFusion extends BaseWrapper { constructor( configString: string | null, @@ -74,16 +77,19 @@ export async function getMediafusionStreams( const cacheKey = getTextHash(`mediafusionConfig:${JSON.stringify(data)}`); const cachedConfig = cache ? cache.get(cacheKey) : null; if (cachedConfig) { - console.log( - `|DBG| wrappers > mediafusion: Returning cached config string` - ); + logger.info(`Returning cached config string`, { + func: 'mediafusion.getConfigString', + }); return cachedConfig; } try { const encryptedStr = await _getConfigString(data); - console.log( - `|INF| wrappers > mediafusion: Config encryption took ${getTimeTakenSincePoint(startTime)}` + logger.info( + `Config encryption took ${getTimeTakenSincePoint(startTime)}`, + { + func: 'mediafusion.getConfigString', + } ); cache?.set(cacheKey, encryptedStr, Settings.CACHE_MEDIAFUSION_CONFIG_TTL); return encryptedStr; @@ -198,9 +204,9 @@ export async function getMediafusionStreams( service.id, service.credentials ); - console.log( - `|INF| wrappers > mediafusion: Getting MediaFusion streams for service: ${service.id}` - ); + logger.info(`Getting MediaFusion streams for ${service.id}`, { + func: 'mediafusion', + }); let encryptedStr: string = ''; try { encryptedStr = await getConfigString(mediafusionConfig); @@ -210,7 +216,7 @@ export async function getMediafusionStreams( ); } } catch (error: any) { - console.error(`|ERR| wrappers > mediafusion: ${error.message}`); + logger.error(`${error.message}`, { func: 'mediafusion' }); throw new Error(error.message); } const mediafusion = new MediaFusion( diff --git a/packages/wrappers/src/orion.ts b/packages/wrappers/src/orion.ts index 071de56e..981eb259 100644 --- a/packages/wrappers/src/orion.ts +++ b/packages/wrappers/src/orion.ts @@ -6,9 +6,11 @@ import { } from '@aiostreams/types'; import { ParsedStream, Stream, Config } from '@aiostreams/types'; import { BaseWrapper } from './base'; -import { addonDetails } from '@aiostreams/utils'; +import { addonDetails, createLogger } from '@aiostreams/utils'; import { Settings } from '@aiostreams/utils'; +const logger = createLogger('wrappers'); + // name, title, url export class OrionStremioAddon extends BaseWrapper { constructor( @@ -142,9 +144,7 @@ export async function getOrionStreams( // otherwise, pass all the services to orion const debridServices = usableServices.map((service) => service.id); - console.log( - `|DBG| wrappers > orion > using debrid services: ${debridServices}` - ); + logger.info(`Using Orion with debrid services: ${debridServices}`); const configString = getOrionConfigString( orionApiKey, orionOptions.linkLimit, diff --git a/packages/wrappers/src/peerflix.ts b/packages/wrappers/src/peerflix.ts index b2a37239..a7703a06 100644 --- a/packages/wrappers/src/peerflix.ts +++ b/packages/wrappers/src/peerflix.ts @@ -1,9 +1,11 @@ import { AddonDetail, StreamRequest } from '@aiostreams/types'; import { ParsedStream, Config } from '@aiostreams/types'; import { BaseWrapper } from './base'; -import { addonDetails } from '@aiostreams/utils'; +import { addonDetails, createLogger } from '@aiostreams/utils'; import { Settings } from '@aiostreams/utils'; +const logger = createLogger('wrappers'); + export class Peerflix extends BaseWrapper { constructor( configString: string | null, @@ -46,7 +48,6 @@ export async function getPeerflixStreams( const indexerTimeout = peerflixOptions.indexerTimeout ? parseInt(peerflixOptions.indexerTimeout) : undefined; - console.log(JSON.stringify(peerflixOptions)); // If overrideUrl is provided, use it to get streams and skip all other steps if (peerflixOptions.overrideUrl) { const peerflix = new Peerflix( @@ -64,8 +65,9 @@ export async function getPeerflixStreams( const usableServices = config.services.filter( (service) => supportedServices.includes(service.id) && service.enabled ); - console.log( - `|DBG| wrappers > peerflix: Found ${usableServices.length} usable services: ${usableServices.map((service) => service.id).join(', ')}` + logger.debug( + `Found ${usableServices.length} usable services: ${usableServices.map((service) => service.id).join(', ')}`, + { func: 'peerflix' } ); // if no usable services found, use peerflix without any configuration @@ -95,9 +97,9 @@ export async function getPeerflixStreams( if (peerflixOptions.useMultipleInstances === 'true') { let retrievedP2PStreams = false; const promises = usableServices.map(async (service) => { - console.log( - `|DBG| wrappers > peerflix: Getting Peerflix streams for ${service.name}` - ); + logger.info(`Getting Peerflix streams for ${service.name}`, { + func: 'peerflix', + }); let configPairs = [getServicePair(service.id, service.credentials)]; if (peerflixOptions.showP2PStreams === 'true' && !retrievedP2PStreams) { configPairs.push(['debridoptions', 'torrentlinks']); diff --git a/packages/wrappers/src/stremio-jackett.ts b/packages/wrappers/src/stremio-jackett.ts index 1419477e..9618c986 100644 --- a/packages/wrappers/src/stremio-jackett.ts +++ b/packages/wrappers/src/stremio-jackett.ts @@ -6,9 +6,11 @@ import { } from '@aiostreams/types'; import { ParsedStream, Config } from '@aiostreams/types'; import { BaseWrapper } from './base'; -import { addonDetails } from '@aiostreams/utils'; +import { addonDetails, createLogger } from '@aiostreams/utils'; import { Settings } from '@aiostreams/utils'; +const logger = createLogger('wrappers'); + // name, title, url export class StremioJackett extends BaseWrapper { constructor( @@ -61,8 +63,10 @@ export class StremioJackett extends BaseWrapper { url.pathname = components.join('/'); } } - console.error(` - |ERR| wrappers > stremioJackett: Error parsing stream config for ${url}`); + logger.error( + `Error parsing stream config for playback URL: ${this.getLoggableUrl(url.toString())}`, + { func: 'stremio-jackett' } + ); } } return parseResult; @@ -201,9 +205,9 @@ export async function getStremioJackettStreams( } const streamPromises = servicesToUse.map(async (service) => { - console.log( - `|INF| wrappers > stremioJackett: Getting StremioJackett streams for ${service.name}` - ); + logger.info(`Getting Stremio-Jackett streams for ${service.name}`, { + func: 'stremio-jackett', + }); const configString = getStremioJackettConfigString( service.id, service.credentials.apiKey, diff --git a/packages/wrappers/src/torbox.ts b/packages/wrappers/src/torbox.ts index dce3aa6a..ed4eb392 100644 --- a/packages/wrappers/src/torbox.ts +++ b/packages/wrappers/src/torbox.ts @@ -8,7 +8,9 @@ import { StreamRequest, } from '@aiostreams/types'; import { parseFilename } from '@aiostreams/parser'; -import { Settings } from '@aiostreams/utils'; +import { createLogger, Settings } from '@aiostreams/utils'; + +const logger = createLogger('wrappers'); interface TorboxStream extends Stream { name: string; @@ -49,9 +51,9 @@ export class Torbox extends BaseWrapper { let type = stream.type; let personal = false; if (stream.name.includes('Your Media')) { - console.log( - `|INF| wrappers > torbox > detected personal stream in ${stream.name}` - ); + logger.debug(`${stream.name} was detected as a personal stream.`, { + func: 'torbox', + }); personal = true; } const [dQuality, dFilename, dSize, dLanguage, dAgeOrSeeders] = diff --git a/packages/wrappers/src/torrentio.ts b/packages/wrappers/src/torrentio.ts index 7a4d8f6c..a8e2f900 100644 --- a/packages/wrappers/src/torrentio.ts +++ b/packages/wrappers/src/torrentio.ts @@ -1,9 +1,11 @@ import { AddonDetail, StreamRequest } from '@aiostreams/types'; import { ParsedStream, Config } from '@aiostreams/types'; import { BaseWrapper } from './base'; -import { addonDetails } from '@aiostreams/utils'; +import { addonDetails, createLogger } from '@aiostreams/utils'; import { Settings } from '@aiostreams/utils'; +const logger = createLogger('wrappers'); + export class Torrentio extends BaseWrapper { constructor( configString: string | null, @@ -90,9 +92,9 @@ export async function getTorrentioStreams( if (torrentioOptions.useMultipleInstances === 'true') { const promises = usableServices.map(async (service) => { - console.log( - `|INF| wrappers > torrentio: Getting Torrentio streams for ${service.name}` - ); + logger.info(`Getting Torrentio streams for ${service.name}`, { + func: 'torrentio', + }); let configString = getServicePair(service.id, service.credentials); const torrentio = new Torrentio( configString, From 1855b3bf94e342f566ef813e16e5f213e0ccadb9 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Fri, 21 Feb 2025 18:08:30 +0000 Subject: [PATCH 03/10] feat: add SDR visual tag closes #88 --- packages/frontend/src/app/configure/page.tsx | 1 + packages/parser/src/regex.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/frontend/src/app/configure/page.tsx b/packages/frontend/src/app/configure/page.tsx index a510037d..262bbd77 100644 --- a/packages/frontend/src/app/configure/page.tsx +++ b/packages/frontend/src/app/configure/page.tsx @@ -69,6 +69,7 @@ const defaultVisualTags: VisualTag[] = [ { '3D': true }, { IMAX: true }, { AI: true }, + { SDR: true }, ]; const defaultAudioTags: AudioTag[] = [ diff --git a/packages/parser/src/regex.ts b/packages/parser/src/regex.ts index c22bfebf..214d6baf 100644 --- a/packages/parser/src/regex.ts +++ b/packages/parser/src/regex.ts @@ -45,6 +45,7 @@ export const PARSE_REGEX = { '3D': createRegex('(bd)?(3|three)[ .\\-_]?(d(imension)?(al)?)'), IMAX: createRegex('imax'), AI: createRegex('ai[ .\\-_]?(upscale|enhanced|remaster)?'), + SDR: createRegex('sdr'), }, audioTags: { Atmos: createRegex('atmos'), From 81392645192cf0ae718af7c37a2c4f8a471868f5 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Fri, 21 Feb 2025 18:10:23 +0000 Subject: [PATCH 04/10] fix(frontend): load override name from previous config --- packages/frontend/src/app/configure/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/app/configure/page.tsx b/packages/frontend/src/app/configure/page.tsx index 262bbd77..349288ba 100644 --- a/packages/frontend/src/app/configure/page.tsx +++ b/packages/frontend/src/app/configure/page.tsx @@ -488,6 +488,7 @@ export default function Configure() { function loadFromConfig(decodedConfig: Config) { console.log('Loaded config', decodedConfig); + setOverrideName(decodedConfig.overrideName || ''); setStreamTypes( loadValidValuesFromObject(decodedConfig.streamTypes, defaultStreamTypes) ); From 62ba07368079075263859b27435fa58b36cf74ee Mon Sep 17 00:00:00 2001 From: Viren070 Date: Fri, 21 Feb 2025 18:54:35 +0000 Subject: [PATCH 05/10] fix(logger): enhance message formatting to handle multi-line strings --- packages/utils/src/logger.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index 56d876df..1ff549ed 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -75,6 +75,16 @@ export const createLogger = (module: string) => { const coloredLevel = winston.format .colorize() .colorize(level, `${level}`); + if (typeof message === 'string') { + return message + .split('\n') + .map((line) => { + return `${emoji} [${timestamp}] | ${coloredLevel} | ${formattedModule}${ + func ? ' (' + func + ')' : '' + } | ${line}`; + }) + .join('\n'); + } return `${emoji} [${timestamp}] | ${coloredLevel} | ${formattedModule}${func ? ' (' + func + ')' : ''} | ${message}`; }) ), From 50313a5261ef2be19194a5fd4cbda169a2e44fb6 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Tue, 25 Feb 2025 14:52:14 +0000 Subject: [PATCH 06/10] fix: remove unused code --- packages/utils/src/logger.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index 1ff549ed..b52ba134 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -36,17 +36,6 @@ const emojiLevelMap: { [key: string]: string } = { // Apply colors to Winston winston.addColors(levelColors); -const cfWorkerCheck = async () => { - try { - const response = await fetch('disable-winston-logs'); - if (response.status === 200) { - return true; - } - } catch (e) { - return false; - } -}; - export const createLogger = (module: string) => { // cloudflare workers do not play nice with winston // so we disable winston logs in cloudflare workers From 2754db6d1c5b9831fd2c9bcd5d881d8911117caf Mon Sep 17 00:00:00 2001 From: Viren070 Date: Tue, 25 Feb 2025 14:52:38 +0000 Subject: [PATCH 07/10] feat(logger): adjust emojis --- packages/utils/src/logger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index b52ba134..ccc64142 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -28,8 +28,8 @@ const levelColors: { [key: string]: string } = { const emojiLevelMap: { [key: string]: string } = { error: '🛑', - warn: '⚠️', - info: '🟢', + warn: '⚠️ ', + info: '🔵', debug: '🐞', }; From 321940276dffc811673da31b798069b5970c6976 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Tue, 25 Feb 2025 14:53:28 +0000 Subject: [PATCH 08/10] refactor(logger): use common function to format logging --- packages/utils/src/logger.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index ccc64142..28a2a93e 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -64,17 +64,15 @@ export const createLogger = (module: string) => { const coloredLevel = winston.format .colorize() .colorize(level, `${level}`); + const formatLine = (line: unknown) => { + return `${emoji} [${timestamp}] | ${coloredLevel} | ${formattedModule}${ + func ? ' (' + func + ')' : '' + } > ${line}`; + }; if (typeof message === 'string') { - return message - .split('\n') - .map((line) => { - return `${emoji} [${timestamp}] | ${coloredLevel} | ${formattedModule}${ - func ? ' (' + func + ')' : '' - } | ${line}`; - }) - .join('\n'); + return message.split('\n').map(formatLine).join('\n'); } - return `${emoji} [${timestamp}] | ${coloredLevel} | ${formattedModule}${func ? ' (' + func + ')' : ''} | ${message}`; + return formatLine(message); }) ), transports: [new winston.transports.Console()], From 495b043434d608694221232ead37aa2e2acdcabd Mon Sep 17 00:00:00 2001 From: Viren070 Date: Tue, 25 Feb 2025 14:53:57 +0000 Subject: [PATCH 09/10] feat(server): improve error message for invalid SECRET_KEY with command suggestion --- packages/addon/src/server.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/addon/src/server.ts b/packages/addon/src/server.ts index 7d65c581..c4a6e75e 100644 --- a/packages/addon/src/server.ts +++ b/packages/addon/src/server.ts @@ -63,7 +63,14 @@ if (Settings.CUSTOM_CONFIGS) { try { if (Settings.SECRET_KEY) loadSecretKey(); } catch (error: any) { - logger.error(`Invalid SECRET_KEY - ${error.message}`); + // determine command to run based on system OS + const command = + process.platform === 'win32' + ? '[System.Guid]::NewGuid().ToString("N").Substring(0, 32) + [System.Guid]::NewGuid().ToString("N").Substring(0, 32)' + : 'openssl rand -hex 32'; + logger.error( + `The secret key is invalid. You will not be able to generate configurations. You can generate a new secret key by running the following command\n${command}` + ); } const cache = new Cache(Settings.MAX_CACHE_SIZE); From 982b170997c88b5531ff959a8d2d82292a561bf8 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Tue, 25 Feb 2025 14:54:13 +0000 Subject: [PATCH 10/10] fix: adjust logging --- packages/addon/src/server.ts | 2 +- packages/utils/src/crypto.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/addon/src/server.ts b/packages/addon/src/server.ts index c4a6e75e..1b73715f 100644 --- a/packages/addon/src/server.ts +++ b/packages/addon/src/server.ts @@ -92,7 +92,7 @@ app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { logger.info( - `${req.method} ${req.path.replace(/\/eyJ[\w\=]+/g, '/*******').replace(/\/(E2?|B)?-[\w-\%]+/g, '/*******')} - ${res.statusCode} - ${getTimeTakenSincePoint(start)}` + `${req.method} ${req.path.replace(/\/ey[JI][\w\=]+/g, '/*******').replace(/\/(E2?|B)?-[\w-\%]+/g, '/*******')} - ${res.statusCode} - ${getTimeTakenSincePoint(start)}` ); }); next(); diff --git a/packages/utils/src/crypto.ts b/packages/utils/src/crypto.ts index fd3f295a..d0cb26e4 100644 --- a/packages/utils/src/crypto.ts +++ b/packages/utils/src/crypto.ts @@ -11,17 +11,27 @@ import { createLogger } from './logger'; const logger = createLogger('crypto'); -export const loadSecretKey = (): Buffer => { +export const loadSecretKey = (): Buffer | string => { const secretKey = Settings.SECRET_KEY; if (!secretKey) { + console.error('No secret key provided'); throw new Error('No secret key provided'); } // must be 64 characters long and hex - if (secretKey.length !== 64) { + + if (secretKey.length === 32) { + // backwards compatibility + logger.warn( + 'Secret key is 32 characters long, consider updating to a 64 character key and reconfiguring for better security' + ); + return secretKey; + } else if (secretKey.length !== 64) { + logger.error('Secret key must be 64 characters long'); throw new Error('Secret key must be 64 characters long'); } if (!/^[0-9a-fA-F]+$/.test(secretKey)) { + logger.error('Secret key must be a hex string (0-9, a-f)'); throw new Error('Secret key must be a hex string (0-9, a-f)'); }