From 9aba1c13b13a2d137900754a7ea0d3c62e1bb807 Mon Sep 17 00:00:00 2001 From: iPromKnight Date: Wed, 7 Feb 2024 19:58:29 +0000 Subject: [PATCH] And thats what happens when you do a crapload of work with *.ts in .eslintignore... :/ --- deployment/docker/.env.example | 4 +- src/node/consumer/.eslintignore | 2 +- src/node/consumer/.eslintrc | 30 +- src/node/consumer/.swcrc | 15 + src/node/consumer/Dockerfile | 7 +- src/node/consumer/esbuild.js | 54 - src/node/consumer/package-lock.json | 1391 ++++++++++++++++- src/node/consumer/package.json | 16 +- .../src/lib/helpers/extension_helpers.ts | 8 +- .../src/lib/helpers/promises_helpers.ts | 18 +- .../src/lib/interfaces/cache_service.ts | 4 +- .../src/lib/interfaces/logging_service.ts | 7 +- .../interfaces/parse_torrent_title_result.ts | 5 + .../src/lib/interfaces/parsed_torrent.ts | 4 +- .../interfaces/process_torrents_job.ts | 0 .../src/lib/interfaces/season_episode_map.ts | 7 + .../lib/interfaces/torrent_entries_service.ts | 14 +- .../lib/interfaces/torrent_file_collection.ts | 6 +- .../interfaces/torrent_processing_service.ts | 2 +- .../{ => lib}/jobs/process_torrents_job.ts | 35 +- .../src/lib/models/composition_root.ts | 8 +- .../lib/models/configuration/cache_config.ts | 2 +- .../models/configuration/database_config.ts | 2 +- .../models/configuration/torrent_config.ts | 2 +- .../src/lib/models/inversify_config.ts | 21 +- .../repository/database_repository.ts | 68 +- .../interfaces/content_attributes.ts | 0 .../interfaces/database_repository.ts | 24 +- .../repository/interfaces/file_attributes.ts | 4 +- .../interfaces/ingested_page_attributes.ts | 0 .../interfaces/ingested_torrent_attributes.ts | 0 .../interfaces/provider_attributes.ts | 0 .../interfaces/skip_torrent_attributes.ts | 0 .../interfaces/subtitle_attributes.ts | 2 +- .../interfaces/torrent_attributes.ts | 4 +- .../{ => lib}/repository/models/content.ts | 2 +- .../src/{ => lib}/repository/models/file.ts | 4 +- .../repository/models/ingestedPage.ts | 0 .../repository/models/ingestedTorrent.ts | 0 .../{ => lib}/repository/models/provider.ts | 0 .../repository/models/skipTorrent.ts | 0 .../{ => lib}/repository/models/subtitle.ts | 4 +- .../{ => lib}/repository/models/torrent.ts | 0 .../src/lib/services/cache_service.ts | 35 +- .../src/lib/services/configuration_service.ts | 6 +- .../src/lib/services/logging_service.ts | 6 +- .../src/lib/services/metadata_service.ts | 88 +- .../lib/services/torrent_download_service.ts | 98 +- .../lib/services/torrent_entries_service.ts | 119 +- .../src/lib/services/torrent_file_service.ts | 329 ++-- .../services/torrent_processing_service.ts | 12 +- .../lib/services/torrent_subtitle_service.ts | 51 +- .../src/lib/services/tracker_service.ts | 6 +- src/node/consumer/src/main.ts | 5 +- src/node/consumer/tsconfig.json | 32 +- 55 files changed, 1987 insertions(+), 576 deletions(-) create mode 100644 src/node/consumer/.swcrc delete mode 100644 src/node/consumer/esbuild.js rename src/node/consumer/src/{ => lib}/interfaces/process_torrents_job.ts (100%) create mode 100644 src/node/consumer/src/lib/interfaces/season_episode_map.ts rename src/node/consumer/src/{ => lib}/jobs/process_torrents_job.ts (72%) rename src/node/consumer/src/{ => lib}/repository/database_repository.ts (84%) rename src/node/consumer/src/{ => lib}/repository/interfaces/content_attributes.ts (100%) rename src/node/consumer/src/{ => lib}/repository/interfaces/database_repository.ts (81%) rename src/node/consumer/src/{ => lib}/repository/interfaces/file_attributes.ts (84%) rename src/node/consumer/src/{ => lib}/repository/interfaces/ingested_page_attributes.ts (100%) rename src/node/consumer/src/{ => lib}/repository/interfaces/ingested_torrent_attributes.ts (100%) rename src/node/consumer/src/{ => lib}/repository/interfaces/provider_attributes.ts (100%) rename src/node/consumer/src/{ => lib}/repository/interfaces/skip_torrent_attributes.ts (100%) rename src/node/consumer/src/{ => lib}/repository/interfaces/subtitle_attributes.ts (90%) rename src/node/consumer/src/{ => lib}/repository/interfaces/torrent_attributes.ts (96%) rename src/node/consumer/src/{ => lib}/repository/models/content.ts (97%) rename src/node/consumer/src/{ => lib}/repository/models/file.ts (98%) rename src/node/consumer/src/{ => lib}/repository/models/ingestedPage.ts (100%) rename src/node/consumer/src/{ => lib}/repository/models/ingestedTorrent.ts (100%) rename src/node/consumer/src/{ => lib}/repository/models/provider.ts (100%) rename src/node/consumer/src/{ => lib}/repository/models/skipTorrent.ts (100%) rename src/node/consumer/src/{ => lib}/repository/models/subtitle.ts (96%) rename src/node/consumer/src/{ => lib}/repository/models/torrent.ts (100%) diff --git a/deployment/docker/.env.example b/deployment/docker/.env.example index 32953d2..1d84fe9 100644 --- a/deployment/docker/.env.example +++ b/deployment/docker/.env.example @@ -23,7 +23,9 @@ RABBIT_URI=amqp://guest:guest@rabbitmq:5672/?heartbeat=30 QUEUE_NAME=ingested JOB_CONCURRENCY=5 JOBS_ENABLED=true -MAX_SINGLE_TORRENT_CONNECTIONS=10 +LOG_LEVEL=info # can be debug for extra verbosity (a lot more verbosity - useful for development) +MAX_CONNECTIONS_PER_TORRENT=10 +MAX_CONNECTIONS_OVERALL=100 TORRENT_TIMEOUT=30000 UDP_TRACKERS_ENABLED=true CONSUMER_REPLICAS=3 diff --git a/src/node/consumer/.eslintignore b/src/node/consumer/.eslintignore index b0a155e..7773828 100644 --- a/src/node/consumer/.eslintignore +++ b/src/node/consumer/.eslintignore @@ -1 +1 @@ -*.ts \ No newline at end of file +dist/ \ No newline at end of file diff --git a/src/node/consumer/.eslintrc b/src/node/consumer/.eslintrc index e0c8130..fbf723b 100644 --- a/src/node/consumer/.eslintrc +++ b/src/node/consumer/.eslintrc @@ -59,27 +59,13 @@ } ], "prefer-destructuring": "error", - "@typescript-eslint/consistent-type-assertions": "off", - "@typescript-eslint/explicit-function-return-type": "off" - }, - "overrides": [ - { - "files": [ - "*.ts", - "*.mts", - "*.cts", - "*.tsx" - ], - "rules": { - "@typescript-eslint/explicit-function-return-type": "error", - "@typescript-eslint/consistent-type-assertions": [ - "error", - { - "assertionStyle": "as", - "objectLiteralTypeAssertions": "never" - } - ] + "@typescript-eslint/explicit-function-return-type": "error", + "@typescript-eslint/consistent-type-assertions": [ + "error", + { + "assertionStyle": "as", + "objectLiteralTypeAssertions": "never" } - } - ] + ] + } } \ No newline at end of file diff --git a/src/node/consumer/.swcrc b/src/node/consumer/.swcrc new file mode 100644 index 0000000..eff14b3 --- /dev/null +++ b/src/node/consumer/.swcrc @@ -0,0 +1,15 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": false, + "decorators": true, + "dynamicImport": true + }, + "target": "es2020", + "baseUrl": "." + }, + "module": { + "type": "commonjs" + } +} \ No newline at end of file diff --git a/src/node/consumer/Dockerfile b/src/node/consumer/Dockerfile index c26b653..8914638 100644 --- a/src/node/consumer/Dockerfile +++ b/src/node/consumer/Dockerfile @@ -1,8 +1,11 @@ FROM node:lts-buster-slim as builder RUN apt-get update && \ - apt-get install -y git && \ - rm -rf /var/lib/apt/lists/* + apt-get upgrade -y && \ + apt-get install -y python3 make g++ && \ + rm -rf /var/lib/apt/lists/* + +RUN npm install -g npm@^8 && npm config set python /usr/bin/python3 WORKDIR /app diff --git a/src/node/consumer/esbuild.js b/src/node/consumer/esbuild.js deleted file mode 100644 index 17393df..0000000 --- a/src/node/consumer/esbuild.js +++ /dev/null @@ -1,54 +0,0 @@ -import {build} from "esbuild"; -import {readFileSync, rmSync} from "fs"; - -const {devDependencies} = JSON.parse(readFileSync("./package.json", "utf8")); - -const start = Date.now(); - -try { - const outdir = "dist"; - - rmSync(outdir, {recursive: true, force: true}); - - build({ - bundle: true, - entryPoints: [ - "./src/main.ts", - ], - external: [...(devDependencies && Object.keys(devDependencies))], - keepNames: true, - minify: true, - outbase: "./src", - outdir, - outExtension: { - ".js": ".cjs", - }, - platform: "node", - plugins: [ - { - name: "populate-import-meta", - setup: ({onLoad}) => { - onLoad({filter: new RegExp(`${import.meta.dirname}/src/.*.(js|ts)$`)}, args => { - const contents = readFileSync(args.path, "utf8"); - - const transformedContents = contents - .replace(/import\.meta/g, `{dirname:__dirname,filename:__filename}`) - .replace(/import\.meta\.filename/g, "__filename") - .replace(/import\.meta\.dirname/g, "__dirname"); - - return {contents: transformedContents, loader: "default"}; - }); - }, - } - ], - }).then(() => { - // biome-ignore lint/style/useTemplate: - // eslint-disable-next-line no-undef - console.log("⚡ " + "\x1b[32m" + `Done in ${Date.now() - start}ms`); - }); -} catch (e) { - // eslint-disable-next-line no-undef - console.log(e); - // eslint-disable-next-line no-undef - process.exit(1); -} \ No newline at end of file diff --git a/src/node/consumer/package-lock.json b/src/node/consumer/package-lock.json index 88b7ff6..a7c3169 100644 --- a/src/node/consumer/package-lock.json +++ b/src/node/consumer/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "@tirke/node-cache-manager-mongodb": "^1.6.0", - "@types/webtorrent": "^0.109.7", "amqplib": "^0.10.3", "axios": "^1.6.1", "bottleneck": "^2.19.5", @@ -27,16 +26,17 @@ "reflect-metadata": "^0.2.1", "sequelize": "^6.36.0", "sequelize-typescript": "^2.1.6", - "user-agents": "^1.0.1444", + "utp-native": "^2.5.3", "webtorrent": "^2.1.35" }, "devDependencies": { + "@swc/cli": "^0.3.9", + "@swc/core": "^1.4.0", "@types/amqplib": "^0.10.4", "@types/magnet-uri": "^5.1.5", "@types/node": "^20.11.16", - "@types/stremio-addon-sdk": "^1.6.10", - "@types/torrent-stream": "^0.0.9", "@types/validator": "^13.11.8", + "@types/webtorrent": "^0.109.7", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "esbuild": "^0.20.0", @@ -518,6 +518,25 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@mole-inc/bin-wrapper": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@mole-inc/bin-wrapper/-/bin-wrapper-8.0.1.tgz", + "integrity": "sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==", + "dev": true, + "dependencies": { + "bin-check": "^4.1.0", + "bin-version-check": "^5.0.0", + "content-disposition": "^0.5.4", + "ext-name": "^5.0.0", + "file-type": "^17.1.6", + "filenamify": "^5.0.2", + "got": "^11.8.5", + "os-filter-obj": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.4", "license": "MIT", @@ -573,6 +592,325 @@ "node": ">=10.0.0" } }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@swc/cli": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.3.9.tgz", + "integrity": "sha512-e5grxGEyNT0fYZEFmhSrRYL1kFAZAXlv+WjfQ35J6J9Hl0EtrMVymAEbGabetg2Q/2FX6HiRcjgc9LrdUCBk4A==", + "dev": true, + "dependencies": { + "@mole-inc/bin-wrapper": "^8.0.1", + "@swc/counter": "^0.1.3", + "commander": "^7.1.0", + "fast-glob": "^3.2.5", + "minimatch": "^9.0.3", + "piscina": "^4.3.0", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3" + }, + "bin": { + "spack": "bin/spack.js", + "swc": "bin/swc.js", + "swcx": "bin/swcx.js" + }, + "engines": { + "node": ">= 16.14.0" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^3.5.1" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@swc/cli/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@swc/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/cli/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@swc/cli/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.0.tgz", + "integrity": "sha512-wc5DMI5BJftnK0Fyx9SNJKkA0+BZSJQx8430yutWmsILkHMBD3Yd9GhlMaxasab9RhgKqZp7Ht30hUYO5ZDvQg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.4.0", + "@swc/core-darwin-x64": "1.4.0", + "@swc/core-linux-arm-gnueabihf": "1.4.0", + "@swc/core-linux-arm64-gnu": "1.4.0", + "@swc/core-linux-arm64-musl": "1.4.0", + "@swc/core-linux-x64-gnu": "1.4.0", + "@swc/core-linux-x64-musl": "1.4.0", + "@swc/core-win32-arm64-msvc": "1.4.0", + "@swc/core-win32-ia32-msvc": "1.4.0", + "@swc/core-win32-x64-msvc": "1.4.0" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.0.tgz", + "integrity": "sha512-UTJ/Vz+s7Pagef6HmufWt6Rs0aUu+EJF4Pzuwvr7JQQ5b1DZeAAUeUtkUTFx/PvCbM8Xfw4XdKBUZfrIKCfW8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.0.tgz", + "integrity": "sha512-f8v58u2GsGak8EtZFN9guXqE0Ep10Suny6xriaW2d8FGqESPyNrnBzli3aqkSeQk5gGqu2zJ7WiiKp3XoUOidA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.0.tgz", + "integrity": "sha512-q2KAkBzmPcTnRij/Y1fgHCKAGevUX/H4uUESrw1J5gmUg9Qip6onKV80lTumA1/aooGJ18LOsB31qdbwmZk9OA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.0.tgz", + "integrity": "sha512-SknGu96W0mzHtLHWm+62fk5+Omp9fMPFO7AWyGFmz2tr8EgRRXtTSrBUnWhAbgcalnhen48GsvtMdxf1KNputg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.0.tgz", + "integrity": "sha512-/k3TDvpBRMDNskHooNN1KqwUhcwkfBlIYxRTnJvsfT2C7My4pffR+4KXmt0IKynlTTbCdlU/4jgX4801FSuliw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.0.tgz", + "integrity": "sha512-GYsTMvNt5+WTVlwwQzOOWsPMw6P/F41u5PGHWmfev8Nd4QJ1h3rWPySKk4mV42IJwH9MgQCVSl3ygwNqwl6kFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.0.tgz", + "integrity": "sha512-jGVPdM/VwF7kK/uYRW5N6FwzKf/FnDjGIR3RPvQokjYJy7Auk+3Oj21C0Jev7sIT9RYnO/TrFEoEozKeD/z2Qw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.0.tgz", + "integrity": "sha512-biHYm1AronEKlt47O/H8sSOBM2BKXMmWT+ApvlxUw50m1RGNnVnE0bgY7tylFuuSiWyXsQPJbmUV708JqORXVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.0.tgz", + "integrity": "sha512-TL5L2tFQb19kJwv6+elToGBj74QXCn9j+hZfwQatvZEJRA5rDK16eH6oAE751dGUArhnWlW3Vj65hViPvTuycw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.0.tgz", + "integrity": "sha512-e2xVezU7XZ2Stzn4i7TOQe2Kn84oYdG0M3A7XI7oTdcpsKCcKwgiMoroiAhqCv+iN20KNqhnWwJiUiTj/qN5AA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "node_modules/@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", + "dev": true + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@thaunknown/idb-chunk-store": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@thaunknown/idb-chunk-store/-/idb-chunk-store-1.0.2.tgz", @@ -652,6 +990,12 @@ "url": "https://github.com/sponsors/tirke" } }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true + }, "node_modules/@types/amqplib": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.4.tgz", @@ -665,10 +1009,23 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/@types/bittorrent-protocol/-/bittorrent-protocol-3.1.6.tgz", "integrity": "sha512-hqiRctJX9t9kknr6nn0q7UlcWHKvw2gSnPc/4jxt7Q/T0RP9txNv27Djue9ZjCNlAJ+irqAnCxtb+TcSpMyhtA==", + "dev": true, "dependencies": { "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "license": "MIT", @@ -676,6 +1033,12 @@ "@types/ms": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -687,10 +1050,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/magnet-uri": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/@types/magnet-uri/-/magnet-uri-5.1.5.tgz", "integrity": "sha512-SbBjlb1KGe38VfjRR+mwqztJd/4skhdKkRbIzPDhTy7IAeEAPZWIVSEkZw00Qr4ZZOGR3/ATJ20WWPBfrKHGdA==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -715,6 +1088,7 @@ "version": "5.8.7", "resolved": "https://registry.npmjs.org/@types/parse-torrent/-/parse-torrent-5.8.7.tgz", "integrity": "sha512-vZtYe450hO+KL7B5fejM8CHWg1LPZKeVXlolphPsWf6n4H0ZUlI6ICbqHoaFmH7JQmU2yRbGgyvqqizdFuGPFQ==", + "dev": true, "dependencies": { "@types/magnet-uri": "*", "@types/node": "*", @@ -725,6 +1099,16 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/parse-torrent-file/-/parse-torrent-file-4.0.6.tgz", "integrity": "sha512-SxqVth0Iv0WuEkqWS5MaY4S4Tlyi+QHkElQREvsUPw2xHcPgKyQ2dkJRRv5vAxmLzH+tnMdOj1Nws/wsenbzUw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -739,19 +1123,6 @@ "version": "9.11.8", "resolved": "https://registry.npmjs.org/@types/simple-peer/-/simple-peer-9.11.8.tgz", "integrity": "sha512-rvqefdp2rvIA6wiomMgKWd2UZNPe6LM2EV5AuY3CPQJF+8TbdrL5TjYdMf0VAjGczzlkH4l1NjDkihwbj3Xodw==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/stremio-addon-sdk": { - "version": "1.6.11", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/torrent-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@types/torrent-stream/-/torrent-stream-0.0.9.tgz", - "integrity": "sha512-SY0K6HNlDdnU7yk4TWpLjlv65/liZnxmftMuOdjRriC2IGExqnAYfl8dprjU1j1KQMPVM/X174cusUPNPloghQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -770,6 +1141,7 @@ "version": "0.109.7", "resolved": "https://registry.npmjs.org/@types/webtorrent/-/webtorrent-0.109.7.tgz", "integrity": "sha512-iDcZkXUMjWOtPpxAWXivs6rpYD++8w4vYJVqGJQKxZh1YxIXm5P3CyHGX735nJ6kFOFXLxD1O0ys/msI1nhRug==", + "dev": true, "dependencies": { "@types/bittorrent-protocol": "*", "@types/node": "*", @@ -1173,6 +1545,41 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/argparse": { "version": "2.0.1", "dev": true, @@ -1361,6 +1768,207 @@ "version": "1.1.1", "license": "MIT" }, + "node_modules/bin-check": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", + "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", + "dev": true, + "dependencies": { + "execa": "^0.7.0", + "executable": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/bin-check/node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "dev": true, + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/bin-check/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/bin-check/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/bin-version": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", + "integrity": "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version-check": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-5.1.0.tgz", + "integrity": "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==", + "dev": true, + "dependencies": { + "bin-version": "^6.0.0", + "semver": "^7.5.3", + "semver-truncate": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version-check/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/bin-version-check/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/bittorrent-lsd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bittorrent-lsd/-/bittorrent-lsd-2.0.0.tgz", @@ -1590,6 +2198,48 @@ "promise-coalesce": "^1.1.2" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.5", "dev": true, @@ -1660,6 +2310,46 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chrome-dgram": { "version": "3.0.6", "funding": [ @@ -1726,6 +2416,18 @@ "node": ">=0.8" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -1758,6 +2460,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/compact2string": { "version": "1.4.1", "license": "BSD", @@ -1769,6 +2480,38 @@ "version": "0.0.1", "license": "MIT" }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/core-util-is": { "version": "1.0.3", "license": "MIT" @@ -1948,6 +2691,33 @@ "version": "2.1.2", "license": "MIT" }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, @@ -1971,6 +2741,15 @@ "node": ">= 10" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.1", "dev": true, @@ -2529,6 +3308,43 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/fast-copy": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", @@ -2643,6 +3459,23 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "17.1.6", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", + "integrity": "sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==", + "dev": true, + "dependencies": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0-alpha.9", + "token-types": "^5.0.0-alpha.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/filename-reserved-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", @@ -2654,6 +3487,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/filenamify": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-5.1.1.tgz", + "integrity": "sha512-M45CbrJLGACfrPOkrTp3j2EcO9OBkKUYME0eiqOCa7i2poaklU0jhlIaMlr8ijLorT0uLAzrn3qXOp5684CkfA==", + "dev": true, + "dependencies": { + "filename-reserved-regex": "^3.0.0", + "strip-outer": "^2.0.0", + "trim-repeated": "^2.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2681,6 +3531,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-versions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", + "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "dev": true, + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "dev": true, @@ -3016,6 +3881,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graphemer": { "version": "1.4.0", "dev": true, @@ -3118,11 +4008,30 @@ "entities": "^4.4.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, "node_modules/http-parser-js": { "version": "0.4.13", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", "integrity": "sha512-u8u5ZaG0Tr/VvHlucK2ufMuOp4/5bvwgneXle+y228K5rMbJOlVjThONcaAw3ikAy8b2OO9RfEucdMHFz3UWMA==" }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3288,6 +4197,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "dev": true, @@ -3405,6 +4328,15 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.1.4", "dev": true, @@ -3682,6 +4614,15 @@ "dev": true, "license": "MIT" }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "10.2.0", "license": "ISC", @@ -3812,6 +4753,15 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "license": "ISC", @@ -3943,8 +4893,7 @@ "node_modules/napi-macros": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", - "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==", - "optional": true + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -3984,6 +4933,28 @@ "node": ">= 0.4.0" } }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "optional": true + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -4040,13 +5011,35 @@ "version": "4.8.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", - "optional": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -4189,6 +5182,36 @@ "node": ">= 0.8.0" } }, + "node_modules/os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "dev": true, + "dependencies": { + "arch": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "dev": true, @@ -4301,6 +5324,19 @@ "node": ">=8" } }, + "node_modules/peek-readable": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", + "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/pg": { "version": "8.11.3", "license": "MIT", @@ -4403,6 +5439,15 @@ "resolved": "https://registry.npmjs.org/piece-length/-/piece-length-2.0.1.tgz", "integrity": "sha512-dBILiDmm43y0JPISWEmVGKBETQjwJe6mSU9GND+P9KW0SJGUwoU/odyH1nbalOP9i8WSYuqf1lQnaj92Bhw+Ug==" }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pino": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/pino/-/pino-8.18.0.tgz", @@ -4550,6 +5595,15 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" }, + "node_modules/piscina": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.3.1.tgz", + "integrity": "sha512-MBj0QYm3hJQ/C/wIXTN1OCYC8uQ4BBJ4LVele2P4ZwVQAH04vkk8E1SpDbuemLAL1dZorbuOob9rYqJeWCcCRg==", + "dev": true, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "license": "MIT", @@ -4613,6 +5667,12 @@ "version": "1.1.0", "license": "MIT" }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -4677,6 +5737,18 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/random-iterate": { "version": "1.0.1", "license": "MIT" @@ -4732,6 +5804,79 @@ "string_decoder": "~0.10.x" } }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dev": true, + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/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==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readable-web-to-node-stream/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==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -4789,6 +5934,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, "node_modules/resolve-from": { "version": "4.0.0", "dev": true, @@ -4806,6 +5957,18 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/retry-as-promised": { "version": "7.0.4", "license": "MIT" @@ -4953,6 +6116,60 @@ "semver": "bin/semver.js" } }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", + "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-truncate/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sequelize": { "version": "6.36.0", "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.36.0.tgz", @@ -5187,6 +6404,39 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "license": "MIT", @@ -5295,6 +6545,15 @@ "node": ">=4" } }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -5314,6 +6573,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-2.0.0.tgz", + "integrity": "sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", + "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -5372,8 +6660,7 @@ "node_modules/timeout-refresh": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-1.0.3.tgz", - "integrity": "sha512-Mz0CX4vBGM5lj8ttbIFt7o4ZMxk/9rgudJRh76EvB7xXZMur7T/cjRiH2w4Fmkq0zxf2QpM8IFvOSRn8FEu3gA==", - "optional": true + "integrity": "sha512-Mz0CX4vBGM5lj8ttbIFt7o4ZMxk/9rgudJRh76EvB7xXZMur7T/cjRiH2w4Fmkq0zxf2QpM8IFvOSRn8FEu3gA==" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -5387,10 +6674,51 @@ "node": ">=8.0" } }, + "node_modules/token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/toposort-class": { "version": "1.0.1", "license": "MIT" }, + "node_modules/trim-repeated": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-2.0.0.tgz", + "integrity": "sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-api-utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.0.tgz", @@ -5973,8 +7301,7 @@ "node_modules/unordered-set": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz", - "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==", - "optional": true + "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==" }, "node_modules/uri-js": { "version": "4.4.1", @@ -5993,13 +7320,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/user-agents": { - "version": "1.1.104", - "license": "BSD-2-Clause", - "dependencies": { - "lodash.clonedeep": "^4.5.0" - } - }, "node_modules/ut_metadata": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/ut_metadata/-/ut_metadata-4.0.3.tgz", @@ -6123,15 +7443,13 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/utp-native": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/utp-native/-/utp-native-2.5.3.tgz", "integrity": "sha512-sWTrWYXPhhWJh+cS2baPzhaZc89zwlWCfwSthUjGhLkZztyPhcQllo+XVVCbNGi7dhyRlxkWxN4NKU6FbA9Y8w==", "hasInstallScript": true, - "optional": true, "dependencies": { "napi-macros": "^2.0.0", "node-gyp-build": "^4.2.0", @@ -6150,7 +7468,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -6177,14 +7494,12 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "optional": true + ] }, "node_modules/utp-native/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==", - "optional": true, "dependencies": { "safe-buffer": "~5.2.0" } diff --git a/src/node/consumer/package.json b/src/node/consumer/package.json index 3e515b1..1a04817 100644 --- a/src/node/consumer/package.json +++ b/src/node/consumer/package.json @@ -3,10 +3,14 @@ "version": "0.0.1", "type": "module", "scripts": { - "build": "node esbuild.js", - "dev": "tsx watch --ignore node_modules src/main.ts | pino-pretty", - "start": "node dist/main.cjs", - "lint": "npx eslint ./src --ext .ts,.js" + "build": "swc src -d dist", + "watch-compile": "swc src -w --out-dir dist", + "watch-dev": "nodemon --watch \"dist/**/*\" -e js ./dist/main.js", + "dev": "concurrently \"npm run watch-compile\" \"npm run watch-dev\"", + "start": "node dist/main.js", + "clean": "rm -rf dist", + "lint": "eslint --ext .ts src", + "lint:fix": "eslint --ext .ts src --fix" }, "license": "MIT", "dependencies": { @@ -27,15 +31,19 @@ "reflect-metadata": "^0.2.1", "sequelize": "^6.36.0", "sequelize-typescript": "^2.1.6", + "utp-native": "^2.5.3", "webtorrent": "^2.1.35" }, "devDependencies": { + "@swc/cli": "^0.3.9", + "@swc/core": "^1.4.0", "@types/amqplib": "^0.10.4", "@types/magnet-uri": "^5.1.5", "@types/node": "^20.11.16", "@types/validator": "^13.11.8", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", + "@types/webtorrent": "^0.109.7", "esbuild": "^0.20.0", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", diff --git a/src/node/consumer/src/lib/helpers/extension_helpers.ts b/src/node/consumer/src/lib/helpers/extension_helpers.ts index b74933b..f24b237 100644 --- a/src/node/consumer/src/lib/helpers/extension_helpers.ts +++ b/src/node/consumer/src/lib/helpers/extension_helpers.ts @@ -47,19 +47,19 @@ const DISK_EXTENSIONS = [ ]; export const ExtensionHelpers = { - isVideo(filename: string) { + isVideo(filename: string): boolean { return this.isExtension(filename, VIDEO_EXTENSIONS); }, - isSubtitle(filename: string) { + isSubtitle(filename: string): boolean { return this.isExtension(filename, SUBTITLE_EXTENSIONS); }, - isDisk(filename: string) { + isDisk(filename: string): boolean { return this.isExtension(filename, DISK_EXTENSIONS); }, - isExtension(filename: string, extensions: string[]) { + isExtension(filename: string, extensions: string[]): boolean { const extensionMatch = filename.match(/\.(\w{2,4})$/); return extensionMatch !== null && extensions.includes(extensionMatch[1].toLowerCase()); } diff --git a/src/node/consumer/src/lib/helpers/promises_helpers.ts b/src/node/consumer/src/lib/helpers/promises_helpers.ts index dacefb7..af38d2d 100644 --- a/src/node/consumer/src/lib/helpers/promises_helpers.ts +++ b/src/node/consumer/src/lib/helpers/promises_helpers.ts @@ -1,24 +1,25 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ export const PromiseHelpers = { - sequence: async function (promises: (() => Promise)[]) { + sequence: async function (promises: (() => Promise)[]): Promise { return promises.reduce((promise, func) => promise.then(result => func().then(res => result.concat(res))), Promise.resolve([])); }, - first: async function (promises) { - return Promise.all(promises.map(p => { - return p.then((val) => Promise.reject(val), (err) => Promise.resolve(err)); + first: async function (promises: any): Promise { + return Promise.all(promises.map((p: any) => { + return p.then((val: any) => Promise.reject(val), (err: any) => Promise.resolve(err)); })).then( (errors) => Promise.reject(errors), (val) => Promise.resolve(val) ); }, - delay: async function (duration: number) { + delay: async function (duration: number): Promise { return new Promise(resolve => setTimeout(() => resolve(), duration)); }, - timeout: async function (timeoutMs: number, promise, message = 'Timed out') { + timeout: async function (timeoutMs: number, promise: any, message = 'Timed out'): Promise { return Promise.race([ promise, new Promise(function (resolve, reject) { @@ -29,7 +30,8 @@ export const PromiseHelpers = { ]); }, - mostCommonValue: function (array) { + mostCommonValue: function (array: any[]): any { return array.sort((a, b) => array.filter(v => v === a).length - array.filter(v => v === b).length).pop(); } -}; \ No newline at end of file +}; +/* eslint-enable @typescript-eslint/no-explicit-any */ \ No newline at end of file diff --git a/src/node/consumer/src/lib/interfaces/cache_service.ts b/src/node/consumer/src/lib/interfaces/cache_service.ts index 3a94858..9487547 100644 --- a/src/node/consumer/src/lib/interfaces/cache_service.ts +++ b/src/node/consumer/src/lib/interfaces/cache_service.ts @@ -1,8 +1,10 @@ import {CacheMethod} from "../services/cache_service"; +/* eslint-disable @typescript-eslint/no-explicit-any */ export interface ICacheService { cacheWrapImdbId: (key: string, method: CacheMethod) => Promise; cacheWrapKitsuId: (key: string, method: CacheMethod) => Promise; cacheWrapMetadata: (id: string, method: CacheMethod) => Promise; cacheTrackers: (method: CacheMethod) => Promise; -} \ No newline at end of file +} +/* eslint-enable @typescript-eslint/no-explicit-any */ \ No newline at end of file diff --git a/src/node/consumer/src/lib/interfaces/logging_service.ts b/src/node/consumer/src/lib/interfaces/logging_service.ts index 06266ed..d980b22 100644 --- a/src/node/consumer/src/lib/interfaces/logging_service.ts +++ b/src/node/consumer/src/lib/interfaces/logging_service.ts @@ -1,9 +1,8 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ export interface ILoggingService { info(message: string, ...args: any[]): void; - error(message: string, ...args: any[]): void; - debug(message: string, ...args: any[]): void; - warn(message: string, ...args: any[]): void; -} \ No newline at end of file +} +/* eslint-enable @typescript-eslint/no-explicit-any */ \ No newline at end of file diff --git a/src/node/consumer/src/lib/interfaces/parse_torrent_title_result.ts b/src/node/consumer/src/lib/interfaces/parse_torrent_title_result.ts index fcb9a61..71514b3 100644 --- a/src/node/consumer/src/lib/interfaces/parse_torrent_title_result.ts +++ b/src/node/consumer/src/lib/interfaces/parse_torrent_title_result.ts @@ -1,3 +1,5 @@ +import {IFileAttributes} from "../repository/interfaces/file_attributes"; + export interface IParseTorrentTitleResult { title?: string; date?: string; @@ -28,4 +30,7 @@ export interface IParseTorrentTitleResult { episode?: number; languages?: string; dubbed?: boolean; + videoFile?: IFileAttributes; + folderName?: string; + fileName?: string; } \ No newline at end of file diff --git a/src/node/consumer/src/lib/interfaces/parsed_torrent.ts b/src/node/consumer/src/lib/interfaces/parsed_torrent.ts index f61cfeb..26ec81d 100644 --- a/src/node/consumer/src/lib/interfaces/parsed_torrent.ts +++ b/src/node/consumer/src/lib/interfaces/parsed_torrent.ts @@ -1,5 +1,5 @@ -import {IParseTorrentTitleResult} from "./parse_torrent_title_result"; import {TorrentType} from "../enums/torrent_types"; +import {IParseTorrentTitleResult} from "./parse_torrent_title_result"; import {ITorrentFileCollection} from "./torrent_file_collection"; export interface IParsedTorrent extends IParseTorrentTitleResult { @@ -9,7 +9,7 @@ export interface IParsedTorrent extends IParseTorrentTitleResult { kitsuId?: number; trackers?: string; provider?: string | null; - infoHash: string | null; + infoHash: string; type: string | TorrentType; uploadDate?: Date; seeders?: number; diff --git a/src/node/consumer/src/interfaces/process_torrents_job.ts b/src/node/consumer/src/lib/interfaces/process_torrents_job.ts similarity index 100% rename from src/node/consumer/src/interfaces/process_torrents_job.ts rename to src/node/consumer/src/lib/interfaces/process_torrents_job.ts diff --git a/src/node/consumer/src/lib/interfaces/season_episode_map.ts b/src/node/consumer/src/lib/interfaces/season_episode_map.ts new file mode 100644 index 0000000..55bb2f7 --- /dev/null +++ b/src/node/consumer/src/lib/interfaces/season_episode_map.ts @@ -0,0 +1,7 @@ +import {ICommonVideoMetadata} from "./common_video_metadata"; + +export interface ISeasonEpisodeMap { + [season: number]: { + [episode: number]: ICommonVideoMetadata; + } +} \ No newline at end of file diff --git a/src/node/consumer/src/lib/interfaces/torrent_entries_service.ts b/src/node/consumer/src/lib/interfaces/torrent_entries_service.ts index 4d4696d..a92ba1c 100644 --- a/src/node/consumer/src/lib/interfaces/torrent_entries_service.ts +++ b/src/node/consumer/src/lib/interfaces/torrent_entries_service.ts @@ -1,18 +1,18 @@ +import {ITorrentAttributes} from "../repository/interfaces/torrent_attributes"; +import {SkipTorrent} from "../repository/models/skipTorrent"; +import {Torrent} from "../repository/models/torrent"; import {IParsedTorrent} from "./parsed_torrent"; -import {Torrent} from "../../repository/models/torrent"; -import {ITorrentAttributes} from "../../repository/interfaces/torrent_attributes"; -import {SkipTorrent} from "../../repository/models/skipTorrent"; export interface ITorrentEntriesService { - createTorrentEntry(torrent: IParsedTorrent, overwrite): Promise; + createTorrentEntry(torrent: IParsedTorrent, overwrite: boolean): Promise; - createSkipTorrentEntry(torrent: Torrent): Promise<[SkipTorrent, boolean]>; + createSkipTorrentEntry(torrent: Torrent): Promise<[SkipTorrent, boolean | null]>; - getStoredTorrentEntry(torrent: Torrent): Promise; + getStoredTorrentEntry(torrent: Torrent): Promise; checkAndUpdateTorrent(torrent: IParsedTorrent): Promise; createTorrentContents(torrent: Torrent): Promise; - updateTorrentSeeders(torrent: ITorrentAttributes): Promise<[number] | ITorrentAttributes>; + updateTorrentSeeders(torrent: ITorrentAttributes): Promise<[number] | undefined>; } \ No newline at end of file diff --git a/src/node/consumer/src/lib/interfaces/torrent_file_collection.ts b/src/node/consumer/src/lib/interfaces/torrent_file_collection.ts index 5b9f824..3306a44 100644 --- a/src/node/consumer/src/lib/interfaces/torrent_file_collection.ts +++ b/src/node/consumer/src/lib/interfaces/torrent_file_collection.ts @@ -1,6 +1,6 @@ -import {IContentAttributes} from "../../repository/interfaces/content_attributes"; -import {IFileAttributes} from "../../repository/interfaces/file_attributes"; -import {ISubtitleAttributes} from "../../repository/interfaces/subtitle_attributes"; +import {IContentAttributes} from "../repository/interfaces/content_attributes"; +import {IFileAttributes} from "../repository/interfaces/file_attributes"; +import {ISubtitleAttributes} from "../repository/interfaces/subtitle_attributes"; export interface ITorrentFileCollection { contents?: IContentAttributes[]; diff --git a/src/node/consumer/src/lib/interfaces/torrent_processing_service.ts b/src/node/consumer/src/lib/interfaces/torrent_processing_service.ts index f4a1db7..e3aa901 100644 --- a/src/node/consumer/src/lib/interfaces/torrent_processing_service.ts +++ b/src/node/consumer/src/lib/interfaces/torrent_processing_service.ts @@ -1,4 +1,4 @@ -import {IIngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes"; +import {IIngestedTorrentAttributes} from "../repository/interfaces/ingested_torrent_attributes"; export interface ITorrentProcessingService { processTorrentRecord(torrent: IIngestedTorrentAttributes): Promise; diff --git a/src/node/consumer/src/jobs/process_torrents_job.ts b/src/node/consumer/src/lib/jobs/process_torrents_job.ts similarity index 72% rename from src/node/consumer/src/jobs/process_torrents_job.ts rename to src/node/consumer/src/lib/jobs/process_torrents_job.ts index 4fed81f..9ba5aef 100644 --- a/src/node/consumer/src/jobs/process_torrents_job.ts +++ b/src/node/consumer/src/lib/jobs/process_torrents_job.ts @@ -1,12 +1,12 @@ import client, {Channel, Connection, ConsumeMessage, Options} from 'amqplib' -import {IIngestedRabbitMessage, IIngestedRabbitTorrent} from "../lib/interfaces/ingested_rabbit_message"; -import {IIngestedTorrentAttributes} from "../repository/interfaces/ingested_torrent_attributes"; -import {configurationService} from '../lib/services/configuration_service'; import {inject, injectable} from "inversify"; -import {IocTypes} from "../lib/models/ioc_types"; -import {ITorrentProcessingService} from "../lib/interfaces/torrent_processing_service"; -import {ILoggingService} from "../lib/interfaces/logging_service"; +import {IIngestedRabbitMessage, IIngestedRabbitTorrent} from "../interfaces/ingested_rabbit_message"; +import {ILoggingService} from "../interfaces/logging_service"; import {IProcessTorrentsJob} from "../interfaces/process_torrents_job"; +import {ITorrentProcessingService} from "../interfaces/torrent_processing_service"; +import {IocTypes} from "../models/ioc_types"; +import {IIngestedTorrentAttributes} from "../repository/interfaces/ingested_torrent_attributes"; +import {configurationService} from '../services/configuration_service'; @injectable() export class ProcessTorrentsJob implements IProcessTorrentsJob { @@ -21,7 +21,7 @@ export class ProcessTorrentsJob implements IProcessTorrentsJob { this.logger = logger; } - public listenToQueue = async () => { + public listenToQueue = async (): Promise => { if (!configurationService.jobConfig.JOBS_ENABLED) { return; } @@ -34,26 +34,27 @@ export class ProcessTorrentsJob implements IProcessTorrentsJob { this.logger.error('Failed to connect and setup channel', error); } } - private processMessage = (msg: ConsumeMessage) => { + + private processMessage = (msg: ConsumeMessage | null): Promise => { const ingestedTorrent: IIngestedTorrentAttributes = this.getMessageAsJson(msg); return this.torrentProcessingService.processTorrentRecord(ingestedTorrent); }; - private getMessageAsJson = (msg: ConsumeMessage): IIngestedTorrentAttributes => { + + private getMessageAsJson = (msg: ConsumeMessage | null): IIngestedTorrentAttributes => { const content = msg?.content.toString('utf8') ?? "{}"; const receivedObject: IIngestedRabbitMessage = JSON.parse(content); const receivedTorrent: IIngestedRabbitTorrent = receivedObject.message; return {...receivedTorrent, info_hash: receivedTorrent.infoHash}; }; - private assertAndConsumeQueue = async (channel: Channel) => { + + private assertAndConsumeQueue = async (channel: Channel): Promise => { this.logger.info('Worker is running! Waiting for new torrents...'); - const ackMsg = async (msg: ConsumeMessage) => { - try { - await this.processMessage(msg); - channel.ack(msg); - } catch (error) { - this.logger.error('Failed processing torrent', error); - } + const ackMsg = async (msg: ConsumeMessage | null): Promise => { + await this.processMessage(msg) + .then(() => this.logger.info('Processed torrent')) + .then(() => msg && channel.ack(msg)) + .catch((error) => this.logger.error('Failed to process torrent', error)); } try { diff --git a/src/node/consumer/src/lib/models/composition_root.ts b/src/node/consumer/src/lib/models/composition_root.ts index e52e2f0..2b8dbf0 100644 --- a/src/node/consumer/src/lib/models/composition_root.ts +++ b/src/node/consumer/src/lib/models/composition_root.ts @@ -1,8 +1,8 @@ import {inject, injectable} from "inversify"; -import {IDatabaseRepository} from "../../repository/interfaces/database_repository"; -import {ITrackerService} from "../interfaces/tracker_service"; -import {IProcessTorrentsJob} from "../../interfaces/process_torrents_job"; import {ICompositionalRoot} from "../interfaces/composition_root"; +import {IProcessTorrentsJob} from "../interfaces/process_torrents_job"; +import {ITrackerService} from "../interfaces/tracker_service"; +import {IDatabaseRepository} from "../repository/interfaces/database_repository"; import {IocTypes} from "./ioc_types"; @injectable() @@ -19,7 +19,7 @@ export class CompositionalRoot implements ICompositionalRoot { this.processTorrentsJob = processTorrentsJob; } - start = async () => { + start = async (): Promise => { await this.trackerService.getTrackers(); await this.databaseRepository.connect(); await this.processTorrentsJob.listenToQueue(); diff --git a/src/node/consumer/src/lib/models/configuration/cache_config.ts b/src/node/consumer/src/lib/models/configuration/cache_config.ts index 0e2c927..1cba364 100644 --- a/src/node/consumer/src/lib/models/configuration/cache_config.ts +++ b/src/node/consumer/src/lib/models/configuration/cache_config.ts @@ -9,7 +9,7 @@ export const cacheConfig = { NO_CACHE: BooleanHelpers.parseBool(process.env.NO_CACHE, false), COLLECTION_NAME: process.env.MONGODB_COLLECTION || 'knightcrawler_consumer_collection', - get MONGO_URI() { + get MONGO_URI(): string { return `mongodb://${this.MONGO_INITDB_ROOT_USERNAME}:${this.MONGO_INITDB_ROOT_PASSWORD}@${this.MONGODB_HOST}:${this.MONGODB_PORT}/${this.MONGODB_DB}?authSource=admin`; } }; \ No newline at end of file diff --git a/src/node/consumer/src/lib/models/configuration/database_config.ts b/src/node/consumer/src/lib/models/configuration/database_config.ts index 8e5dccd..bcefcd6 100644 --- a/src/node/consumer/src/lib/models/configuration/database_config.ts +++ b/src/node/consumer/src/lib/models/configuration/database_config.ts @@ -8,7 +8,7 @@ export const databaseConfig = { POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD || 'postgres', AUTO_CREATE_AND_APPLY_MIGRATIONS: BooleanHelpers.parseBool(process.env.AUTO_CREATE_AND_APPLY_MIGRATIONS, false), - get POSTGRES_URI() { + get POSTGRES_URI(): string { return `postgres://${this.POSTGRES_USER}:${this.POSTGRES_PASSWORD}@${this.POSTGRES_HOST}:${this.POSTGRES_PORT}/${this.POSTGRES_DB}`; } }; \ No newline at end of file diff --git a/src/node/consumer/src/lib/models/configuration/torrent_config.ts b/src/node/consumer/src/lib/models/configuration/torrent_config.ts index 543f277..683a49a 100644 --- a/src/node/consumer/src/lib/models/configuration/torrent_config.ts +++ b/src/node/consumer/src/lib/models/configuration/torrent_config.ts @@ -1,5 +1,5 @@ export const torrentConfig = { - MAX_CONNECTIONS_PER_TORRENT: parseInt(process.env.MAX_SINGLE_TORRENT_CONNECTIONS || "20", 10), + MAX_CONNECTIONS_PER_TORRENT: parseInt(process.env.MAX_CONNECTIONS_PER_TORRENT || "20", 10), MAX_CONNECTIONS_OVERALL: parseInt(process.env.MAX_CONNECTIONS_OVERALL || "100", 10), TIMEOUT: parseInt(process.env.TORRENT_TIMEOUT || "30000", 10) }; \ No newline at end of file diff --git a/src/node/consumer/src/lib/models/inversify_config.ts b/src/node/consumer/src/lib/models/inversify_config.ts index 17cd120..17da92b 100644 --- a/src/node/consumer/src/lib/models/inversify_config.ts +++ b/src/node/consumer/src/lib/models/inversify_config.ts @@ -1,30 +1,29 @@ -import "reflect-metadata"; // required import {Container} from "inversify"; -import {IocTypes} from "./ioc_types"; import {ICacheService} from "../interfaces/cache_service"; +import {ICompositionalRoot} from "../interfaces/composition_root"; import {ILoggingService} from "../interfaces/logging_service"; import {IMetadataService} from "../interfaces/metadata_service"; +import {IProcessTorrentsJob} from "../interfaces/process_torrents_job"; +import {ITorrentDownloadService} from "../interfaces/torrent_download_service"; +import {ITorrentEntriesService} from "../interfaces/torrent_entries_service"; import {ITorrentFileService} from "../interfaces/torrent_file_service"; import {ITorrentProcessingService} from "../interfaces/torrent_processing_service"; import {ITorrentSubtitleService} from "../interfaces/torrent_subtitle_service"; -import {ITorrentEntriesService} from "../interfaces/torrent_entries_service"; -import {ITorrentDownloadService} from "../interfaces/torrent_download_service"; import {ITrackerService} from "../interfaces/tracker_service"; -import {IProcessTorrentsJob} from "../../interfaces/process_torrents_job"; -import {ICompositionalRoot} from "../interfaces/composition_root"; -import {IDatabaseRepository} from "../../repository/interfaces/database_repository"; -import {CompositionalRoot} from "./composition_root"; +import {ProcessTorrentsJob} from "../jobs/process_torrents_job"; +import {DatabaseRepository} from "../repository/database_repository"; +import {IDatabaseRepository} from "../repository/interfaces/database_repository"; import {CacheService} from "../services/cache_service"; import {LoggingService} from "../services/logging_service"; import {MetadataService} from "../services/metadata_service"; import {TorrentDownloadService} from "../services/torrent_download_service"; import {TorrentEntriesService} from "../services/torrent_entries_service"; -import {TorrentProcessingService} from "../services/torrent_processing_service"; import {TorrentFileService} from "../services/torrent_file_service"; +import {TorrentProcessingService} from "../services/torrent_processing_service"; import {TorrentSubtitleService} from "../services/torrent_subtitle_service"; import {TrackerService} from "../services/tracker_service"; -import {DatabaseRepository} from "../../repository/database_repository"; -import {ProcessTorrentsJob} from "../../jobs/process_torrents_job"; +import {CompositionalRoot} from "./composition_root"; +import {IocTypes} from "./ioc_types"; const serviceContainer = new Container(); diff --git a/src/node/consumer/src/repository/database_repository.ts b/src/node/consumer/src/lib/repository/database_repository.ts similarity index 84% rename from src/node/consumer/src/repository/database_repository.ts rename to src/node/consumer/src/lib/repository/database_repository.ts index 2d1dfff..c59f778 100644 --- a/src/node/consumer/src/repository/database_repository.ts +++ b/src/node/consumer/src/lib/repository/database_repository.ts @@ -1,24 +1,24 @@ +import {inject, injectable} from "inversify"; import moment from 'moment'; import {literal, Op, WhereOptions} from "sequelize"; import {Model, Sequelize} from 'sequelize-typescript'; -import {configurationService} from '../lib/services/configuration_service'; -import {PromiseHelpers} from '../lib/helpers/promises_helpers'; -import {Provider} from "./models/provider"; -import {File} from "./models/file"; -import {Torrent} from "./models/torrent"; -import {IngestedTorrent} from "./models/ingestedTorrent"; -import {Subtitle} from "./models/subtitle"; -import {Content} from "./models/content"; -import {SkipTorrent} from "./models/skipTorrent"; -import {IFileAttributes, IFileCreationAttributes} from "./interfaces/file_attributes"; -import {ITorrentAttributes, ITorrentCreationAttributes} from "./interfaces/torrent_attributes"; -import {IngestedPage} from "./models/ingestedPage"; -import {ILoggingService} from "../lib/interfaces/logging_service"; -import {IocTypes} from "../lib/models/ioc_types"; -import {inject, injectable} from "inversify"; -import {IDatabaseRepository} from "./interfaces/database_repository"; +import {PromiseHelpers} from '../helpers/promises_helpers'; +import {ILoggingService} from "../interfaces/logging_service"; +import {IocTypes} from "../models/ioc_types"; +import {configurationService} from '../services/configuration_service'; import {IContentCreationAttributes} from "./interfaces/content_attributes"; -import {ISubtitleCreationAttributes} from "./interfaces/subtitle_attributes"; +import {IDatabaseRepository} from "./interfaces/database_repository"; +import {IFileAttributes, IFileCreationAttributes} from "./interfaces/file_attributes"; +import {ISubtitleAttributes, ISubtitleCreationAttributes} from "./interfaces/subtitle_attributes"; +import {ITorrentAttributes, ITorrentCreationAttributes} from "./interfaces/torrent_attributes"; +import {Content} from "./models/content"; +import {File} from "./models/file"; +import {IngestedPage} from "./models/ingestedPage"; +import {IngestedTorrent} from "./models/ingestedTorrent"; +import {Provider} from "./models/provider"; +import {SkipTorrent} from "./models/skipTorrent"; +import {Subtitle} from "./models/subtitle"; +import {Torrent} from "./models/torrent"; @injectable() export class DatabaseRepository implements IDatabaseRepository { @@ -33,6 +33,7 @@ export class DatabaseRepository implements IDatabaseRepository { SkipTorrent, IngestedTorrent, IngestedPage]; + private logger: ILoggingService; constructor(@inject(IocTypes.ILoggingService) logger: ILoggingService) { @@ -40,7 +41,7 @@ export class DatabaseRepository implements IDatabaseRepository { this.database = this.createDatabase(); } - public connect = async () => { + public connect = async (): Promise => { try { await this.database.sync({alter: configurationService.databaseConfig.AUTO_CREATE_AND_APPLY_MIGRATIONS}); } catch (error) { @@ -50,7 +51,7 @@ export class DatabaseRepository implements IDatabaseRepository { } }; - public getProvider = async (provider: Provider) => { + public getProvider = async (provider: Provider): Promise => { try { const [result] = await Provider.findOrCreate({where: {name: {[Op.eq]: provider.name}}, defaults: provider}); return result; @@ -121,7 +122,7 @@ export class DatabaseRepository implements IDatabaseRepository { await this.createSubtitles(torrent.infoHash, torrent.subtitles); } catch (error) { this.logger.error(`Failed to create torrent: ${torrent.infoHash}`); - this.logger.debug(error); + this.logger.debug("Error: ", error); } }; @@ -145,13 +146,13 @@ export class DatabaseRepository implements IDatabaseRepository { if (operatingFile.dataValues) { await operatingFile.save(); } else { - await File.upsert(operatingFile); + await File.upsert(file); } - await this.upsertSubtitles(operatingFile, operatingFile.subtitles); + await this.upsertSubtitles(operatingFile, file.subtitles); } else { if (operatingFile.subtitles && operatingFile.subtitles.length) { operatingFile.subtitles = operatingFile.subtitles.map(subtitle => { - subtitle.title = subtitle.path; + subtitle.title = subtitle.path || ''; return subtitle; }); } @@ -159,7 +160,7 @@ export class DatabaseRepository implements IDatabaseRepository { } } catch (error) { this.logger.error(`Failed to create file: ${file.infoHash}`); - this.logger.debug(error); + this.logger.debug("Error: ", error); } }; @@ -169,25 +170,26 @@ export class DatabaseRepository implements IDatabaseRepository { public deleteFile = async (id: number): Promise => File.destroy({where: {id: id}}); - public createSubtitles = async (infoHash: string, subtitles: ISubtitleCreationAttributes[]): Promise[]> => { + public createSubtitles = async (infoHash: string, subtitles: ISubtitleCreationAttributes[] | undefined): Promise[]> => { if (subtitles && subtitles.length) { - return Subtitle.bulkCreate(subtitles.map(subtitle => ({infoHash, title: subtitle.path, ...subtitle}))); + return Subtitle.bulkCreate(subtitles.map(subtitle => ({...subtitle, infoHash: infoHash, title: subtitle.path}))); } return Promise.resolve(); }; - public upsertSubtitles = async (file: File, subtitles: Subtitle[]): Promise => { + public upsertSubtitles = async (file: File, subtitles: ISubtitleCreationAttributes[] | undefined): Promise => { if (file.id && subtitles && subtitles.length) { await PromiseHelpers.sequence(subtitles .map(subtitle => { subtitle.fileId = file.id; subtitle.infoHash = subtitle.infoHash || file.infoHash; - subtitle.title = subtitle.title || subtitle.path; + subtitle.title = subtitle.title || subtitle.path || ''; return subtitle; }) .map(subtitle => async () => { - if (subtitle.dataValues) { - await subtitle.save(); + const operatingInstance = Subtitle.build(subtitle); + if (operatingInstance.dataValues) { + await operatingInstance.save(); } else { await Subtitle.create(subtitle); } @@ -199,9 +201,9 @@ export class DatabaseRepository implements IDatabaseRepository { public getUnassignedSubtitles = async (): Promise => Subtitle.findAll({where: {fileId: null}}); - public createContents = async (infoHash: string, contents: IContentCreationAttributes[]): Promise => { + public createContents = async (infoHash: string, contents: IContentCreationAttributes[] | undefined): Promise => { if (contents && contents.length) { - await Content.bulkCreate(contents.map(content => ({infoHash, ...content})), {ignoreDuplicates: true}); + await Content.bulkCreate(contents.map(content => ({...content, infoHash})), {ignoreDuplicates: true}); await Torrent.update({opened: true}, {where: {infoHash: infoHash}, silent: true}); } }; @@ -216,7 +218,7 @@ export class DatabaseRepository implements IDatabaseRepository { return result.dataValues as SkipTorrent; }; - public createSkipTorrent = async (torrent: ITorrentCreationAttributes): Promise<[SkipTorrent, boolean]> => SkipTorrent.upsert({infoHash: torrent.infoHash}); + public createSkipTorrent = async (torrent: ITorrentCreationAttributes): Promise<[SkipTorrent, boolean | null]> => SkipTorrent.upsert({infoHash: torrent.infoHash}); private createDatabase = (): Sequelize => { const newDatabase = new Sequelize( diff --git a/src/node/consumer/src/repository/interfaces/content_attributes.ts b/src/node/consumer/src/lib/repository/interfaces/content_attributes.ts similarity index 100% rename from src/node/consumer/src/repository/interfaces/content_attributes.ts rename to src/node/consumer/src/lib/repository/interfaces/content_attributes.ts diff --git a/src/node/consumer/src/repository/interfaces/database_repository.ts b/src/node/consumer/src/lib/repository/interfaces/database_repository.ts similarity index 81% rename from src/node/consumer/src/repository/interfaces/database_repository.ts rename to src/node/consumer/src/lib/repository/interfaces/database_repository.ts index a0d1d43..9331853 100644 --- a/src/node/consumer/src/repository/interfaces/database_repository.ts +++ b/src/node/consumer/src/lib/repository/interfaces/database_repository.ts @@ -1,15 +1,15 @@ -import {Provider} from "../models/provider"; import {WhereOptions} from "sequelize"; -import {ITorrentAttributes, ITorrentCreationAttributes} from "./torrent_attributes"; -import {Torrent} from "../models/torrent"; -import {IFileAttributes, IFileCreationAttributes} from "./file_attributes"; -import {File} from "../models/file"; -import {Subtitle} from "../models/subtitle"; import {Model} from "sequelize-typescript"; import {Content} from "../models/content"; +import {File} from "../models/file"; +import {Provider} from "../models/provider"; import {SkipTorrent} from "../models/skipTorrent"; -import {ISubtitleCreationAttributes} from "./subtitle_attributes"; +import {Subtitle} from "../models/subtitle"; +import {Torrent} from "../models/torrent"; import {IContentCreationAttributes} from "./content_attributes"; +import {IFileAttributes, IFileCreationAttributes} from "./file_attributes"; +import {ISubtitleAttributes, ISubtitleCreationAttributes} from "./subtitle_attributes"; +import {ITorrentAttributes, ITorrentCreationAttributes} from "./torrent_attributes"; export interface IDatabaseRepository { connect(): Promise; @@ -26,9 +26,9 @@ export interface IDatabaseRepository { getTorrentsWithoutSize(): Promise; - getUpdateSeedersTorrents(limit): Promise; + getUpdateSeedersTorrents(limit: number): Promise; - getUpdateSeedersNewTorrents(limit): Promise; + getUpdateSeedersNewTorrents(limit: number): Promise; getNoContentsTorrents(): Promise; @@ -46,9 +46,9 @@ export interface IDatabaseRepository { deleteFile(id: number): Promise; - createSubtitles(infoHash: string, subtitles: ISubtitleCreationAttributes[]): Promise[]>; + createSubtitles(infoHash: string, subtitles: ISubtitleCreationAttributes[]): Promise[]>; - upsertSubtitles(file: File, subtitles: Subtitle[]): Promise; + upsertSubtitles(file: File, subtitles: ISubtitleCreationAttributes[] | undefined): Promise; getSubtitles(infoHash: string): Promise; @@ -60,5 +60,5 @@ export interface IDatabaseRepository { getSkipTorrent(infoHash: string): Promise; - createSkipTorrent(torrent: ITorrentCreationAttributes): Promise<[SkipTorrent, boolean]>; + createSkipTorrent(torrent: ITorrentCreationAttributes): Promise<[SkipTorrent, boolean | null]>; } \ No newline at end of file diff --git a/src/node/consumer/src/repository/interfaces/file_attributes.ts b/src/node/consumer/src/lib/repository/interfaces/file_attributes.ts similarity index 84% rename from src/node/consumer/src/repository/interfaces/file_attributes.ts rename to src/node/consumer/src/lib/repository/interfaces/file_attributes.ts index da75405..25f025c 100644 --- a/src/node/consumer/src/repository/interfaces/file_attributes.ts +++ b/src/node/consumer/src/lib/repository/interfaces/file_attributes.ts @@ -1,12 +1,12 @@ import {Optional} from "sequelize"; +import {IParseTorrentTitleResult} from "../../interfaces/parse_torrent_title_result"; import {ISubtitleAttributes} from "./subtitle_attributes"; -import {IParseTorrentTitleResult} from "../../lib/interfaces/parse_torrent_title_result"; export interface IFileAttributes extends IParseTorrentTitleResult { id?: number; infoHash?: string; fileIndex?: number; - title?: string; + title: string; size?: number; imdbId?: string; imdbSeason?: number; diff --git a/src/node/consumer/src/repository/interfaces/ingested_page_attributes.ts b/src/node/consumer/src/lib/repository/interfaces/ingested_page_attributes.ts similarity index 100% rename from src/node/consumer/src/repository/interfaces/ingested_page_attributes.ts rename to src/node/consumer/src/lib/repository/interfaces/ingested_page_attributes.ts diff --git a/src/node/consumer/src/repository/interfaces/ingested_torrent_attributes.ts b/src/node/consumer/src/lib/repository/interfaces/ingested_torrent_attributes.ts similarity index 100% rename from src/node/consumer/src/repository/interfaces/ingested_torrent_attributes.ts rename to src/node/consumer/src/lib/repository/interfaces/ingested_torrent_attributes.ts diff --git a/src/node/consumer/src/repository/interfaces/provider_attributes.ts b/src/node/consumer/src/lib/repository/interfaces/provider_attributes.ts similarity index 100% rename from src/node/consumer/src/repository/interfaces/provider_attributes.ts rename to src/node/consumer/src/lib/repository/interfaces/provider_attributes.ts diff --git a/src/node/consumer/src/repository/interfaces/skip_torrent_attributes.ts b/src/node/consumer/src/lib/repository/interfaces/skip_torrent_attributes.ts similarity index 100% rename from src/node/consumer/src/repository/interfaces/skip_torrent_attributes.ts rename to src/node/consumer/src/lib/repository/interfaces/skip_torrent_attributes.ts diff --git a/src/node/consumer/src/repository/interfaces/subtitle_attributes.ts b/src/node/consumer/src/lib/repository/interfaces/subtitle_attributes.ts similarity index 90% rename from src/node/consumer/src/repository/interfaces/subtitle_attributes.ts rename to src/node/consumer/src/lib/repository/interfaces/subtitle_attributes.ts index ce0ecb9..bef6538 100644 --- a/src/node/consumer/src/repository/interfaces/subtitle_attributes.ts +++ b/src/node/consumer/src/lib/repository/interfaces/subtitle_attributes.ts @@ -3,7 +3,7 @@ import {Optional} from "sequelize"; export interface ISubtitleAttributes { infoHash: string; fileIndex: number; - fileId?: number; + fileId?: number | null; title: string; path: string; } diff --git a/src/node/consumer/src/repository/interfaces/torrent_attributes.ts b/src/node/consumer/src/lib/repository/interfaces/torrent_attributes.ts similarity index 96% rename from src/node/consumer/src/repository/interfaces/torrent_attributes.ts rename to src/node/consumer/src/lib/repository/interfaces/torrent_attributes.ts index 3dfba91..64e4696 100644 --- a/src/node/consumer/src/repository/interfaces/torrent_attributes.ts +++ b/src/node/consumer/src/lib/repository/interfaces/torrent_attributes.ts @@ -1,11 +1,11 @@ import {Optional} from "sequelize"; import {IContentAttributes} from "./content_attributes"; -import {ISubtitleAttributes} from "./subtitle_attributes"; import {IFileAttributes} from "./file_attributes"; +import {ISubtitleAttributes} from "./subtitle_attributes"; export interface ITorrentAttributes { infoHash: string; - provider?: string; + provider?: string | null; torrentId?: string; title?: string; size?: number; diff --git a/src/node/consumer/src/repository/models/content.ts b/src/node/consumer/src/lib/repository/models/content.ts similarity index 97% rename from src/node/consumer/src/repository/models/content.ts rename to src/node/consumer/src/lib/repository/models/content.ts index 6a697ee..0209273 100644 --- a/src/node/consumer/src/repository/models/content.ts +++ b/src/node/consumer/src/lib/repository/models/content.ts @@ -18,5 +18,5 @@ export class Content extends Model Torrent, {constraints: false, foreignKey: 'infoHash'}) - torrent: Torrent; + torrent?: Torrent; } \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/file.ts b/src/node/consumer/src/lib/repository/models/file.ts similarity index 98% rename from src/node/consumer/src/repository/models/file.ts rename to src/node/consumer/src/lib/repository/models/file.ts index 94a0b0a..a9785c8 100644 --- a/src/node/consumer/src/repository/models/file.ts +++ b/src/node/consumer/src/lib/repository/models/file.ts @@ -1,7 +1,7 @@ import {BelongsTo, Column, DataType, ForeignKey, HasMany, Model, Table} from 'sequelize-typescript'; import {IFileAttributes, IFileCreationAttributes} from "../interfaces/file_attributes"; -import {Torrent} from "./torrent"; import {Subtitle} from "./subtitle"; +import {Torrent} from "./torrent"; const indexes = [ { @@ -55,5 +55,5 @@ export class File extends Model { declare subtitles?: Subtitle[]; @BelongsTo(() => Torrent, {constraints: false, foreignKey: 'infoHash'}) - torrent: Torrent; + torrent?: Torrent; } \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/ingestedPage.ts b/src/node/consumer/src/lib/repository/models/ingestedPage.ts similarity index 100% rename from src/node/consumer/src/repository/models/ingestedPage.ts rename to src/node/consumer/src/lib/repository/models/ingestedPage.ts diff --git a/src/node/consumer/src/repository/models/ingestedTorrent.ts b/src/node/consumer/src/lib/repository/models/ingestedTorrent.ts similarity index 100% rename from src/node/consumer/src/repository/models/ingestedTorrent.ts rename to src/node/consumer/src/lib/repository/models/ingestedTorrent.ts diff --git a/src/node/consumer/src/repository/models/provider.ts b/src/node/consumer/src/lib/repository/models/provider.ts similarity index 100% rename from src/node/consumer/src/repository/models/provider.ts rename to src/node/consumer/src/lib/repository/models/provider.ts diff --git a/src/node/consumer/src/repository/models/skipTorrent.ts b/src/node/consumer/src/lib/repository/models/skipTorrent.ts similarity index 100% rename from src/node/consumer/src/repository/models/skipTorrent.ts rename to src/node/consumer/src/lib/repository/models/skipTorrent.ts diff --git a/src/node/consumer/src/repository/models/subtitle.ts b/src/node/consumer/src/lib/repository/models/subtitle.ts similarity index 96% rename from src/node/consumer/src/repository/models/subtitle.ts rename to src/node/consumer/src/lib/repository/models/subtitle.ts index f8ffa1e..06fb142 100644 --- a/src/node/consumer/src/repository/models/subtitle.ts +++ b/src/node/consumer/src/lib/repository/models/subtitle.ts @@ -32,7 +32,7 @@ export class Subtitle extends Model File, {constraints: false, foreignKey: 'fileId'}) - file: File; + file?: File; - path: string; + path?: string; } \ No newline at end of file diff --git a/src/node/consumer/src/repository/models/torrent.ts b/src/node/consumer/src/lib/repository/models/torrent.ts similarity index 100% rename from src/node/consumer/src/repository/models/torrent.ts rename to src/node/consumer/src/lib/repository/models/torrent.ts diff --git a/src/node/consumer/src/lib/services/cache_service.ts b/src/node/consumer/src/lib/services/cache_service.ts index 1afae05..ab3b0a5 100644 --- a/src/node/consumer/src/lib/services/cache_service.ts +++ b/src/node/consumer/src/lib/services/cache_service.ts @@ -1,12 +1,12 @@ -import {Cache, createCache, MemoryCache, memoryStore, Store} from 'cache-manager'; import {mongoDbStore} from '@tirke/node-cache-manager-mongodb' -import {configurationService} from './configuration_service'; +import {Cache, createCache, MemoryCache, memoryStore} from 'cache-manager'; +import {inject, injectable} from "inversify"; import {CacheType} from "../enums/cache_types"; import {ICacheOptions} from "../interfaces/cache_options"; import {ICacheService} from "../interfaces/cache_service"; -import {inject, injectable} from "inversify"; -import {IocTypes} from "../models/ioc_types"; import {ILoggingService} from "../interfaces/logging_service"; +import {IocTypes} from "../models/ioc_types"; +import {configurationService} from './configuration_service'; const GLOBAL_KEY_PREFIX = 'knightcrawler-consumer'; const IMDB_ID_PREFIX = `${GLOBAL_KEY_PREFIX}|imdb_id`; @@ -18,13 +18,14 @@ const GLOBAL_TTL: number = Number(process.env.METADATA_TTL) || 7 * 24 * 60 * 60; const MEMORY_TTL: number = Number(process.env.METADATA_TTL) || 2 * 60 * 60; // 2 hours const TRACKERS_TTL: number = 2 * 24 * 60 * 60; // 2 days +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ export type CacheMethod = () => any; @injectable() export class CacheService implements ICacheService { private logger: ILoggingService; - private readonly memoryCache: MemoryCache; - private readonly remoteCache: Cache | MemoryCache; + private readonly memoryCache: MemoryCache | undefined; + private readonly remoteCache: Cache | MemoryCache | undefined; constructor(@inject(IocTypes.ILoggingService) logger: ILoggingService) { this.logger = logger; @@ -37,24 +38,24 @@ export class CacheService implements ICacheService { this.remoteCache = this.initiateRemoteCache(); } - public cacheWrapImdbId = (key: string, method: CacheMethod): Promise => + public cacheWrapImdbId = (key: string, method: CacheMethod): Promise => this.cacheWrap(CacheType.MongoDb, `${IMDB_ID_PREFIX}:${key}`, method, {ttl: GLOBAL_TTL}); - public cacheWrapKitsuId = (key: string, method: CacheMethod): Promise => + public cacheWrapKitsuId = (key: string, method: CacheMethod): Promise => this.cacheWrap(CacheType.MongoDb, `${KITSU_ID_PREFIX}:${key}`, method, {ttl: GLOBAL_TTL}); - public cacheWrapMetadata = (id: string, method: CacheMethod): Promise => + public cacheWrapMetadata = (id: string, method: CacheMethod): Promise => this.cacheWrap(CacheType.Memory, `${METADATA_PREFIX}:${id}`, method, {ttl: MEMORY_TTL}); - public cacheTrackers = (method: CacheMethod): Promise => + public cacheTrackers = (method: CacheMethod): Promise => this.cacheWrap(CacheType.Memory, `${TRACKERS_KEY_PREFIX}`, method, {ttl: TRACKERS_TTL}); - private initiateMemoryCache = () => + private initiateMemoryCache = (): MemoryCache => createCache(memoryStore(), { ttl: MEMORY_TTL }); - private initiateMongoCache = () => { + private initiateMongoCache = (): Cache => { const store = mongoDbStore({ collectionName: configurationService.cacheConfig.COLLECTION_NAME, ttl: GLOBAL_TTL, @@ -70,28 +71,28 @@ export class CacheService implements ICacheService { }); } - private initiateRemoteCache = (): Cache => { + private initiateRemoteCache = (): Cache | undefined => { if (configurationService.cacheConfig.NO_CACHE) { this.logger.debug('Cache is disabled'); - return null; + return undefined; } return configurationService.cacheConfig.MONGO_URI ? this.initiateMongoCache() : this.initiateMemoryCache(); } - private getCacheType = (cacheType: CacheType): MemoryCache | Cache => { + private getCacheType = (cacheType: CacheType): MemoryCache | Cache | undefined => { switch (cacheType) { case CacheType.Memory: return this.memoryCache; case CacheType.MongoDb: return this.remoteCache; default: - return null; + return undefined; } } private cacheWrap = async ( - cacheType: CacheType, key: string, method: CacheMethod, options: ICacheOptions): Promise => { + cacheType: CacheType, key: string, method: CacheMethod, options: ICacheOptions): Promise => { const cache = this.getCacheType(cacheType); if (configurationService.cacheConfig.NO_CACHE || !cache) { diff --git a/src/node/consumer/src/lib/services/configuration_service.ts b/src/node/consumer/src/lib/services/configuration_service.ts index 32b56ca..8955dc9 100644 --- a/src/node/consumer/src/lib/services/configuration_service.ts +++ b/src/node/consumer/src/lib/services/configuration_service.ts @@ -1,10 +1,10 @@ -import {rabbitConfig} from "../models/configuration/rabbit_config"; -import {cacheConfig} from "../models/configuration/cache_config"; +import {cacheConfig} from "../models/configuration/cache_config"; import {databaseConfig} from "../models/configuration/database_config"; import {jobConfig} from "../models/configuration/job_config"; import {metadataConfig} from "../models/configuration/metadata_config"; -import {trackerConfig} from "../models/configuration/tracker_config"; +import {rabbitConfig} from "../models/configuration/rabbit_config"; import {torrentConfig} from "../models/configuration/torrent_config"; +import {trackerConfig} from "../models/configuration/tracker_config"; export const configurationService = { rabbitConfig: rabbitConfig, diff --git a/src/node/consumer/src/lib/services/logging_service.ts b/src/node/consumer/src/lib/services/logging_service.ts index ddc4dee..ee703ba 100644 --- a/src/node/consumer/src/lib/services/logging_service.ts +++ b/src/node/consumer/src/lib/services/logging_service.ts @@ -1,7 +1,8 @@ +import {injectable} from "inversify"; import {Logger, pino} from "pino"; import {ILoggingService} from "../interfaces/logging_service"; -import {injectable} from "inversify"; +/* eslint-disable @typescript-eslint/no-explicit-any */ @injectable() export class LoggingService implements ILoggingService { private readonly logger: Logger; @@ -27,4 +28,5 @@ export class LoggingService implements ILoggingService { public warn = (message: string, ...args: any[]): void => { this.logger.warn(message, args); }; -} \ No newline at end of file +} +/* eslint-enable @typescript-eslint/no-explicit-any */ \ No newline at end of file diff --git a/src/node/consumer/src/lib/services/metadata_service.ts b/src/node/consumer/src/lib/services/metadata_service.ts index f684d8d..1015fb1 100644 --- a/src/node/consumer/src/lib/services/metadata_service.ts +++ b/src/node/consumer/src/lib/services/metadata_service.ts @@ -1,17 +1,20 @@ + import axios, {AxiosResponse} from 'axios'; import {ResultTypes, search} from 'google-sr'; +import {inject, injectable} from "inversify"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error import nameToImdb from 'name-to-imdb'; import {TorrentType} from '../enums/torrent_types'; -import {IMetadataResponse} from "../interfaces/metadata_response"; +import {ICacheService} from "../interfaces/cache_service"; import {ICinemetaJsonResponse} from "../interfaces/cinemeta_metadata"; import {ICommonVideoMetadata} from "../interfaces/common_video_metadata"; +import {IKitsuCatalogJsonResponse} from "../interfaces/kitsu_catalog_metadata"; import {IKitsuJsonResponse} from "../interfaces/kitsu_metadata"; import {IMetaDataQuery} from "../interfaces/metadata_query"; -import {IKitsuCatalogJsonResponse} from "../interfaces/kitsu_catalog_metadata"; +import {IMetadataResponse} from "../interfaces/metadata_response"; import {IMetadataService} from "../interfaces/metadata_service"; -import {inject, injectable} from "inversify"; import {IocTypes} from "../models/ioc_types"; -import {ICacheService} from "../interfaces/cache_service"; const CINEMETA_URL = 'https://v3-cinemeta.strem.io'; const KITSU_URL = 'https://anime-kitsu.strem.fun'; @@ -25,10 +28,10 @@ export class MetadataService implements IMetadataService { this.cacheService = cacheService; } - public getKitsuId = async (info: IMetaDataQuery): Promise => { - const title = this.escapeTitle(info.title.replace(/\s\|\s.*/, '')); + public getKitsuId = async (info: IMetaDataQuery): Promise => { + const title = this.escapeTitle(info.title!.replace(/\s\|\s.*/, '')); const year = info.year ? ` ${info.year}` : ''; - const season = info.season > 1 ? ` S${info.season}` : ''; + const season = info.season || 0 > 1 ? ` S${info.season}` : ''; const key = `${title}${year}${season}`; const query = encodeURIComponent(key); @@ -45,7 +48,7 @@ export class MetadataService implements IMetadataService { }; public getImdbId = async (info: IMetaDataQuery): Promise => { - const name = this.escapeTitle(info.title); + const name = this.escapeTitle(info.title!); const year = info.year || (info.date && info.date.slice(0, 4)); const key = `${name}_${year || 'NA'}_${info.type}`; const query = `${name} ${year || ''} ${info.type} imdb`; @@ -102,7 +105,7 @@ export class MetadataService implements IMetadataService { .trim(); private requestMetadata = async (url: string): Promise => { - let response: AxiosResponse = await axios.get(url, {timeout: TIMEOUT}); + const response: AxiosResponse = await axios.get(url, {timeout: TIMEOUT}); let result: IMetadataResponse; const body = response.data; if ('kitsu_id' in body.meta) { @@ -117,14 +120,14 @@ export class MetadataService implements IMetadataService { }; private handleCinemetaResponse = (body: ICinemetaJsonResponse): IMetadataResponse => ({ - imdbId: parseInt(body.meta.imdb_id), - type: body.meta.type, - title: body.meta.name, - year: parseInt(body.meta.year), - country: body.meta.country, - genres: body.meta.genres, - status: body.meta.status, - videos: body.meta.videos + imdbId: parseInt(body.meta?.imdb_id || '0'), + type: body.meta?.type, + title: body.meta?.name, + year: parseInt(body.meta?.year || '0'), + country: body.meta?.country, + genres: body.meta?.genres, + status: body.meta?.status, + videos: body.meta?.videos ? body.meta.videos.map(video => ({ name: video.name, season: video.season, @@ -133,10 +136,10 @@ export class MetadataService implements IMetadataService { imdbEpisode: video.episode, })) : [], - episodeCount: body.meta.videos + episodeCount: body.meta?.videos ? this.getEpisodeCount(body.meta.videos) : [], - totalCount: body.meta.videos + totalCount: body.meta?.videos ? body.meta.videos.filter( entry => entry.season !== 0 && entry.episode !== 0 ).length @@ -144,15 +147,15 @@ export class MetadataService implements IMetadataService { }); private handleKitsuResponse = (body: IKitsuJsonResponse): IMetadataResponse => ({ - kitsuId: parseInt(body.meta.kitsu_id), - type: body.meta.type, - title: body.meta.name, - year: parseInt(body.meta.year), - country: body.meta.country, - genres: body.meta.genres, - status: body.meta.status, - videos: body.meta.videos - ? body.meta.videos.map(video => ({ + kitsuId: parseInt(body.meta?.kitsu_id || '0'), + type: body.meta?.type, + title: body.meta?.name, + year: parseInt(body.meta?.year || '0'), + country: body.meta?.country, + genres: body.meta?.genres, + status: body.meta?.status, + videos: body.meta?.videos + ? body.meta?.videos.map(video => ({ name: video.title, season: video.season, episode: video.episode, @@ -161,29 +164,32 @@ export class MetadataService implements IMetadataService { released: video.released, })) : [], - episodeCount: body.meta.videos + episodeCount: body.meta?.videos ? this.getEpisodeCount(body.meta.videos) : [], - totalCount: body.meta.videos + totalCount: body.meta?.videos ? body.meta.videos.filter( entry => entry.season !== 0 && entry.episode !== 0 ).length : 0, }); - private getEpisodeCount = (videos: ICommonVideoMetadata[]) => Object.values( - videos - .filter(entry => entry.season !== 0 && entry.episode !== 0) - .sort((a, b) => a.season - b.season) - .reduce((map, next) => { - map[next.season] = map[next.season] + 1 || 1; - return map; - }, {}) - ); + private getEpisodeCount = (videos: ICommonVideoMetadata[]): number[] => + Object.values( + videos + .filter(entry => entry.season !== null && entry.season !== 0 && entry.episode !== 0) + .sort((a, b) => (a.season || 0) - (b.season || 0)) + .reduce((map: Record, next) => { + if(next.season || next.season === 0) { + map[next.season] = (map[next.season] || 0) + 1; + } + return map; + }, {}) + ); private getIMDbIdFromNameToImdb = (name: string, info: IMetaDataQuery): Promise => { - const year = info.year; - const type = info.type; + const {year} = info; + const {type} = info; return new Promise((resolve, reject) => { nameToImdb({name, year, type}, function (err: Error, res: string) { if (res) { diff --git a/src/node/consumer/src/lib/services/torrent_download_service.ts b/src/node/consumer/src/lib/services/torrent_download_service.ts index 6201940..03a1719 100644 --- a/src/node/consumer/src/lib/services/torrent_download_service.ts +++ b/src/node/consumer/src/lib/services/torrent_download_service.ts @@ -1,17 +1,17 @@ -import {encode} from 'magnet-uri'; -import {configurationService} from './configuration_service'; -import {ExtensionHelpers} from '../helpers/extension_helpers'; -import {ITorrentFileCollection} from "../interfaces/torrent_file_collection"; -import {IParsedTorrent} from "../interfaces/parsed_torrent"; -import {IFileAttributes} from "../../repository/interfaces/file_attributes"; -import {ISubtitleAttributes} from "../../repository/interfaces/subtitle_attributes"; -import {IContentAttributes} from "../../repository/interfaces/content_attributes"; -import {parse} from "parse-torrent-title"; -import {ITorrentDownloadService} from "../interfaces/torrent_download_service"; import {inject, injectable} from "inversify"; -import {ILoggingService} from "../interfaces/logging_service"; -import {IocTypes} from "../models/ioc_types"; +import {encode} from 'magnet-uri'; +import {parse} from "parse-torrent-title"; import WebTorrent from "webtorrent"; +import {ExtensionHelpers} from '../helpers/extension_helpers'; +import {ILoggingService} from "../interfaces/logging_service"; +import {IParsedTorrent} from "../interfaces/parsed_torrent"; +import {ITorrentDownloadService} from "../interfaces/torrent_download_service"; +import {ITorrentFileCollection} from "../interfaces/torrent_file_collection"; +import {IocTypes} from "../models/ioc_types"; +import {IContentAttributes} from "../repository/interfaces/content_attributes"; +import {IFileAttributes} from "../repository/interfaces/file_attributes"; +import {ISubtitleAttributes} from "../repository/interfaces/subtitle_attributes"; +import {configurationService} from './configuration_service'; interface ITorrentFile { name: string; @@ -20,13 +20,13 @@ interface ITorrentFile { fileIndex: number; } -const clientOptions = { +const clientOptions : WebTorrent.Options = { maxConns: configurationService.torrentConfig.MAX_CONNECTIONS_OVERALL, + utp: false, } -const torrentOptions = { +const torrentOptions: WebTorrent.TorrentOptions = { skipVerify: true, - addUID: true, destroyStoreOnDestroy: true, private: true, maxWebConns: configurationService.torrentConfig.MAX_CONNECTIONS_PER_TORRENT, @@ -61,23 +61,20 @@ export class TorrentDownloadService implements ITorrentDownloadService { if (!torrent.infoHash) { return Promise.reject(new Error("No infoHash...")); } - const magnet = encode({infoHash: torrent.infoHash, announce: torrent.trackers.split(',')}); - - this.logger.debug(`Constructing torrent stream for ${torrent.title} with magnet ${magnet}`); + const magnet = encode({infoHash: torrent.infoHash, announce: torrent.trackers!.split(',')}); return new Promise((resolve, reject) => { + this.logger.debug(`Adding torrent with infoHash ${torrent.infoHash} to webtorrent client...`); + + const currentTorrent = this.torrentClient.add(magnet, torrentOptions); + const timeoutId = setTimeout(() => { - this.torrentClient.remove(magnet, {destroyStore: true}); + this.removeTorrent(currentTorrent, torrent); reject(new Error('No available connections for torrent!')); }, timeout); - - this.logger.debug(`Adding torrent with infoHash ${torrent.infoHash}`); - this.torrentClient.add(magnet, torrentOptions, (torrent) => { - - this.logger.debug(`torrent with infoHash ${torrent.infoHash} added to client.`); - - const files: ITorrentFile[] = torrent.files.map((file, fileId) => ({ + currentTorrent.on('ready', () => { + const files: ITorrentFile[] = currentTorrent.files.map((file, fileId) => ({ fileIndex: fileId, length: file.length, name: file.name, @@ -87,13 +84,22 @@ export class TorrentDownloadService implements ITorrentDownloadService { this.logger.debug(`Found ${files.length} files in torrent ${torrent.infoHash}`); resolve(files); - - this.torrentClient.remove(magnet, {destroyStore: true}); clearTimeout(timeoutId); + this.removeTorrent(currentTorrent, torrent); }); }); }; + private removeTorrent = (currentTorrent: WebTorrent.Torrent, torrent: IParsedTorrent): void => { + try { + this.torrentClient.remove(currentTorrent, {destroyStore: true}, () => { + this.logger.debug(`Removed torrent ${torrent.infoHash} from webtorrent client...`); + }); + } catch (error) { + this.logClientErrors(error); + } + }; + private filterVideos = (torrent: IParsedTorrent, torrentFiles: ITorrentFile[]): IFileAttributes[] => { if (torrentFiles.length === 1 && !Number.isInteger(torrentFiles[0].fileIndex)) { return [this.mapTorrentFileToFileAttributes(torrent, torrentFiles[0])]; @@ -104,13 +110,25 @@ export class TorrentDownloadService implements ITorrentDownloadService { const minAnimeExtraRatio = 5; const minRedundantRatio = videos.length <= 3 ? 30 : Number.MAX_VALUE; - const isSample = (video: ITorrentFile) => video.path.toString()?.match(/sample|bonus|promo/i) && maxSize / video.length > minSampleRatio; - const isRedundant = (video: ITorrentFile) => maxSize / video.length > minRedundantRatio; - const isExtra = (video: ITorrentFile) => video.path.toString()?.match(/extras?\//i); - const isAnimeExtra = (video: ITorrentFile) => video.path.toString()?.match(/(?:\b|_)(?:NC)?(?:ED|OP|PV)(?:v?\d\d?)?(?:\b|_)/i) - && maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio; - const isWatermark = (video: ITorrentFile) => video.path.toString()?.match(/^[A-Z-]+(?:\.[A-Z]+)?\.\w{3,4}$/) - && maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio + const isSample = (video: ITorrentFile): boolean => video.path?.toString()?.match(/sample|bonus|promo/i) && maxSize / video.length > minSampleRatio || false; + const isRedundant = (video: ITorrentFile):boolean => maxSize / video.length > minRedundantRatio; + const isExtra = (video: ITorrentFile): boolean => /extras?\//i.test(video.path?.toString() || ""); + const isAnimeExtra = (video: ITorrentFile): boolean => { + if (!video.path || !video.length) { + return false; + } + + return video.path.toString()?.match(/(?:\b|_)(?:NC)?(?:ED|OP|PV)(?:v?\d\d?)?(?:\b|_)/i) + && maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio || false; + }; + const isWatermark = (video: ITorrentFile): boolean => { + if (!video.path || !video.length) { + return false; + } + + return video.path.toString()?.match(/^[A-Z-]+(?:\.[A-Z]+)?\.\w{3,4}$/) + && maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio || false; + } return videos .filter(video => !isSample(video)) @@ -133,11 +151,11 @@ export class TorrentDownloadService implements ITorrentDownloadService { size: file.length, fileIndex: file.fileIndex || 0, path: file.path, - infoHash: torrent.infoHash, - imdbId: torrent.imdbId.toString(), + infoHash: torrent.infoHash?.toString(), + imdbId: torrent.imdbId?.toString() || '', imdbSeason: torrent.season || 0, imdbEpisode: torrent.episode || 0, - kitsuId: parseInt(torrent.kitsuId?.toString()) || 0, + kitsuId: parseInt(torrent.kitsuId?.toString() || '0') || 0, kitsuEpisode: torrent.episode || 0, }; @@ -162,8 +180,8 @@ export class TorrentDownloadService implements ITorrentDownloadService { size: file.length, }); - private logClientErrors(errors: Error | string) { - this.logger.error(`Error in torrent client: ${errors}`); + private logClientErrors(errors: Error | string | unknown): void { + this.logger.error(`Error in webtorrent client: ${errors}`); } } diff --git a/src/node/consumer/src/lib/services/torrent_entries_service.ts b/src/node/consumer/src/lib/services/torrent_entries_service.ts index 93355b6..141bf7c 100644 --- a/src/node/consumer/src/lib/services/torrent_entries_service.ts +++ b/src/node/consumer/src/lib/services/torrent_entries_service.ts @@ -1,22 +1,24 @@ -import {parse} from 'parse-torrent-title'; -import {IParsedTorrent} from "../interfaces/parsed_torrent"; -import {TorrentType} from '../enums/torrent_types'; -import {ITorrentFileCollection} from "../interfaces/torrent_file_collection"; -import {Torrent} from "../../repository/models/torrent"; -import {PromiseHelpers} from '../helpers/promises_helpers'; -import {ITorrentAttributes, ITorrentCreationAttributes} from "../../repository/interfaces/torrent_attributes"; -import {File} from "../../repository/models/file"; -import {Subtitle} from "../../repository/models/subtitle"; -import {ITorrentEntriesService} from "../interfaces/torrent_entries_service"; import {inject, injectable} from "inversify"; -import {IocTypes} from "../models/ioc_types"; -import {IMetadataService} from "../interfaces/metadata_service"; +import {parse} from 'parse-torrent-title'; +import {TorrentType} from '../enums/torrent_types'; +import {PromiseHelpers} from '../helpers/promises_helpers'; import {ILoggingService} from "../interfaces/logging_service"; +import {IMetaDataQuery} from "../interfaces/metadata_query"; +import {IMetadataService} from "../interfaces/metadata_service"; +import {IParsedTorrent} from "../interfaces/parsed_torrent"; +import {ITorrentEntriesService} from "../interfaces/torrent_entries_service"; +import {ITorrentFileCollection} from "../interfaces/torrent_file_collection"; import {ITorrentFileService} from "../interfaces/torrent_file_service"; import {ITorrentSubtitleService} from "../interfaces/torrent_subtitle_service"; -import {IDatabaseRepository} from "../../repository/interfaces/database_repository"; -import {IIngestedTorrentCreationAttributes} from "../../repository/interfaces/ingested_torrent_attributes"; -import {IFileCreationAttributes} from "../../repository/interfaces/file_attributes"; +import {IocTypes} from "../models/ioc_types"; +import {IDatabaseRepository} from "../repository/interfaces/database_repository"; +import {IFileCreationAttributes} from "../repository/interfaces/file_attributes"; +import {ISubtitleAttributes} from "../repository/interfaces/subtitle_attributes"; +import {ITorrentAttributes, ITorrentCreationAttributes} from "../repository/interfaces/torrent_attributes"; +import {File} from "../repository/models/file"; +import {SkipTorrent} from "../repository/models/skipTorrent"; +import {Subtitle} from "../repository/models/subtitle"; +import {Torrent} from "../repository/models/torrent"; @injectable() export class TorrentEntriesService implements ITorrentEntriesService { @@ -39,6 +41,11 @@ export class TorrentEntriesService implements ITorrentEntriesService { } public createTorrentEntry = async (torrent: IParsedTorrent, overwrite = false): Promise => { + if (!torrent.title) { + this.logger.warn(`No title found for ${torrent.provider} [${torrent.infoHash}]`); + return; + } + const titleInfo = parse(torrent.title); if (!torrent.imdbId && torrent.type !== TorrentType.Anime) { @@ -93,7 +100,7 @@ export class TorrentEntriesService implements ITorrentEntriesService { }); return this.repository.createTorrent(newTorrent) - .then(() => PromiseHelpers.sequence(fileCollection.videos.map(video => () => { + .then(() => PromiseHelpers.sequence(fileCollection.videos!.map(video => () => { const newVideo: IFileCreationAttributes = {...video, infoHash: video.infoHash, title: video.title}; if (!newVideo.kitsuId) { newVideo.kitsuId = 0; @@ -103,7 +110,7 @@ export class TorrentEntriesService implements ITorrentEntriesService { .then(() => this.logger.info(`Created ${torrent.provider} entry for [${torrent.infoHash}] ${torrent.title}`)); }; - private assignKitsuId = async (kitsuQuery: { year: number | string; season: number; title: string }, torrent: IParsedTorrent) => { + private assignKitsuId = async (kitsuQuery: IMetaDataQuery, torrent: IParsedTorrent): Promise => { await this.metadataService.getKitsuId(kitsuQuery) .then((result: number | Error) => { if (typeof result === 'number') { @@ -118,10 +125,10 @@ export class TorrentEntriesService implements ITorrentEntriesService { }); }; - public createSkipTorrentEntry = async (torrent: Torrent) => this.repository.createSkipTorrent(torrent); + public createSkipTorrentEntry: (torrent: Torrent) => Promise<[SkipTorrent, boolean | null]> = async (torrent: Torrent)=> this.repository.createSkipTorrent(torrent.dataValues); - public getStoredTorrentEntry = async (torrent: Torrent) => this.repository.getSkipTorrent(torrent.infoHash) - .catch(() => this.repository.getTorrent(torrent)) + public getStoredTorrentEntry = async (torrent: Torrent): Promise => this.repository.getSkipTorrent(torrent.infoHash) + .catch(() => this.repository.getTorrent(torrent.dataValues)) .catch(() => undefined); public checkAndUpdateTorrent = async (torrent: IParsedTorrent): Promise => { @@ -141,7 +148,7 @@ export class TorrentEntriesService implements ITorrentEntriesService { } if (existingTorrent.provider === 'KickassTorrents' && torrent.provider) { existingTorrent.provider = torrent.provider; - existingTorrent.torrentId = torrent.torrentId; + existingTorrent.torrentId = torrent.torrentId!; } if (!existingTorrent.languages && torrent.languages && existingTorrent.provider !== 'RARBG') { @@ -149,11 +156,14 @@ export class TorrentEntriesService implements ITorrentEntriesService { await existingTorrent.save(); this.logger.debug(`Updated [${existingTorrent.infoHash}] ${existingTorrent.title} language to ${torrent.languages}`); } + return this.createTorrentContents(existingTorrent) - .then(() => this.updateTorrentSeeders(existingTorrent)); + .then(() => this.updateTorrentSeeders(existingTorrent.dataValues)) + .then(() => Promise.resolve(true)) + .catch(() => Promise.reject(false)); }; - public createTorrentContents = async (torrent: Torrent) => { + public createTorrentContents = async (torrent: Torrent): Promise => { if (torrent.opened) { return; } @@ -167,7 +177,7 @@ export class TorrentEntriesService implements ITorrentEntriesService { const kitsuId: number = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.kitsuId || 0)); const fileCollection: ITorrentFileCollection = await this.fileService.parseTorrentFiles(torrent) - .then(torrentContents => notOpenedVideo ? torrentContents : {...torrentContents, videos: storedVideos}) + .then(torrentContents => notOpenedVideo ? torrentContents : {...torrentContents, videos: storedVideos.map(video => video.dataValues)}) .then(torrentContents => this.subtitleService.assignSubtitles(torrentContents)) .then(torrentContents => this.assignMetaIds(torrentContents, imdbId, kitsuId)) .catch(error => { @@ -181,22 +191,24 @@ export class TorrentEntriesService implements ITorrentEntriesService { return; } - if (notOpenedVideo && fileCollection.videos.length === 1) { + if (notOpenedVideo && fileCollection.videos?.length === 1) { // if both have a single video and stored one was not opened, update stored one to true metadata and use that - storedVideos[0].fileIndex = fileCollection.videos[0].fileIndex; + storedVideos[0].fileIndex = fileCollection?.videos[0]?.fileIndex || 0; storedVideos[0].title = fileCollection.videos[0].title; - storedVideos[0].size = fileCollection.videos[0].size; - storedVideos[0].subtitles = fileCollection.videos[0].subtitles.map(subtitle => Subtitle.build(subtitle)); - fileCollection.videos[0] = storedVideos[0]; + storedVideos[0].size = fileCollection.videos[0].size || 0; + const subtitles: ISubtitleAttributes[] = fileCollection.videos[0]?.subtitles || []; + storedVideos[0].subtitles = subtitles.map(subtitle => Subtitle.build(subtitle)); + fileCollection.videos[0] = {...storedVideos[0], subtitles: subtitles}; } // no videos available or more than one new videos were in the torrent - const shouldDeleteOld = notOpenedVideo && fileCollection.videos.every(video => !video.id); + const shouldDeleteOld = notOpenedVideo && fileCollection.videos?.every(video => !video.id) || false; - const newTorrent: Torrent = Torrent.build({ + const newTorrent: ITorrentCreationAttributes = { ...torrent, + files: fileCollection.videos, contents: fileCollection.contents, subtitles: fileCollection.subtitles - }); + }; return this.repository.createTorrent(newTorrent) .then(() => { @@ -206,7 +218,7 @@ export class TorrentEntriesService implements ITorrentEntriesService { } return Promise.resolve(); }) - .then(() => PromiseHelpers.sequence(fileCollection.videos.map(video => () => { + .then(() => PromiseHelpers.sequence(fileCollection.videos!.map(video => (): Promise => { const newVideo: IFileCreationAttributes = {...video, infoHash: video.infoHash, title: video.title}; return this.repository.createFile(newVideo) }))) @@ -214,19 +226,25 @@ export class TorrentEntriesService implements ITorrentEntriesService { .catch(error => this.logger.error(`Failed saving contents for [${torrent.infoHash}] ${torrent.title}`, error)); }; - public updateTorrentSeeders = async (torrent: ITorrentAttributes) => { + public updateTorrentSeeders = async (torrent: ITorrentAttributes): Promise<[number]> => { if (!(torrent.infoHash || (torrent.provider && torrent.torrentId)) || !Number.isInteger(torrent.seeders)) { - return torrent; + return [0]; } - + + if (torrent.seeders === undefined) { + this.logger.warn(`Seeders not found for ${torrent.provider} [${torrent.infoHash}] ${torrent.title}`); + return [0]; + } + + return this.repository.setTorrentSeeders(torrent, torrent.seeders) .catch(error => { this.logger.warn('Failed updating seeders:', error); - return undefined; + return [0]; }); }; - private assignMetaIds = (fileCollection: ITorrentFileCollection, imdbId: string, kitsuId: number): ITorrentFileCollection => { + private assignMetaIds = (fileCollection: ITorrentFileCollection, imdbId: string | undefined, kitsuId: number): ITorrentFileCollection => { if (fileCollection.videos && fileCollection.videos.length) { fileCollection.videos.forEach(video => { video.imdbId = imdbId || ''; @@ -237,26 +255,31 @@ export class TorrentEntriesService implements ITorrentEntriesService { return fileCollection; }; - private overwriteExistingFiles = async (torrent: IParsedTorrent, torrentContents: ITorrentFileCollection) => { + private overwriteExistingFiles = async (torrent: IParsedTorrent, torrentContents: ITorrentFileCollection): Promise => { const videos = torrentContents && torrentContents.videos; if (videos && videos.length) { const existingFiles = await this.repository.getFiles(torrent.infoHash) - .then((existing) => existing - .reduce((map, next) => { - const fileIndex = next.fileIndex !== undefined ? next.fileIndex : null; + .then((existing) => existing.reduce<{ [key: number]: File[] }>((map, next) => { + const fileIndex = next.fileIndex !== undefined ? next.fileIndex : null; + if (fileIndex !== null) { map[fileIndex] = (map[fileIndex] || []).concat(next); - return map; - }, {})) + } + return map; + }, {})) .catch(() => undefined); if (existingFiles && Object.keys(existingFiles).length) { const overwrittenVideos = videos .map(file => { - const mapping = videos.length === 1 && Object.keys(existingFiles).length === 1 - ? Object.values(existingFiles)[0] - : existingFiles[file.fileIndex !== undefined ? file.fileIndex : null]; + const index = file.fileIndex !== undefined ? file.fileIndex : null; + let mapping; + if (index !== null) { + mapping = videos.length === 1 && Object.keys(existingFiles).length === 1 + ? Object.values(existingFiles)[0] + : existingFiles[index]; + } if (mapping) { const originalFile = mapping.shift(); - return {id: originalFile.id, ...file}; + return {id: originalFile!.id, ...file}; } return file; }); diff --git a/src/node/consumer/src/lib/services/torrent_file_service.ts b/src/node/consumer/src/lib/services/torrent_file_service.ts index d98f128..59a3360 100644 --- a/src/node/consumer/src/lib/services/torrent_file_service.ts +++ b/src/node/consumer/src/lib/services/torrent_file_service.ts @@ -1,23 +1,24 @@ import Bottleneck from 'bottleneck'; +import {inject, injectable} from "inversify"; import moment from 'moment'; import {parse} from 'parse-torrent-title'; -import {PromiseHelpers} from '../helpers/promises_helpers'; import {TorrentType} from '../enums/torrent_types'; -import {configurationService} from './configuration_service'; import {ExtensionHelpers} from '../helpers/extension_helpers'; -import {IMetadataResponse} from "../interfaces/metadata_response"; -import {IMetaDataQuery} from "../interfaces/metadata_query"; +import {PromiseHelpers} from '../helpers/promises_helpers'; import {ICommonVideoMetadata} from "../interfaces/common_video_metadata"; -import {ITorrentFileCollection} from "../interfaces/torrent_file_collection"; -import {IParsedTorrent} from "../interfaces/parsed_torrent"; -import {IFileAttributes} from "../../repository/interfaces/file_attributes"; -import {IContentAttributes} from "../../repository/interfaces/content_attributes"; -import {ITorrentFileService} from "../interfaces/torrent_file_service"; -import {inject, injectable} from "inversify"; -import {IocTypes} from "../models/ioc_types"; -import {IMetadataService} from "../interfaces/metadata_service"; -import {ITorrentDownloadService} from "../interfaces/torrent_download_service"; import {ILoggingService} from "../interfaces/logging_service"; +import {IMetaDataQuery} from "../interfaces/metadata_query"; +import {IMetadataResponse} from "../interfaces/metadata_response"; +import {IMetadataService} from "../interfaces/metadata_service"; +import {IParsedTorrent} from "../interfaces/parsed_torrent"; +import {ISeasonEpisodeMap} from "../interfaces/season_episode_map"; +import {ITorrentDownloadService} from "../interfaces/torrent_download_service"; +import {ITorrentFileCollection} from "../interfaces/torrent_file_collection"; +import {ITorrentFileService} from "../interfaces/torrent_file_service"; +import {IocTypes} from "../models/ioc_types"; +import {IContentAttributes} from "../repository/interfaces/content_attributes"; +import {IFileAttributes} from "../repository/interfaces/file_attributes"; +import {configurationService} from './configuration_service'; const MIN_SIZE: number = 5 * 1024 * 1024; // 5 MB const MULTIPLE_FILES_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB @@ -41,6 +42,10 @@ export class TorrentFileService implements ITorrentFileService { } public parseTorrentFiles = async (torrent: IParsedTorrent): Promise => { + if (!torrent.title) { + return Promise.reject(new Error('Torrent title is missing')); + } + const parsedTorrentName = parse(torrent.title); const query: IMetaDataQuery = { id: torrent.kitsuId || torrent.imdbId, @@ -49,6 +54,12 @@ export class TorrentFileService implements ITorrentFileService { const metadata = await this.metadataService.getMetadata(query) .then(meta => Object.assign({}, meta)) .catch(() => undefined); + + if (metadata === undefined || metadata instanceof Error) { + this.logger.warn(`Failed to retrieve metadata for torrent ${torrent.title}`); + this.logger.debug(`Metadata Error: ${torrent.title}`, metadata); + return Promise.reject(new Error('Failed to retrieve metadata')); + } if (torrent.type !== TorrentType.Anime && metadata && metadata.type && metadata.type !== torrent.type) { // it's actually a movie/series @@ -56,7 +67,7 @@ export class TorrentFileService implements ITorrentFileService { } if (torrent.type === TorrentType.Movie && (!parsedTorrentName.seasons || - parsedTorrentName.season === 5 && [1, 5].includes(parsedTorrentName.episode))) { + parsedTorrentName.season === 5 && [1, 5].includes(parsedTorrentName.episode || 0))) { return this.parseMovieFiles(torrent, metadata); } @@ -67,22 +78,27 @@ export class TorrentFileService implements ITorrentFileService { if (torrent.isPack) { return true; } + if (!torrent.title) { + return false; + } const parsedInfo = parse(torrent.title); if (torrent.type === TorrentType.Movie) { return parsedInfo.complete || typeof parsedInfo.year === 'string' || /movies/i.test(torrent.title); } - const hasMultipleEpisodes = parsedInfo.complete || - torrent.size > MULTIPLE_FILES_SIZE || + + const hasMultipleEpisodes = Boolean(parsedInfo.complete || torrent.size || 0 > MULTIPLE_FILES_SIZE || (parsedInfo.seasons && parsedInfo.seasons.length > 1) || (parsedInfo.episodes && parsedInfo.episodes.length > 1) || - (parsedInfo.seasons && !parsedInfo.episodes); - const hasSingleEpisode = Number.isInteger(parsedInfo.episode) || (!parsedInfo.episodes && parsedInfo.date); + (parsedInfo.seasons && !parsedInfo.episodes)); + + const hasSingleEpisode: boolean = Boolean(Number.isInteger(parsedInfo.episode) || (!parsedInfo.episodes && parsedInfo.date)); + return hasMultipleEpisodes && !hasSingleEpisode; }; private parseSeriesVideos = (torrent: IParsedTorrent, videos: IFileAttributes[]): IFileAttributes[] => { - const parsedTorrentName = parse(torrent.title); - const hasMovies = parsedTorrentName.complete || !!torrent.title.match(/movies?(?:\W|$)/i); + const parsedTorrentName = parse(torrent.title!); + const hasMovies = parsedTorrentName.complete || !!torrent.title!.match(/movies?(?:\W|$)/i); const parsedVideos = videos.map(video => this.parseSeriesVideo(video)); return parsedVideos.map(video => ({...video, isMovie: this.isMovieVideo(torrent, video, parsedVideos, hasMovies)})); @@ -90,28 +106,32 @@ export class TorrentFileService implements ITorrentFileService { private parseMovieFiles = async (torrent: IParsedTorrent, metadata: IMetadataResponse): Promise => { const fileCollection: ITorrentFileCollection = await this.getMoviesTorrentContent(torrent); + if (fileCollection.videos === undefined || fileCollection.videos.length === 0) { + return {...fileCollection, videos: this.getDefaultFileEntries(torrent)}; + } + const filteredVideos = fileCollection.videos - .filter(video => video.size > MIN_SIZE) + .filter(video => video.size! > MIN_SIZE) .filter(video => !this.isFeaturette(video)); if (this.isSingleMovie(filteredVideos)) { const parsedVideos = filteredVideos.map(video => ({ infoHash: torrent.infoHash, fileIndex: video.fileIndex, - title: video.path || torrent.title, + title: video.path || video.title || video.fileName || '', size: video.size || torrent.size, imdbId: torrent.imdbId?.toString() || metadata && metadata.imdbId?.toString(), - kitsuId: parseInt(torrent.kitsuId?.toString() || metadata && metadata.kitsuId?.toString()) + kitsuId: parseInt(torrent.kitsuId?.toString() || metadata && metadata.kitsuId?.toString() || '0') })); return {...fileCollection, videos: parsedVideos}; } const parsedVideos = await PromiseHelpers.sequence(filteredVideos.map(video => () => this.isFeaturette(video) ? Promise.resolve(video) - : this.findMovieImdbId(video.title).then(imdbId => ({...video, imdbId})))) - .then(videos => videos.map(video => ({ + : this.findMovieImdbId(video.title).then(imdbId => ({...video, imdbId: imdbId?.toString() || ''})))) + .then(videos => videos.map((video: IFileAttributes) => ({ infoHash: torrent.infoHash, fileIndex: video.fileIndex, - title: video.path || video.name, + title: video.path || video.title, size: video.size, imdbId: video.imdbId, }))); @@ -120,8 +140,12 @@ export class TorrentFileService implements ITorrentFileService { private parseSeriesFiles = async (torrent: IParsedTorrent, metadata: IMetadataResponse): Promise => { const fileCollection: ITorrentFileCollection = await this.getSeriesTorrentContent(torrent); + if (fileCollection.videos === undefined || fileCollection.videos.length === 0) { + return {...fileCollection, videos: this.getDefaultFileEntries(torrent)}; + } + const parsedVideos: IFileAttributes[] = await Promise.resolve(fileCollection.videos) - .then(videos => videos.filter(video => videos.length === 1 || video.size > MIN_SIZE)) + .then(videos => videos.filter(video => videos?.length === 1 || video.size! > MIN_SIZE)) .then(videos => this.parseSeriesVideos(torrent, videos)) .then(videos => this.decomposeEpisodes(torrent, videos, metadata)) .then(videos => this.assignKitsuOrImdbEpisodes(torrent, videos, metadata)) @@ -138,23 +162,24 @@ export class TorrentFileService implements ITorrentFileService { const files = await this.torrentDownloadService.getTorrentFiles(torrent, configurationService.torrentConfig.TIMEOUT) .catch(error => { if (!this.isPackTorrent(torrent)) { - const entries = [{name: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}]; + const entries = this.getDefaultFileEntries(torrent); return {videos: entries, contents: [], subtitles: [], files: entries} } return Promise.reject(error); }); - if (files.contents && files.contents.length && !files.videos.length && this.isDiskTorrent(files.contents)) { - files.videos = [{name: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}]; + if (files.contents && files.contents.length && !files.videos?.length && this.isDiskTorrent(files.contents)) { + files.videos = this.getDefaultFileEntries(torrent); } + return files; }; private getDefaultFileEntries = (torrent: IParsedTorrent): IFileAttributes[] => [{ - title: torrent.title, + title: torrent.title!, path: torrent.title, size: torrent.size, - fileIndex: null + fileIndex: 0, }]; private getSeriesTorrentContent = async (torrent: IParsedTorrent): Promise => this.torrentDownloadService.getTorrentFiles(torrent, configurationService.torrentConfig.TIMEOUT) @@ -167,13 +192,13 @@ export class TorrentFileService implements ITorrentFileService { private mapSeriesEpisode = async (torrent: IParsedTorrent, file: IFileAttributes, files: IFileAttributes[]): Promise => { if (!file.episodes && !file.episodes) { - if (files.length === 1 || files.some(f => f.episodes || f.episodes) || parse(torrent.title).seasons) { + if (files.length === 1 || files.some(f => f.episodes || f.episodes) || parse(torrent.title!).seasons) { return Promise.resolve([{ infoHash: torrent.infoHash, fileIndex: file.fileIndex, title: file.path || file.title, size: file.size, - imdbId: torrent.imdbId.toString() || file.imdbId.toString(), + imdbId: torrent?.imdbId?.toString() || file?.imdbId?.toString() || '', }]); } return Promise.resolve([]); @@ -184,14 +209,14 @@ export class TorrentFileService implements ITorrentFileService { fileIndex: file.fileIndex, title: file.path || file.title, size: file.size, - imdbId: file.imdbId.toString() || torrent.imdbId.toString(), + imdbId: file?.imdbId?.toString() || torrent?.imdbId?.toString() || '', imdbSeason: file.season, season: file.season, imdbEpisode: file.episodes && file.episodes[index], episode: file.episodes && file.episodes[index], kitsuEpisode: file.episodes && file.episodes[index], episodes: file.episodes, - kitsuId: parseInt(file.kitsuId.toString() || torrent.kitsuId.toString()) || 0, + kitsuId: parseInt(file.kitsuId?.toString() || torrent.kitsuId?.toString() || '0') || 0, }))) }; @@ -222,7 +247,7 @@ export class TorrentFileService implements ITorrentFileService { title: file.path || file.title, size: file.size, imdbId: imdbId, - kitsuId: parseInt(kitsuId) || 0, + kitsuId: parseInt(kitsuId?.toString() || '0') || 0, episodes: undefined, imdbSeason: undefined, imdbEpisode: undefined, @@ -232,21 +257,21 @@ export class TorrentFileService implements ITorrentFileService { // at this point, TypeScript infers that metadataOrError is actually MetadataResponse const metadata = metadataOrError; const hasEpisode = metadata.videos && metadata.videos.length && (file.episode || metadata.videos.length === 1); - const episodeVideo = hasEpisode && metadata.videos[(file.episode || 1) - 1]; + const episodeVideo = hasEpisode && metadata.videos && metadata.videos[(file.episode || 1) - 1]; return [{ infoHash: torrent.infoHash, fileIndex: file.fileIndex, title: file.path || file.title, size: file.size, - imdbId: metadata.imdbId.toString() || imdbId, - kitsuId: parseInt(metadata.kitsuId.toString() || kitsuId) || 0, + imdbId: metadata.imdbId?.toString() || imdbId || '', + kitsuId: parseInt(metadata.kitsuId?.toString() || kitsuId?.toString() || '0') || 0, imdbSeason: episodeVideo && metadata.imdbId ? episodeVideo.season : undefined, - imdbEpisode: episodeVideo && metadata.imdbId | metadata.kitsuId ? episodeVideo.episode || episodeVideo.episode : undefined, - kitsuEpisode: episodeVideo && metadata.imdbId | metadata.kitsuId ? episodeVideo.episode || episodeVideo.episode : undefined, + imdbEpisode: episodeVideo && metadata.imdbId || episodeVideo && metadata.kitsuId ? episodeVideo.episode || episodeVideo.episode : undefined, + kitsuEpisode: episodeVideo && metadata.imdbId || episodeVideo && metadata.kitsuId ? episodeVideo.episode || episodeVideo.episode : undefined, }]; }; - private decomposeEpisodes = async (torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse = {episodeCount: []}) => { + private decomposeEpisodes = async (torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse = {episodeCount: []}):Promise => { if (files.every(file => !file.episodes && !file.date)) { return files; } @@ -291,18 +316,18 @@ export class TorrentFileService implements ITorrentFileService { return files; }; - private preprocessEpisodes = (files: IFileAttributes[]) => { + private preprocessEpisodes = (files: IFileAttributes[]): void => { // reverse special episode naming when they named with 0 episode, ie. S02E00 files .filter(file => Number.isInteger(file.season) && file.episode === 0) .forEach(file => { file.episode = file.season - file.episodes = [file.season] + file.episodes = [file.season || 0]; file.season = 0; }) }; - private isConcatSeasonAndEpisodeFiles = (files: IFileAttributes[], sortedEpisodes: number[], metadata: IMetadataResponse) => { + private isConcatSeasonAndEpisodeFiles = (files: IFileAttributes[], sortedEpisodes: number[], metadata: IMetadataResponse): boolean => { if (metadata.kitsuId !== undefined) { // anime does not use this naming scheme in 99% of cases; return false; @@ -317,58 +342,59 @@ export class TorrentFileService implements ITorrentFileService { const threshold = Math.max(Math.ceil(files.length * 0.8), 5); const sortedConcatEpisodes = sortedEpisodes .filter(ep => ep > 100) - .filter(ep => metadata.episodeCount[this.div100(ep) - 1] < ep) - .filter(ep => metadata.episodeCount[this.div100(ep) - 1] >= this.mod100(ep)); + .filter(ep => metadata.episodeCount && metadata.episodeCount[this.div100(ep) - 1] < ep) + .filter(ep => metadata.episodeCount && metadata.episodeCount[this.div100(ep) - 1] >= this.mod100(ep)); const concatFileEpisodes = files .filter(file => !file.isMovie && file.episodes) - .filter(file => !file.season || file.episodes.every(ep => this.div100(ep) === file.season)); + .filter(file => !file.season || file.episodes?.every(ep => this.div100(ep) === file.season)); const concatAboveTotalEpisodeCount = files .filter(file => !file.isMovie && file.episodes && file.episodes.every(ep => ep > 100)) - .filter(file => file.episodes.every(ep => ep > metadata.totalCount)); + .filter(file => file.episodes?.every(ep => ep > metadata.totalCount!)); return sortedConcatEpisodes.length >= thresholdSorted && concatFileEpisodes.length >= threshold || concatAboveTotalEpisodeCount.length >= thresholdAbove; }; - private isDateEpisodeFiles = (files: IFileAttributes[], metadata: IMetadataResponse) => files.every(file => (!file.season || !metadata.episodeCount[file.season - 1]) && file.date); + private isDateEpisodeFiles = (files: IFileAttributes[], metadata: IMetadataResponse): boolean => files.every(file => (!file.season || metadata.episodeCount && !metadata.episodeCount[file.season - 1]) && file.date); - private isAbsoluteEpisodeFiles = (torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse) => { + private isAbsoluteEpisodeFiles = (torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse): boolean => { const threshold = Math.ceil(files.length / 5); const isAnime = torrent.type === TorrentType.Anime && torrent.kitsuId; - const nonMovieEpisodes = files - .filter(file => !file.isMovie && file.episodes); + const nonMovieEpisodes = files.filter(file => !file.isMovie && file.episodes); const absoluteEpisodes = files .filter(file => file.season && file.episodes) - .filter(file => file.episodes.every(ep => metadata.episodeCount[file.season - 1] < ep)) + .filter(file => file.episodes?.every(ep => + metadata.episodeCount && file.season && metadata.episodeCount[file.season - 1] < ep)); return nonMovieEpisodes.every(file => !file.season) - || (isAnime && nonMovieEpisodes.every(file => file.season > metadata.episodeCount.length)) + || (isAnime && nonMovieEpisodes.every(file => + metadata.episodeCount && file.season && file.season > metadata.episodeCount.length)) || absoluteEpisodes.length >= threshold; }; - private isNewEpisodeNotInMetadata = (torrent: IParsedTorrent, video: IFileAttributes, metadata: IMetadataResponse) => { - // new episode might not yet been indexed by cinemeta. - // detect this if episode number is larger than the last episode or season is larger than the last one - // only for non anime metas + private isNewEpisodeNotInMetadata = (torrent: IParsedTorrent, video: IFileAttributes, metadata: IMetadataResponse): boolean => { const isAnime = torrent.type === TorrentType.Anime && torrent.kitsuId; - return !isAnime && !video.isMovie && video.episodes && video.season !== 1 - && /continuing|current/i.test(metadata.status) - && video.season >= metadata.episodeCount.length - && video.episodes.every(ep => ep > (metadata.episodeCount[video.season - 1] || 0)); + return !!( !isAnime && !video.isMovie && video.episodes && video.season !== 1 + && metadata.status && /continuing|current/i.test(metadata.status) + && metadata.episodeCount && video.season && video.season >= metadata.episodeCount.length + && video.episodes.every(ep => metadata.episodeCount && video.season && ep > (metadata.episodeCount[video.season - 1] || 0))); }; - private decomposeConcatSeasonAndEpisodeFiles = (files: IFileAttributes[], metadata: IMetadataResponse) => { + private decomposeConcatSeasonAndEpisodeFiles = (files: IFileAttributes[], metadata: IMetadataResponse): void => { files .filter(file => file.episodes && file.season !== 0 && file.episodes.every(ep => ep > 100)) - .filter(file => metadata.episodeCount[(file.season || this.div100(file.episodes[0])) - 1] < 100) - .filter(file => file.season && file.episodes.every(ep => this.div100(ep) === file.season) || !file.season) + .filter(file => file.episodes && metadata?.episodeCount && + ((file.season || this.div100(file.episodes[0])) - 1) >= 0 && + metadata.episodeCount[(file.season || this.div100(file.episodes[0])) - 1] < 100) + .filter(file => (file.season && file.episodes && file.episodes.every(ep => this.div100(ep) === file.season)) || !file.season) .forEach(file => { - file.season = this.div100(file.episodes[0]); - file.episodes = file.episodes.map(ep => this.mod100(ep)) + if (file.episodes) { + file.season = this.div100(file.episodes[0]); + file.episodes = file.episodes.map(ep => this.mod100(ep)); + } }); - }; - private decomposeAbsoluteEpisodeFiles = (torrent: IParsedTorrent, videos: IFileAttributes[], metadata: IMetadataResponse) => { - if (metadata.episodeCount.length === 0) { + private decomposeAbsoluteEpisodeFiles = (torrent: IParsedTorrent, videos: IFileAttributes[], metadata: IMetadataResponse): void => { + if (metadata.episodeCount?.length === 0) { videos .filter(file => !Number.isInteger(file.season) && file.episodes && !file.isMovie) .forEach(file => { @@ -376,29 +402,38 @@ export class TorrentFileService implements ITorrentFileService { }); return; } + if (!metadata.episodeCount) return; + videos .filter(file => file.episodes && !file.isMovie && file.season !== 0) .filter(file => !this.isNewEpisodeNotInMetadata(torrent, file, metadata)) - .filter(file => !file.season || (metadata.episodeCount[file.season - 1] || 0) < file.episodes[0]) + .filter(file => { + if (!file.episodes || !metadata.episodeCount) return false; + return !file.season || (metadata.episodeCount[file.season - 1] || 0) < file.episodes[0]; + }) .forEach(file => { - const seasonIdx = ([...metadata.episodeCount.keys()] - .find((i) => metadata.episodeCount.slice(0, i + 1).reduce((a, b) => a + b) >= file.episodes[0]) - + 1 || metadata.episodeCount.length) - 1; + if(!file.episodes || !metadata.episodeCount) return; + + let seasonIdx = metadata.episodeCount + .map((_, i) => i) + .find(i => metadata.episodeCount && file.episodes && metadata.episodeCount.slice(0, i + 1).reduce((a, b) => a + b) >= file.episodes[0]); + + seasonIdx = (seasonIdx || 1 || metadata.episodeCount.length) - 1; file.season = seasonIdx + 1; file.episodes = file.episodes - .map(ep => ep - metadata.episodeCount.slice(0, seasonIdx).reduce((a, b) => a + b, 0)) + .map(ep => ep - (metadata.episodeCount?.slice(0, seasonIdx).reduce((a, b) => a + b, 0) || 0)); }); }; - private decomposeDateEpisodeFiles = (files: IFileAttributes[], metadata: IMetadataResponse) => { + private decomposeDateEpisodeFiles = (files: IFileAttributes[], metadata: IMetadataResponse): void => { if (!metadata || !metadata.videos || !metadata.videos.length) { return; } const timeZoneOffset = this.getTimeZoneOffset(metadata.country); - const offsetVideos = metadata.videos - .reduce((map, video) => { + const offsetVideos: { [key: string]: ICommonVideoMetadata } = metadata.videos + .reduce((map: { [key: string]: ICommonVideoMetadata }, video: ICommonVideoMetadata) => { const releaseDate = moment(video.released).utcOffset(timeZoneOffset).format('YYYY-MM-DD'); map[releaseDate] = video; return map; @@ -407,15 +442,15 @@ export class TorrentFileService implements ITorrentFileService { files .filter(file => file.date) .forEach(file => { - const video = offsetVideos[file.date]; + const video = offsetVideos[file.date!]; if (video) { file.season = video.season; - file.episodes = [video.episode]; + file.episodes = [video.episode || 0]; } }); }; - private getTimeZoneOffset = (country: string | undefined) => { + private getTimeZoneOffset = (country: string | undefined): string => { switch (country) { case 'United States': case 'USA': @@ -425,7 +460,7 @@ export class TorrentFileService implements ITorrentFileService { } }; - private assignKitsuOrImdbEpisodes = (torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse) => { + private assignKitsuOrImdbEpisodes = (torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse): IFileAttributes[] => { if (!metadata || !metadata.videos || !metadata.videos.length) { if (torrent.type === TorrentType.Anime) { // assign episodes as kitsu episodes for anime when no metadata available for imdb mapping @@ -437,31 +472,35 @@ export class TorrentFileService implements ITorrentFileService { }) if (metadata.type === TorrentType.Movie && files.every(file => !file.imdbId)) { // sometimes a movie has episode naming, thus not recognized as a movie and imdbId not assigned - files.forEach(file => file.imdbId = metadata.imdbId.toString()); + files.forEach(file => file.imdbId = metadata.imdbId?.toString()); } } return files; } - const seriesMapping: ICommonVideoMetadata = metadata.videos - .reduce((map, video) => { - const episodeMap = map[video.season] || {}; - episodeMap[video.episode] = video; - map[video.season] = episodeMap; + const seriesMapping = metadata.videos + .filter(video => video.season !== undefined && Number.isInteger(video.season) && video.episode !== undefined && Number.isInteger(video.episode)) + .reduce((map, video) => { + if (video.season !== undefined && video.episode !== undefined) { + const episodeMap = map[video.season] || {}; + episodeMap[video.episode] = video; + map[video.season] = episodeMap; + } return map; }, {}); + if (metadata.videos.some(video => Number.isInteger(video.season)) || !metadata.imdbId) { - files.filter((file => Number.isInteger(file.season) && file.episodes)) + files.filter(file => file && Number.isInteger(file.season) && file.episodes) .map(file => { - const seasonMapping = seriesMapping[file.season]; - const episodeMapping = seasonMapping && seasonMapping[file.episodes[0]]; + const seasonMapping = file && file.season && seriesMapping[file.season] || null; + const episodeMapping = seasonMapping && file && file.episodes && file.episodes[0] && seasonMapping[file.episodes[0]] || null; + if (episodeMapping && Number.isInteger(episodeMapping.season)) { - file.imdbId = metadata.imdbId.toString(); + file.imdbId = metadata.imdbId?.toString(); file.season = episodeMapping.season; - file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].episode); + file.episodes = file.episodes && file.episodes.map(ep => (seasonMapping && seasonMapping[ep]) ? Number(seasonMapping[ep].episode) : 0); } else { - // no imdb mapping available for episode file.season = undefined; file.episodes = undefined; } @@ -471,11 +510,16 @@ export class TorrentFileService implements ITorrentFileService { files .filter(file => Number.isInteger(file.season) && file.episodes) .forEach(file => { + if (!file.season || !file.episodes) { + return; + } if (seriesMapping[file.season]) { + const seasonMapping = seriesMapping[file.season]; - file.imdbId = metadata.imdbId.toString(); - file.kitsuId = seasonMapping[file.episodes[0]] && seasonMapping[file.episodes[0]].kitsuId || 0; - file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode); + file.imdbId = metadata.imdbId?.toString(); + file.kitsuId = seasonMapping[file.episodes[0]] && parseInt(seasonMapping[file.episodes[0]].id || '0') || 0; + file.episodes = file.episodes.map(ep => seasonMapping[ep]?.episode) + .filter((ep): ep is number => ep !== undefined); } else if (seriesMapping[file.season - 1]) { // sometimes a second season might be a continuation of the previous season const seasonMapping = seriesMapping[file.season - 1] as ICommonVideoMetadata; @@ -483,48 +527,65 @@ export class TorrentFileService implements ITorrentFileService { const firstKitsuId = episodes.length && episodes[0]; const differentTitlesCount = new Set(episodes.map(ep => ep.id)).size const skippedCount = episodes.filter(ep => ep.id === firstKitsuId).length; + const emptyArray: number[] = []; const seasonEpisodes = files - .filter((otherFile: IFileAttributes) => otherFile.season === file.season) - .reduce((a, b) => a.concat(b.episodes), []); + .filter((otherFile: IFileAttributes) => otherFile.season === file.season && otherFile.episodes) + .reduce((a, b) => a.concat(b.episodes || []), emptyArray); const isAbsoluteOrder = seasonEpisodes.every(ep => ep > skippedCount && ep <= episodes.length) const isNormalOrder = seasonEpisodes.every(ep => ep + skippedCount <= episodes.length) if (differentTitlesCount >= 1 && (isAbsoluteOrder || isNormalOrder)) { - file.imdbId = metadata.imdbId.toString(); + const {season} = file; + const [episode] = file.episodes; + file.imdbId = metadata.imdbId?.toString(); file.season = file.season - 1; file.episodes = file.episodes.map(ep => isAbsoluteOrder ? ep : ep + skippedCount); - file.kitsuId = seasonMapping[file.episodes[0]].kitsuId || 0; - file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode); + const currentEpisode = seriesMapping[season][episode]; + file.kitsuId = currentEpisode ? parseInt(currentEpisode.id || '0') : 0; + if (typeof season === 'number' && Array.isArray(file.episodes)) { + file.episodes = file.episodes.map(ep => + seriesMapping[season] + && seriesMapping[season][ep] + && seriesMapping[season][ep].episode + || ep); + } } } else if (Object.values(seriesMapping).length === 1 && seriesMapping[1]) { // sometimes series might be named with sequel season but it's not a season on imdb and a new title + // eslint-disable-next-line prefer-destructuring const seasonMapping = seriesMapping[1]; - file.imdbId = metadata.imdbId.toString(); + file.imdbId = metadata.imdbId?.toString(); file.season = 1; - file.kitsuId = seasonMapping[file.episodes[0]].kitsuId || 0; - file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode); + file.kitsuId = parseInt(seasonMapping[file.episodes[0]].id || '0') || 0; + file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].episode) + .filter((ep): ep is number => ep !== undefined); } }); } return files; }; - private needsCinemetaMetadataForAnime = (files: IFileAttributes[], metadata: IMetadataResponse) => { + private needsCinemetaMetadataForAnime = (files: IFileAttributes[], metadata: IMetadataResponse): boolean => { if (!metadata || !metadata.imdbId || !metadata.videos || !metadata.videos.length) { return false; } - const minSeason = Math.min(...metadata.videos.map(video => video.season)) || Number.MAX_VALUE; - const maxSeason = Math.max(...metadata.videos.map(video => video.season)) || Number.MAX_VALUE; - const differentSeasons = new Set(metadata.videos + const seasons = metadata.videos .map(video => video.season) - .filter(season => Number.isInteger(season))).size; + .filter((season): season is number => season !== null && season !== undefined); + + // Using || 0 instead of || Number.MAX_VALUE to match previous logic + const minSeason = Math.min(...seasons) || 0; + const maxSeason = Math.max(...seasons) || 0; + const differentSeasons = new Set(seasons.filter(season => Number.isInteger(season))).size; + const total = metadata.totalCount || Number.MAX_VALUE; + return differentSeasons > 1 || files .filter(file => !file.isMovie && file.episodes) - .some(file => file.season < minSeason || file.season > maxSeason || file.episodes.every(ep => ep > total)); + .some(file => file.season || 0 < minSeason || file.season || 0 > maxSeason || file.episodes?.every(ep => ep > total)); }; - private updateToCinemetaMetadata = async (metadata: IMetadataResponse) => { + private updateToCinemetaMetadata = async (metadata: IMetadataResponse): Promise => { const query: IMetaDataQuery = { id: metadata.imdbId, type: metadata.type @@ -538,7 +599,7 @@ export class TorrentFileService implements ITorrentFileService { return metadata; // or throw newMetadataOrError to propagate error up the call stack } // At this point TypeScript infers newMetadataOrError to be of type MetadataResponse - let newMetadata = newMetadataOrError; + const newMetadata = newMetadataOrError; if (!newMetadata.videos || !newMetadata.videos.length) { return metadata; } else { @@ -550,7 +611,7 @@ export class TorrentFileService implements ITorrentFileService { }) }; - private findMovieImdbId = (title: IFileAttributes | string) => { + private findMovieImdbId = (title: IFileAttributes | string):Promise => { const parsedTitle = typeof title === 'string' ? parse(title) : title; this.logger.debug(`Finding movie imdbId for ${title}`); return this.imdb_limiter.schedule(async () => { @@ -567,7 +628,7 @@ export class TorrentFileService implements ITorrentFileService { }); }; - private findMovieKitsuId = async (title: IFileAttributes | string) => { + private findMovieKitsuId = async (title: IFileAttributes | string):Promise => { const parsedTitle = typeof title === 'string' ? parse(title) : title; const kitsuQuery = { title: parsedTitle.title, @@ -582,20 +643,20 @@ export class TorrentFileService implements ITorrentFileService { } }; - private isDiskTorrent = (contents: IContentAttributes[]) => contents.some(content => ExtensionHelpers.isDisk(content.path)); + private isDiskTorrent = (contents: IContentAttributes[]): boolean => contents.some(content => ExtensionHelpers.isDisk(content.path)); - private isSingleMovie = (videos: IFileAttributes[]) => videos.length === 1 || + private isSingleMovie = (videos: IFileAttributes[]): boolean => videos.length === 1 || (videos.length === 2 && - videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?1\b|^0?1\.\w{2,4}$/i.test(v.path)) && - videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?2\b|^0?2\.\w{2,4}$/i.test(v.path))); + videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?1\b|^0?1\.\w{2,4}$/i.test(v.path!)) && + videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?2\b|^0?2\.\w{2,4}$/i.test(v.path!))) !== undefined; - private isFeaturette = (video: IFileAttributes) => /featurettes?\/|extras-grym/i.test(video.path); + private isFeaturette = (video: IFileAttributes):boolean => /featurettes?\/|extras-grym/i.test(video.path!); private parseSeriesVideo = (video: IFileAttributes): IFileAttributes => { const videoInfo = parse(video.title); // the episode may be in a folder containing season number - if (!Number.isInteger(videoInfo.season) && video.path.includes('/')) { - const folders = video.path.split('/'); + if (!Number.isInteger(videoInfo.season) && video.path?.includes('/')) { + const folders = video.path?.split('/'); const pathInfo = parse(folders[folders.length - 2]); videoInfo.season = pathInfo.season; } @@ -604,12 +665,12 @@ export class TorrentFileService implements ITorrentFileService { } if (!Number.isInteger(videoInfo.season) && videoInfo.seasons && videoInfo.seasons.length > 1) { // in case single file was interpreted as having multiple seasons - videoInfo.season = videoInfo.seasons[0]; + [videoInfo.season] = videoInfo.seasons; } - if (!Number.isInteger(videoInfo.season) && video.path.includes('/') && video.seasons + if (!Number.isInteger(videoInfo.season) && video.path?.includes('/') && video.seasons && video.seasons.length > 1) { // russian season are usually named with 'series name-2` i.e. Улицы разбитых фонарей-6/22. Одиночный выстрел.mkv - const folderPathSeasonMatch = video.path.match(/[\u0400-\u04ff]-(\d{1,2})(?=.*\/)/); + const folderPathSeasonMatch = video.path?.match(/[\u0400-\u04ff]-(\d{1,2})(?=.*\/)/); videoInfo.season = folderPathSeasonMatch && parseInt(folderPathSeasonMatch[1], 10) || undefined; } // sometimes video file does not have correct date format as in torrent title @@ -619,18 +680,18 @@ export class TorrentFileService implements ITorrentFileService { // limit number of episodes in case of incorrect parsing if (videoInfo.episodes && videoInfo.episodes.length > 20) { videoInfo.episodes = [videoInfo.episodes[0]]; - videoInfo.episode = videoInfo.episodes[0]; + [videoInfo.episode] = videoInfo.episodes; } // force episode to any found number if it was not parsed if (!videoInfo.episodes && !videoInfo.date) { const epMatcher = videoInfo.title.match( /(? other.title === video.title && other.year === video.year).length < 3; }; - private clearInfoFields = (video: IFileAttributes) => { + private clearInfoFields = (video: IFileAttributes): IFileAttributes => { video.imdbId = undefined; video.imdbSeason = undefined; video.imdbEpisode = undefined; @@ -670,9 +731,9 @@ export class TorrentFileService implements ITorrentFileService { return video; }; - private div100 = (episode: number) => (episode / 100 >> 0); + private div100 = (episode: number): number => (episode / 100 >> 0); - private mod100 = (episode: number) => episode % 100; + private mod100 = (episode: number): number => episode % 100; } diff --git a/src/node/consumer/src/lib/services/torrent_processing_service.ts b/src/node/consumer/src/lib/services/torrent_processing_service.ts index c88d680..dd19b78 100644 --- a/src/node/consumer/src/lib/services/torrent_processing_service.ts +++ b/src/node/consumer/src/lib/services/torrent_processing_service.ts @@ -1,12 +1,12 @@ -import {TorrentType} from "../enums/torrent_types"; -import {IIngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes"; -import {IParsedTorrent} from "../interfaces/parsed_torrent"; -import {ITorrentProcessingService} from "../interfaces/torrent_processing_service"; import {inject, injectable} from "inversify"; -import {IocTypes} from "../models/ioc_types"; -import {ITorrentEntriesService} from "../interfaces/torrent_entries_service"; +import {TorrentType} from "../enums/torrent_types"; import {ILoggingService} from "../interfaces/logging_service"; +import {IParsedTorrent} from "../interfaces/parsed_torrent"; +import {ITorrentEntriesService} from "../interfaces/torrent_entries_service"; +import {ITorrentProcessingService} from "../interfaces/torrent_processing_service"; import {ITrackerService} from "../interfaces/tracker_service"; +import {IocTypes} from "../models/ioc_types"; +import {IIngestedTorrentAttributes} from "../repository/interfaces/ingested_torrent_attributes"; @injectable() export class TorrentProcessingService implements ITorrentProcessingService { diff --git a/src/node/consumer/src/lib/services/torrent_subtitle_service.ts b/src/node/consumer/src/lib/services/torrent_subtitle_service.ts index 97878f8..ec227dc 100644 --- a/src/node/consumer/src/lib/services/torrent_subtitle_service.ts +++ b/src/node/consumer/src/lib/services/torrent_subtitle_service.ts @@ -1,8 +1,9 @@ +import {injectable} from "inversify"; import {parse} from 'parse-torrent-title'; import {ITorrentFileCollection} from "../interfaces/torrent_file_collection"; -import {IFileAttributes} from "../../repository/interfaces/file_attributes"; import {ITorrentSubtitleService} from "../interfaces/torrent_subtitle_service"; -import {injectable} from "inversify"; +import {IFileAttributes} from "../repository/interfaces/file_attributes"; +import {ISubtitleAttributes} from "../repository/interfaces/subtitle_attributes"; @injectable() export class TorrentSubtitleService implements ITorrentSubtitleService { @@ -28,57 +29,57 @@ export class TorrentSubtitleService implements ITorrentSubtitleService { return fileCollection; }; - private parseVideo = (video: IFileAttributes) => { - const fileName = video.title.split('/').pop().replace(/\.(\w{2,4})$/, ''); - const folderName = video.title.replace(/\/?[^/]+$/, ''); + private parseVideo = (video: IFileAttributes): IFileAttributes => { + const fileName = video.title?.split('/')?.pop()?.replace(/\.(\w{2,4})$/, '') || ''; + const folderName = video.title?.replace(/\/?[^/]+$/, '') || ''; return { videoFile: video, fileName: fileName, folderName: folderName, - ...this.parseFilename(video.title) + ...this.parseFilename(video.title.toString() || '') }; } - private mostProbableSubtitleVideos = (subtitle: any, parsedVideos: any[]) => { - const subTitle = (subtitle.title || subtitle.path).split('/').pop().replace(/\.(\w{2,4})$/, ''); + private mostProbableSubtitleVideos = (subtitle: ISubtitleAttributes, parsedVideos: IFileAttributes[]) : IFileAttributes[] => { + const subTitle = (subtitle.title || subtitle.path)?.split('/')?.pop()?.replace(/\.(\w{2,4})$/, '') || ''; const parsedSub = this.parsePath(subtitle.title || subtitle.path); - const byFileName = parsedVideos.filter(video => subTitle.includes(video.fileName)); + const byFileName = parsedVideos.filter(video => subTitle.includes(video.title!)); if (byFileName.length === 1) { - return byFileName.map(v => v.videoFile); + return byFileName.map(v => v); } const byTitleSeasonEpisode = parsedVideos.filter(video => video.title === parsedSub.title - && this.arrayEquals(video.seasons, parsedSub.seasons) - && this.arrayEquals(video.episodes, parsedSub.episodes)); + && this.arrayEquals(video.seasons || [], parsedSub.seasons || []) + && this.arrayEquals(video.episodes || [], parsedSub.episodes || [])); if (this.singleVideoFile(byTitleSeasonEpisode)) { - return byTitleSeasonEpisode.map(v => v.videoFile); + return byTitleSeasonEpisode.map(v => v); } - const bySeasonEpisode = parsedVideos.filter(video => this.arrayEquals(video.seasons, parsedSub.seasons) - && this.arrayEquals(video.episodes, parsedSub.episodes)); + const bySeasonEpisode = parsedVideos.filter(video => this.arrayEquals(video.seasons || [], parsedSub.seasons || []) + && this.arrayEquals(video.episodes || [], parsedSub.episodes || [])); if (this.singleVideoFile(bySeasonEpisode)) { - return bySeasonEpisode.map(v => v.videoFile); + return bySeasonEpisode.map(v => v); } const byTitle = parsedVideos.filter(video => video.title && video.title === parsedSub.title); if (this.singleVideoFile(byTitle)) { - return byTitle.map(v => v.videoFile); + return byTitle.map(v => v); } - const byEpisode = parsedVideos.filter(video => this.arrayEquals(video.episodes, parsedSub.episodes)); + const byEpisode = parsedVideos.filter(video => this.arrayEquals(video.episodes || [], parsedSub.episodes || [])); if (this.singleVideoFile(byEpisode)) { - return byEpisode.map(v => v.videoFile); + return byEpisode.map(v => v); } - return undefined; + return []; } - private singleVideoFile = (videos: any[]) => { - return new Set(videos.map(v => v.videoFile.fileIndex)).size === 1; + private singleVideoFile = (videos: IFileAttributes[]): boolean => { + return new Set(videos.map(v => v.fileIndex)).size === 1; } - private parsePath = (path: string) => { + private parsePath = (path: string): IFileAttributes => { const pathParts = path.split('/').map(part => this.parseFilename(part)); const parsedWithEpisode = pathParts.find(parsed => parsed.season && parsed.episodes); return parsedWithEpisode || pathParts[pathParts.length - 1]; } - private parseFilename = (filename: string) => { + private parseFilename = (filename: string) : IFileAttributes => { const parsedInfo = parse(filename) const titleEpisode = parsedInfo.title.match(/(\d+)$/); if (!parsedInfo.episodes && titleEpisode) { @@ -87,7 +88,7 @@ export class TorrentSubtitleService implements ITorrentSubtitleService { return parsedInfo; } - private arrayEquals = (array1: any[], array2: any[]) => { + private arrayEquals = (array1: T[], array2: T[]): boolean => { if (!array1 || !array2) return array1 === array2; return array1.length === array2.length && array1.every((value, index) => value === array2[index]) } diff --git a/src/node/consumer/src/lib/services/tracker_service.ts b/src/node/consumer/src/lib/services/tracker_service.ts index 5918e19..ad324aa 100644 --- a/src/node/consumer/src/lib/services/tracker_service.ts +++ b/src/node/consumer/src/lib/services/tracker_service.ts @@ -1,10 +1,10 @@ import axios, {AxiosResponse} from 'axios'; -import {configurationService} from './configuration_service'; -import {ITrackerService} from "../interfaces/tracker_service"; import {inject, injectable} from "inversify"; -import {IocTypes} from "../models/ioc_types"; import {ICacheService} from "../interfaces/cache_service"; import {ILoggingService} from "../interfaces/logging_service"; +import {ITrackerService} from "../interfaces/tracker_service"; +import {IocTypes} from "../models/ioc_types"; +import {configurationService} from './configuration_service'; @injectable() export class TrackerService implements ITrackerService { diff --git a/src/node/consumer/src/main.ts b/src/node/consumer/src/main.ts index a523bdf..9efc598 100644 --- a/src/node/consumer/src/main.ts +++ b/src/node/consumer/src/main.ts @@ -1,8 +1,9 @@ +import "reflect-metadata"; // required +import {ICompositionalRoot} from "./lib/interfaces/composition_root"; import {serviceContainer} from "./lib/models/inversify_config"; import {IocTypes} from "./lib/models/ioc_types"; -import {ICompositionalRoot} from "./lib/interfaces/composition_root"; -(async () => { +(async (): Promise => { const compositionalRoot = serviceContainer.get(IocTypes.ICompositionalRoot); await compositionalRoot.start(); })(); \ No newline at end of file diff --git a/src/node/consumer/tsconfig.json b/src/node/consumer/tsconfig.json index 435ebc2..cf2dbcd 100644 --- a/src/node/consumer/tsconfig.json +++ b/src/node/consumer/tsconfig.json @@ -1,23 +1,29 @@ { "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "node", - "outDir": "dist", - "pretty": true, + "target": "es2020", + "module": "es2020", + "allowJs": true, "removeComments": true, - "rootDir": "./src", + "resolveJsonModule": true, + "typeRoots": [ + "./node_modules/@types" + ], "sourceMap": true, - "target": "ES6", + "outDir": "dist", + "strict": true, "lib": [ - "es6" - ], - "types": [ - "node", - "reflect-metadata" + "es2020" ], + "baseUrl": ".", + "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "allowSyntheticDefaultImports": true - } + "moduleResolution": "Node", + "skipLibCheck": true, + }, + "include": [ + "src/**/*" + ], + "exclude": ["node_modules"], } \ No newline at end of file