feat: clamav tarama sistemi ve hata yönetimi iyileştirmeleri ekle
ClamAV entegrasyonu ile indirilen altyazı dosyalarının otomatik virüs taraması eklendi. Pipeline tabanlı hata yönetimi sistemi ile hatalar kategorize edilip daha iyi işleniyor. Türkcealtyazi sağlayıcısı TV dizileri için sezon/bölüm bazlı eşleştirme ve paket indirme desteği kazandı. Dosya izleyicide olay çiftleme (deduplication) mekanizması eklendi. Metin kodlaması normalizasyonu Türkçe karakterler için geliştirildi.
This commit is contained in:
841
services/core/package-lock.json
generated
841
services/core/package-lock.json
generated
@@ -27,6 +27,278 @@
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"cpu": [
|
||||
@@ -42,6 +314,159 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/ajv-compiler": {
|
||||
"version": "4.0.5",
|
||||
"funding": [
|
||||
@@ -175,6 +600,58 @@
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
|
||||
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||
"version": "3.0.3",
|
||||
"cpu": [
|
||||
@@ -186,10 +663,261 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
|
||||
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
|
||||
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
|
||||
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"cpu": [
|
||||
@@ -202,6 +930,104 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"dev": true,
|
||||
@@ -890,6 +1716,21 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -14,5 +14,6 @@ export const env = {
|
||||
mediaMoviePath: process.env.MEDIA_MOVIE_PATH ?? '/media/movie',
|
||||
enableApiKey: process.env.ENABLE_API_KEY === 'true',
|
||||
apiKey: process.env.API_KEY ?? '',
|
||||
watcherDedupWindowMs: Number(process.env.CORE_WATCHER_DEDUP_WINDOW_MS ?? 15000),
|
||||
isDev: (process.env.NODE_ENV ?? 'development') !== 'production'
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import IORedis from 'ioredis';
|
||||
import { Redis } from 'ioredis';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
export const redis = new IORedis({
|
||||
export const redis = new Redis({
|
||||
host: env.redisHost,
|
||||
port: env.redisPort,
|
||||
maxRetriesPerRequest: null
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import { redis } from '../db/redis.js';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
export const fileEventsQueue = new Queue('fileEvents', { connection: redis });
|
||||
export const mediaAnalysisQueue = new Queue('mediaAnalysis', { connection: redis });
|
||||
export const subtitleFetchQueue = new Queue('subtitleFetch', { connection: redis });
|
||||
export const finalizeWriteQueue = new Queue('finalizeWrite', { connection: redis });
|
||||
const connection = {
|
||||
host: env.redisHost,
|
||||
port: env.redisPort,
|
||||
maxRetriesPerRequest: null
|
||||
};
|
||||
|
||||
export const fileEventsQueue = new Queue('fileEvents', { connection });
|
||||
export const mediaAnalysisQueue = new Queue('mediaAnalysis', { connection });
|
||||
export const subtitleFetchQueue = new Queue('subtitleFetch', { connection });
|
||||
export const finalizeWriteQueue = new Queue('finalizeWrite', { connection });
|
||||
|
||||
export function createWorker(name: string, processor: any): Worker {
|
||||
return new Worker(name, processor, { connection: redis, concurrency: 2 });
|
||||
return new Worker(name, processor, { connection, concurrency: 2 });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { env } from '../config/env.js';
|
||||
import { createJobForPath } from '../workers/pipeline.js';
|
||||
import { MediaFileModel } from '../models/MediaFile.js';
|
||||
import { fileEventsQueue } from '../queues/queues.js';
|
||||
|
||||
export async function debugRoutes(app: FastifyInstance): Promise<void> {
|
||||
@@ -17,11 +16,10 @@ export async function debugRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.status(400).send({ error: 'Path does not exist in container' });
|
||||
}
|
||||
|
||||
const jobId = await createJobForPath(body.path, body.kind);
|
||||
const media = await MediaFileModel.findOne({ path: body.path }).lean();
|
||||
if (!media) return reply.status(500).send({ error: 'media not persisted' });
|
||||
|
||||
await fileEventsQueue.add('debug', { jobId, mediaFileId: String(media._id), path: body.path });
|
||||
return { ok: true, jobId };
|
||||
const created = await createJobForPath(body.path, body.kind);
|
||||
if (created.enqueued) {
|
||||
await fileEventsQueue.add('debug', { jobId: created.jobId, mediaFileId: created.mediaFileId, path: body.path });
|
||||
}
|
||||
return { ok: true, jobId: created.jobId, deduped: !created.enqueued };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,19 +11,51 @@ export async function jobRoutes(app: FastifyInstance): Promise<void> {
|
||||
page: z.coerce.number().default(1),
|
||||
limit: z.coerce.number().default(20),
|
||||
status: z.string().optional(),
|
||||
search: z.string().optional()
|
||||
search: z.string().optional(),
|
||||
dedupe: z.coerce.boolean().default(true)
|
||||
})
|
||||
.parse(req.query);
|
||||
|
||||
const filter: any = {};
|
||||
if (q.status) filter.status = q.status;
|
||||
if (q.search) filter.$or = [{ 'requestSnapshot.title': { $regex: q.search, $options: 'i' } }];
|
||||
|
||||
const skip = (q.page - 1) * q.limit;
|
||||
const [items, total] = await Promise.all([
|
||||
JobModel.find(filter).sort({ createdAt: -1 }).skip(skip).limit(q.limit).lean(),
|
||||
JobModel.countDocuments(filter)
|
||||
]);
|
||||
let items: any[] = [];
|
||||
let total = 0;
|
||||
|
||||
if (!q.dedupe) {
|
||||
const filter: any = {};
|
||||
if (q.status) filter.status = q.status;
|
||||
if (q.search) filter.$or = [{ 'requestSnapshot.title': { $regex: q.search, $options: 'i' } }];
|
||||
|
||||
[items, total] = await Promise.all([
|
||||
JobModel.find(filter).sort({ createdAt: -1 }).skip(skip).limit(q.limit).lean(),
|
||||
JobModel.countDocuments(filter)
|
||||
]);
|
||||
} else {
|
||||
const pipeline: any[] = [
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{ $group: { _id: '$mediaFileId', latest: { $first: '$$ROOT' } } },
|
||||
{ $replaceRoot: { newRoot: '$latest' } }
|
||||
];
|
||||
|
||||
const postMatch: any = {};
|
||||
if (q.status) postMatch.status = q.status;
|
||||
if (q.search) {
|
||||
postMatch.$or = [
|
||||
{ 'requestSnapshot.title': { $regex: q.search, $options: 'i' } },
|
||||
{ 'requestSnapshot.release': { $regex: q.search, $options: 'i' } }
|
||||
];
|
||||
}
|
||||
if (Object.keys(postMatch).length > 0) {
|
||||
pipeline.push({ $match: postMatch });
|
||||
}
|
||||
|
||||
const [countAgg, listAgg] = await Promise.all([
|
||||
JobModel.aggregate([...pipeline, { $count: 'total' }]),
|
||||
JobModel.aggregate([...pipeline, { $sort: { createdAt: -1 } }, { $skip: skip }, { $limit: q.limit }])
|
||||
]);
|
||||
|
||||
total = countAgg[0]?.total ?? 0;
|
||||
items = listAgg;
|
||||
}
|
||||
|
||||
return { items, total, page: q.page, limit: q.limit };
|
||||
});
|
||||
|
||||
@@ -33,23 +33,67 @@ export async function waitForStable(filePath: string, checks: number, intervalSe
|
||||
}
|
||||
|
||||
export function normalizeSubtitleBuffer(input: Buffer): string {
|
||||
const normalizeNewlines = (text: string): string => text.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
|
||||
const textQualityScore = (text: string): number => {
|
||||
const replacements = (text.match(/\uFFFD/g) || []).length;
|
||||
const turkishLetters = (text.match(/[çğıöşüÇĞİÖŞÜ]/g) || []).length;
|
||||
const suspiciousMojibake = (text.match(/[þýðÞÝÐÃÂ]/g) || []).length;
|
||||
return turkishLetters * 2 - replacements * 5 - suspiciousMojibake * 2;
|
||||
};
|
||||
|
||||
const maybeRepairTurkishMojibake = (text: string): string => {
|
||||
const suspicious = (text.match(/[þýðÞÝÐ]/g) || []).length;
|
||||
if (suspicious < 2) return text;
|
||||
const repaired = iconv.decode(Buffer.from(text, 'latin1'), 'windows-1254');
|
||||
return textQualityScore(repaired) > textQualityScore(text) ? repaired : text;
|
||||
};
|
||||
|
||||
const finalizeText = (text: string): string => normalizeNewlines(maybeRepairTurkishMojibake(text));
|
||||
|
||||
if (input.length >= 2) {
|
||||
// UTF-16 LE BOM
|
||||
if (input[0] === 0xff && input[1] === 0xfe) {
|
||||
return finalizeText(iconv.decode(input, 'utf16-le'));
|
||||
}
|
||||
// UTF-16 BE BOM
|
||||
if (input[0] === 0xfe && input[1] === 0xff) {
|
||||
return finalizeText(iconv.decode(input, 'utf16-be'));
|
||||
}
|
||||
}
|
||||
|
||||
if (input.length >= 3 && input[0] === 0xef && input[1] === 0xbb && input[2] === 0xbf) {
|
||||
return input.toString('utf8').replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
return finalizeText(input.toString('utf8'));
|
||||
}
|
||||
|
||||
// If bytes round-trip as UTF-8, prefer UTF-8 regardless of heuristic detection.
|
||||
const utf8Decoded = input.toString('utf8');
|
||||
if (Buffer.from(utf8Decoded, 'utf8').equals(input)) {
|
||||
return finalizeText(utf8Decoded);
|
||||
}
|
||||
|
||||
const detected = jschardet.detect(input);
|
||||
const enc = (detected.encoding || '').toLowerCase();
|
||||
const confidence = typeof detected.confidence === 'number' ? detected.confidence : 0;
|
||||
|
||||
const pickSingleByteDecode = (): string => {
|
||||
const tr = iconv.decode(input, 'windows-1254');
|
||||
const latin = iconv.decode(input, 'latin1');
|
||||
return textQualityScore(tr) >= textQualityScore(latin) ? tr : latin;
|
||||
};
|
||||
|
||||
let decoded: string;
|
||||
if (enc.includes('utf')) {
|
||||
if (enc.includes('utf') && confidence >= 0.6) {
|
||||
decoded = input.toString('utf8');
|
||||
} else if (enc.includes('windows-1254') || enc.includes('iso-8859-9')) {
|
||||
decoded = iconv.decode(input, 'windows-1254');
|
||||
} else if (enc.includes('windows') || enc.includes('latin') || enc.includes('iso-8859-1') || confidence < 0.7) {
|
||||
decoded = pickSingleByteDecode();
|
||||
} else {
|
||||
decoded = iconv.decode(input, 'latin1');
|
||||
}
|
||||
|
||||
return decoded.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
return finalizeText(decoded);
|
||||
}
|
||||
|
||||
export async function nextSubtitlePath(basePathWithoutExt: string, lang: string, ext: 'srt' | 'ass', overwrite: boolean): Promise<string> {
|
||||
|
||||
20
services/core/src/watcher/dedup.ts
Normal file
20
services/core/src/watcher/dedup.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export function createEventDeduper(windowMs: number): (filePath: string, now?: number) => boolean {
|
||||
const recentByPath = new Map<string, number>();
|
||||
const safeWindow = Number.isFinite(windowMs) && windowMs > 0 ? windowMs : 0;
|
||||
|
||||
return (filePath: string, now = Date.now()) => {
|
||||
const last = recentByPath.get(filePath);
|
||||
if (typeof last === 'number' && now - last < safeWindow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
recentByPath.set(filePath, now);
|
||||
|
||||
// Keep memory bounded by removing old keys opportunistically.
|
||||
for (const [p, ts] of recentByPath) {
|
||||
if (now - ts >= safeWindow) recentByPath.delete(p);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,10 @@ import { WatchedPathModel } from '../models/WatchedPath.js';
|
||||
import { createJobForPath } from '../workers/pipeline.js';
|
||||
import { fileEventsQueue } from '../queues/queues.js';
|
||||
import { isVideoFile } from '../utils/file.js';
|
||||
import { env } from '../config/env.js';
|
||||
import { createEventDeduper } from './dedup.js';
|
||||
|
||||
let watcherStarted = false;
|
||||
|
||||
export async function ensureDefaultWatchedPaths(tvPath: string, moviePath: string): Promise<void> {
|
||||
await WatchedPathModel.updateOne({ path: tvPath }, { $setOnInsert: { path: tvPath, kind: 'tv', enabled: true } }, { upsert: true });
|
||||
@@ -10,6 +14,9 @@ export async function ensureDefaultWatchedPaths(tvPath: string, moviePath: strin
|
||||
}
|
||||
|
||||
export async function startWatcher(): Promise<void> {
|
||||
if (watcherStarted) return;
|
||||
watcherStarted = true;
|
||||
|
||||
const watched = await WatchedPathModel.find({ enabled: true }).lean();
|
||||
const paths = watched.map((w) => w.path);
|
||||
if (paths.length === 0) {
|
||||
@@ -18,26 +25,25 @@ export async function startWatcher(): Promise<void> {
|
||||
}
|
||||
|
||||
const byPath = new Map(watched.map((w) => [w.path, w.kind]));
|
||||
const shouldProcessEvent = createEventDeduper(env.watcherDedupWindowMs);
|
||||
const watcher = chokidar.watch(paths, { ignoreInitial: false, awaitWriteFinish: false, persistent: true });
|
||||
|
||||
watcher.on('add', async (p) => {
|
||||
if (!isVideoFile(p)) return;
|
||||
if (!shouldProcessEvent(p)) return;
|
||||
const kind = resolveKind(p, byPath);
|
||||
const jobId = await createJobForPath(p, kind === 'movie' ? 'movie' : 'tv');
|
||||
const media = await import('../models/MediaFile.js');
|
||||
const mediaDoc = await media.MediaFileModel.findOne({ path: p }).lean();
|
||||
if (!mediaDoc) return;
|
||||
await fileEventsQueue.add('add', { jobId, mediaFileId: String(mediaDoc._id), path: p });
|
||||
const created = await createJobForPath(p, kind === 'movie' ? 'movie' : 'tv');
|
||||
if (!created.enqueued) return;
|
||||
await fileEventsQueue.add('add', { jobId: created.jobId, mediaFileId: created.mediaFileId, path: p });
|
||||
});
|
||||
|
||||
watcher.on('change', async (p) => {
|
||||
if (!isVideoFile(p)) return;
|
||||
if (!shouldProcessEvent(p)) return;
|
||||
const kind = resolveKind(p, byPath);
|
||||
const jobId = await createJobForPath(p, kind === 'movie' ? 'movie' : 'tv');
|
||||
const media = await import('../models/MediaFile.js');
|
||||
const mediaDoc = await media.MediaFileModel.findOne({ path: p }).lean();
|
||||
if (!mediaDoc) return;
|
||||
await fileEventsQueue.add('change', { jobId, mediaFileId: String(mediaDoc._id), path: p });
|
||||
const created = await createJobForPath(p, kind === 'movie' ? 'movie' : 'tv');
|
||||
if (!created.enqueued) return;
|
||||
await fileEventsQueue.add('change', { jobId: created.jobId, mediaFileId: created.mediaFileId, path: p });
|
||||
});
|
||||
|
||||
watcher.on('unlink', async (p) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { Job } from 'bullmq';
|
||||
import { Types } from 'mongoose';
|
||||
import { JobModel } from '../models/Job.js';
|
||||
import { MediaFileModel } from '../models/MediaFile.js';
|
||||
@@ -32,9 +33,23 @@ interface FinalizeData {
|
||||
}
|
||||
|
||||
const activeWorkers: any[] = [];
|
||||
let workersStarted = false;
|
||||
const ACTIVE_JOB_STATUSES = [
|
||||
'PENDING',
|
||||
'WAITING_FILE_STABLE',
|
||||
'PARSED',
|
||||
'ANALYZED',
|
||||
'REQUESTING_API',
|
||||
'FOUND_TEMP',
|
||||
'NORMALIZING_ENCODING',
|
||||
'WRITING_SUBTITLE'
|
||||
] as const;
|
||||
|
||||
export function startWorkers(): void {
|
||||
activeWorkers.push(createWorker('fileEvents', async (job) => {
|
||||
if (workersStarted) return;
|
||||
workersStarted = true;
|
||||
|
||||
activeWorkers.push(createWorker('fileEvents', async (job: Job<FileEventData>) => {
|
||||
const data = job.data as FileEventData;
|
||||
const settings = await SettingModel.findById('global').lean();
|
||||
if (!settings) throw new Error('settings missing');
|
||||
@@ -47,7 +62,31 @@ export function startWorkers(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const stable = await waitForStable(data.path, settings.stableChecks, settings.stableIntervalSeconds);
|
||||
let stable = false;
|
||||
try {
|
||||
stable = await waitForStable(data.path, settings.stableChecks, settings.stableIntervalSeconds);
|
||||
} catch (err: any) {
|
||||
const missing = err?.code === 'ENOENT';
|
||||
if (missing) {
|
||||
await MediaFileModel.findByIdAndUpdate(data.mediaFileId, { status: 'MISSING', lastSeenAt: new Date() });
|
||||
}
|
||||
await JobModel.findByIdAndUpdate(data.jobId, {
|
||||
status: 'ERROR',
|
||||
error: {
|
||||
code: missing ? 'FILE_NOT_FOUND' : 'FILE_STABLE_CHECK_FAILED',
|
||||
message: missing ? 'file not found during stability check' : (err?.message || 'stability check failed')
|
||||
}
|
||||
});
|
||||
await writeJobLog({
|
||||
jobId: data.jobId,
|
||||
step: 'JOB_ERROR',
|
||||
level: 'error',
|
||||
message: missing ? `File not found during stability check: ${data.path}` : 'Stability check failed',
|
||||
meta: missing ? undefined : { error: err?.message }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stable) {
|
||||
await JobModel.findByIdAndUpdate(data.jobId, { status: 'ERROR', error: { code: 'FILE_NOT_STABLE', message: 'file not stable' } });
|
||||
await writeJobLog({ jobId: data.jobId, step: 'JOB_ERROR', level: 'error', message: 'File did not become stable' });
|
||||
@@ -87,7 +126,7 @@ export function startWorkers(): void {
|
||||
await mediaAnalysisQueue.add('analyze', { jobId: data.jobId, mediaFileId: data.mediaFileId });
|
||||
}));
|
||||
|
||||
activeWorkers.push(createWorker('mediaAnalysis', async (job) => {
|
||||
activeWorkers.push(createWorker('mediaAnalysis', async (job: Job<SubtitleFetchData>) => {
|
||||
const { jobId, mediaFileId } = job.data as SubtitleFetchData;
|
||||
const media = await MediaFileModel.findById(mediaFileId).lean();
|
||||
if (!media) return;
|
||||
@@ -114,7 +153,7 @@ export function startWorkers(): void {
|
||||
await subtitleFetchQueue.add('search', { jobId, mediaFileId });
|
||||
}));
|
||||
|
||||
activeWorkers.push(createWorker('subtitleFetch', async (job) => {
|
||||
activeWorkers.push(createWorker('subtitleFetch', async (job: Job<SubtitleFetchData>) => {
|
||||
const { jobId, mediaFileId } = job.data as SubtitleFetchData;
|
||||
const media = await MediaFileModel.findById(mediaFileId).lean();
|
||||
const settings = await SettingModel.findById('global').lean();
|
||||
@@ -135,7 +174,8 @@ export function startWorkers(): void {
|
||||
mediaInfo: media.mediaInfo,
|
||||
preferHI: settings.preferHI,
|
||||
preferForced: settings.preferForced,
|
||||
securityLimits: settings.securityLimits
|
||||
securityLimits: settings.securityLimits,
|
||||
features: settings.features
|
||||
};
|
||||
|
||||
const res = await apiClient.post('/v1/subtitles/search', payload);
|
||||
@@ -189,7 +229,7 @@ export function startWorkers(): void {
|
||||
});
|
||||
}));
|
||||
|
||||
activeWorkers.push(createWorker('finalizeWrite', async (job) => {
|
||||
activeWorkers.push(createWorker('finalizeWrite', async (job: Job<FinalizeData>) => {
|
||||
const data = job.data as FinalizeData;
|
||||
const media = await MediaFileModel.findById(data.mediaFileId).lean();
|
||||
const settings = await SettingModel.findById('global').lean();
|
||||
@@ -205,14 +245,22 @@ export function startWorkers(): void {
|
||||
|
||||
const parsed = path.parse(media.path);
|
||||
const target = await nextSubtitlePath(path.join(parsed.dir, parsed.name), data.lang, extensionFromPath(data.bestPath), settings.overwriteExisting);
|
||||
await writeJobLog({ jobId: data.jobId, step: 'FINAL_TARGET_PATH_RESOLVED', message: target });
|
||||
|
||||
const targetExists = target !== `${path.join(parsed.dir, parsed.name)}.${data.lang}.${extensionFromPath(data.bestPath)}`;
|
||||
if (targetExists) {
|
||||
await writeJobLog({ jobId: data.jobId, step: 'WRITE_TARGET_SKIPPED_EXISTS', message: 'Target exists, using incremented filename', meta: { target } });
|
||||
}
|
||||
|
||||
await fs.writeFile(target, normalized, 'utf8');
|
||||
await writeJobLog({ jobId: data.jobId, step: 'WRITE_TARGET_DONE', message: target });
|
||||
const tempTarget = `${target}.tmp-${Date.now()}`;
|
||||
await fs.writeFile(tempTarget, normalized, 'utf8');
|
||||
await fs.rename(tempTarget, target);
|
||||
await writeJobLog({
|
||||
jobId: data.jobId,
|
||||
step: 'WRITE_TARGET_DONE',
|
||||
message: target,
|
||||
meta: { bytes: Buffer.byteLength(normalized, 'utf8') }
|
||||
});
|
||||
|
||||
await JobModel.findByIdAndUpdate(data.jobId, {
|
||||
status: 'DONE',
|
||||
@@ -240,7 +288,10 @@ export function startWorkers(): void {
|
||||
}));
|
||||
}
|
||||
|
||||
export async function createJobForPath(filePath: string, mediaTypeHint: 'tv' | 'movie'): Promise<string> {
|
||||
export async function createJobForPath(
|
||||
filePath: string,
|
||||
mediaTypeHint: 'tv' | 'movie'
|
||||
): Promise<{ jobId: string; mediaFileId: string; enqueued: boolean }> {
|
||||
const st = await fs.stat(filePath);
|
||||
|
||||
const media = await MediaFileModel.findOneAndUpdate(
|
||||
@@ -258,6 +309,27 @@ export async function createJobForPath(filePath: string, mediaTypeHint: 'tv' | '
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
const existing = await JobModel.findOne({
|
||||
mediaFileId: media._id,
|
||||
status: { $in: ACTIVE_JOB_STATUSES }
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.lean();
|
||||
|
||||
if (existing) {
|
||||
await writeJobLog({
|
||||
jobId: existing._id as Types.ObjectId,
|
||||
step: 'WATCH_EVENT_DEDUPED',
|
||||
message: `Skipped duplicate watch event for ${filePath}`,
|
||||
meta: { mediaFileId: String(media._id), existingStatus: existing.status }
|
||||
});
|
||||
return {
|
||||
jobId: String(existing._id),
|
||||
mediaFileId: String(media._id),
|
||||
enqueued: false
|
||||
};
|
||||
}
|
||||
|
||||
const created = await JobModel.create({
|
||||
mediaFileId: media._id,
|
||||
status: 'PENDING',
|
||||
@@ -270,5 +342,9 @@ export async function createJobForPath(filePath: string, mediaTypeHint: 'tv' | '
|
||||
message: `Queued watch event for ${filePath}`
|
||||
});
|
||||
|
||||
return String(created._id);
|
||||
return {
|
||||
jobId: String(created._id),
|
||||
mediaFileId: String(media._id),
|
||||
enqueued: true
|
||||
};
|
||||
}
|
||||
|
||||
33
services/core/test/encoding.test.ts
Normal file
33
services/core/test/encoding.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import iconv from 'iconv-lite';
|
||||
import { normalizeSubtitleBuffer } from '../src/utils/file.js';
|
||||
|
||||
describe('normalizeSubtitleBuffer', () => {
|
||||
const sample = '1\r\n00:00:01,000 --> 00:00:02,000\r\nÇığ ÖşÜ ıİ\r\n';
|
||||
|
||||
it('keeps valid utf8 Turkish characters intact', () => {
|
||||
const buf = Buffer.from(sample, 'utf8');
|
||||
const out = normalizeSubtitleBuffer(buf);
|
||||
expect(out).toBe('1\n00:00:01,000 --> 00:00:02,000\nÇığ ÖşÜ ıİ\n');
|
||||
});
|
||||
|
||||
it('decodes windows-1254 Turkish text correctly', () => {
|
||||
const buf = iconv.encode(sample, 'windows-1254');
|
||||
const out = normalizeSubtitleBuffer(buf);
|
||||
expect(out).toBe('1\n00:00:01,000 --> 00:00:02,000\nÇığ ÖşÜ ıİ\n');
|
||||
});
|
||||
|
||||
it('decodes utf16-le BOM correctly', () => {
|
||||
const buf = iconv.encode(sample, 'utf16-le');
|
||||
const withBom = Buffer.concat([Buffer.from([0xff, 0xfe]), buf]);
|
||||
const out = normalizeSubtitleBuffer(withBom);
|
||||
expect(out).toBe('1\n00:00:01,000 --> 00:00:02,000\nÇığ ÖşÜ ıİ\n');
|
||||
});
|
||||
|
||||
it('repairs mojibake form with \u00fd/\u00fe/\u00f0 Turkish corruption', () => {
|
||||
const mojibake = '1\r\n00:00:01,000 --> 00:00:02,000\r\nKýz þöyle dedi: aðýr bir iþ.\r\n';
|
||||
const buf = Buffer.from(mojibake, 'utf8');
|
||||
const out = normalizeSubtitleBuffer(buf);
|
||||
expect(out).toContain('Kız şöyle dedi: ağır bir iş.');
|
||||
});
|
||||
});
|
||||
19
services/core/test/watcherDedup.test.ts
Normal file
19
services/core/test/watcherDedup.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createEventDeduper } from '../src/watcher/dedup.js';
|
||||
|
||||
describe('createEventDeduper', () => {
|
||||
it('suppresses repeated events inside window for same path', () => {
|
||||
const dedupe = createEventDeduper(1000);
|
||||
|
||||
expect(dedupe('/media/movie/a.mkv', 1000)).toBe(true);
|
||||
expect(dedupe('/media/movie/a.mkv', 1500)).toBe(false);
|
||||
expect(dedupe('/media/movie/a.mkv', 2001)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not suppress different paths', () => {
|
||||
const dedupe = createEventDeduper(1000);
|
||||
|
||||
expect(dedupe('/media/movie/a.mkv', 1000)).toBe(true);
|
||||
expect(dedupe('/media/movie/b.mkv', 1200)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user