diff --git a/backend/package-lock.json b/backend/package-lock.json index 4c7273c..8422843 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,7 +20,241 @@ "youtube-dl-exec": "^2.4.17" }, "devDependencies": { - "nodemon": "^3.0.3" + "@types/cors": "^2.8.19", + "@types/express": "^5.0.5", + "@types/fs-extra": "^11.0.4", + "@types/multer": "^2.0.0", + "@types/node": "^24.10.1", + "nodemon": "^3.0.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/accepts": { @@ -36,6 +270,32 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -104,6 +364,13 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -655,6 +922,13 @@ "node": ">= 0.10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -770,6 +1044,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -1622,6 +1906,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2706,6 +2997,50 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2743,6 +3078,20 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -2759,6 +3108,13 @@ "node": ">=18.17" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -2807,6 +3163,13 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2902,6 +3265,16 @@ "node": ">=0.4" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/youtube-dl-exec": { "version": "2.5.8", "resolved": "https://registry.npmjs.org/youtube-dl-exec/-/youtube-dl-exec-2.5.8.tgz", diff --git a/backend/package.json b/backend/package.json index 6a7a7f7..163f17b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,8 +3,9 @@ "version": "1.0.0", "main": "server.js", "scripts": { - "start": "node server.js", - "dev": "nodemon server.js", + "start": "ts-node src/server.ts", + "dev": "nodemon src/server.ts", + "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], @@ -23,6 +24,13 @@ "youtube-dl-exec": "^2.4.17" }, "devDependencies": { - "nodemon": "^3.0.3" + "@types/cors": "^2.8.19", + "@types/express": "^5.0.5", + "@types/fs-extra": "^11.0.4", + "@types/multer": "^2.0.0", + "@types/node": "^24.10.1", + "nodemon": "^3.0.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" } } diff --git a/backend/src/config/paths.js b/backend/src/config/paths.js deleted file mode 100644 index 353705e..0000000 --- a/backend/src/config/paths.js +++ /dev/null @@ -1,24 +0,0 @@ -const path = require("path"); - -// Assuming the application is started from the 'backend' directory -const ROOT_DIR = process.cwd(); - -const UPLOADS_DIR = path.join(ROOT_DIR, "uploads"); -const VIDEOS_DIR = path.join(UPLOADS_DIR, "videos"); -const IMAGES_DIR = path.join(UPLOADS_DIR, "images"); -const DATA_DIR = path.join(ROOT_DIR, "data"); - -const VIDEOS_DATA_PATH = path.join(DATA_DIR, "videos.json"); -const STATUS_DATA_PATH = path.join(DATA_DIR, "status.json"); -const COLLECTIONS_DATA_PATH = path.join(DATA_DIR, "collections.json"); - -module.exports = { - ROOT_DIR, - UPLOADS_DIR, - VIDEOS_DIR, - IMAGES_DIR, - DATA_DIR, - VIDEOS_DATA_PATH, - STATUS_DATA_PATH, - COLLECTIONS_DATA_PATH, -}; diff --git a/backend/src/config/paths.ts b/backend/src/config/paths.ts new file mode 100644 index 0000000..4a689df --- /dev/null +++ b/backend/src/config/paths.ts @@ -0,0 +1,13 @@ +import path from "path"; + +// Assuming the application is started from the 'backend' directory +export const ROOT_DIR: string = process.cwd(); + +export const UPLOADS_DIR: string = path.join(ROOT_DIR, "uploads"); +export const VIDEOS_DIR: string = path.join(UPLOADS_DIR, "videos"); +export const IMAGES_DIR: string = path.join(UPLOADS_DIR, "images"); +export const DATA_DIR: string = path.join(ROOT_DIR, "data"); + +export const VIDEOS_DATA_PATH: string = path.join(DATA_DIR, "videos.json"); +export const STATUS_DATA_PATH: string = path.join(DATA_DIR, "status.json"); +export const COLLECTIONS_DATA_PATH: string = path.join(DATA_DIR, "collections.json"); diff --git a/backend/src/controllers/collectionController.js b/backend/src/controllers/collectionController.ts similarity index 81% rename from backend/src/controllers/collectionController.js rename to backend/src/controllers/collectionController.ts index 2e05bd8..9d63bc2 100644 --- a/backend/src/controllers/collectionController.js +++ b/backend/src/controllers/collectionController.ts @@ -1,7 +1,9 @@ -const storageService = require("../services/storageService"); +import { Request, Response } from "express"; +import * as storageService from "../services/storageService"; +import { Collection } from "../services/storageService"; // Get all collections -const getCollections = (req, res) => { +export const getCollections = (req: Request, res: Response): void => { try { const collections = storageService.getCollections(); res.json(collections); @@ -14,7 +16,7 @@ const getCollections = (req, res) => { }; // Create a new collection -const createCollection = (req, res) => { +export const createCollection = (req: Request, res: Response): any => { try { const { name, videoId } = req.body; @@ -25,11 +27,12 @@ const createCollection = (req, res) => { } // Create a new collection - const newCollection = { + const newCollection: Collection = { id: Date.now().toString(), name, videos: videoId ? [videoId] : [], createdAt: new Date().toISOString(), + title: name, // Ensure title is also set as it's required by the interface }; // Save the new collection @@ -45,7 +48,7 @@ const createCollection = (req, res) => { }; // Update a collection -const updateCollection = (req, res) => { +export const updateCollection = (req: Request, res: Response): any => { try { const { id } = req.params; const { name, videoId, action } = req.body; @@ -57,6 +60,7 @@ const updateCollection = (req, res) => { // Update the collection if (name) { collection.name = name; + collection.title = name; // Update title as well } // Add or remove a video @@ -92,7 +96,7 @@ const updateCollection = (req, res) => { }; // Delete a collection -const deleteCollection = (req, res) => { +export const deleteCollection = (req: Request, res: Response): any => { try { const { id } = req.params; const { deleteVideos } = req.query; @@ -101,7 +105,7 @@ const deleteCollection = (req, res) => { if (deleteVideos === 'true') { const collection = storageService.getCollectionById(id); if (collection && collection.videos && collection.videos.length > 0) { - collection.videos.forEach(videoId => { + collection.videos.forEach((videoId) => { storageService.deleteVideo(videoId); }); } @@ -123,10 +127,3 @@ const deleteCollection = (req, res) => { .json({ success: false, error: "Failed to delete collection" }); } }; - -module.exports = { - getCollections, - createCollection, - updateCollection, - deleteCollection, -}; diff --git a/backend/src/controllers/videoController.js b/backend/src/controllers/videoController.ts similarity index 84% rename from backend/src/controllers/videoController.js rename to backend/src/controllers/videoController.ts index d451c2c..93b53e2 100644 --- a/backend/src/controllers/videoController.js +++ b/backend/src/controllers/videoController.ts @@ -1,16 +1,18 @@ -const storageService = require("../services/storageService"); -const downloadService = require("../services/downloadService"); -const { - isValidUrl, - extractUrlFromText, - resolveShortUrl, - isBilibiliUrl, - trimBilibiliUrl, - extractBilibiliVideoId, -} = require("../utils/helpers"); +import { Request, Response } from "express"; +import downloadManager from "../services/downloadManager"; +import * as downloadService from "../services/downloadService"; +import * as storageService from "../services/storageService"; +import { + extractBilibiliVideoId, + extractUrlFromText, + isBilibiliUrl, + isValidUrl, + resolveShortUrl, + trimBilibiliUrl, +} from "../utils/helpers"; // Search for videos -const searchVideos = async (req, res) => { +export const searchVideos = async (req: Request, res: Response): Promise => { try { const { query } = req.query; @@ -18,9 +20,9 @@ const searchVideos = async (req, res) => { return res.status(400).json({ error: "Search query is required" }); } - const results = await downloadService.searchYouTube(query); + const results = await downloadService.searchYouTube(query as string); res.status(200).json({ results }); - } catch (error) { + } catch (error: any) { console.error("Error searching for videos:", error); res.status(500).json({ error: "Failed to search for videos", @@ -29,12 +31,8 @@ const searchVideos = async (req, res) => { } }; -const downloadManager = require("../services/downloadManager"); - -// ... (imports remain the same) - // Download video -const downloadVideo = async (req, res) => { +export const downloadVideo = async (req: Request, res: Response): Promise => { try { const { youtubeUrl, downloadAllParts, collectionName, downloadCollection, collectionInfo } = req.body; let videoUrl = youtubeUrl; @@ -122,16 +120,17 @@ const downloadVideo = async (req, res) => { const { videosNumber, title } = partsInfo; // Update title in storage - storageService.addActiveDownload(downloadId, title); + storageService.addActiveDownload(downloadId, title || "Bilibili Video"); // Create a collection for the multi-part video if collectionName is provided - let collectionId = null; + let collectionId: string | null = null; if (collectionName) { const newCollection = { id: Date.now().toString(), name: collectionName, videos: [], createdAt: new Date().toISOString(), + title: collectionName, }; storageService.saveCollection(newCollection); collectionId = newCollection.id; @@ -146,28 +145,27 @@ const downloadVideo = async (req, res) => { firstPartUrl, 1, videosNumber, - title + title || "Bilibili Video" ); // Add to collection if needed if (collectionId && firstPartResult.videoData) { storageService.atomicUpdateCollection(collectionId, (collection) => { - collection.videos.push(firstPartResult.videoData.id); + collection.videos.push(firstPartResult.videoData!.id); return collection; }); } // Set up background download for remaining parts // Note: We don't await this, it runs in background - // But we should probably track it? For now, let's keep it simple - // and only track the first part as the "main" download if (videosNumber > 1) { downloadService.downloadRemainingBilibiliParts( baseUrl, 2, videosNumber, - title, - collectionId + title || "Bilibili Video", + collectionId!, + downloadId // Pass downloadId to track progress ); } @@ -203,26 +201,22 @@ const downloadVideo = async (req, res) => { }; // Add to download manager - // We don't await the result here because we want to return immediately - // that the download has been queued/started downloadManager.addDownload(downloadTask, downloadId, initialTitle) - .then(result => { + .then((result: any) => { console.log("Download completed successfully:", result); }) - .catch(error => { + .catch((error: any) => { console.error("Download failed:", error); }); // Return success immediately indicating the download is queued/started - // We can't return the video object yet because it hasn't been downloaded - // The frontend will need to refresh or listen for updates res.status(200).json({ success: true, message: "Download queued", downloadId }); - } catch (error) { + } catch (error: any) { console.error("Error queuing download:", error); res .status(500) @@ -231,7 +225,7 @@ const downloadVideo = async (req, res) => { }; // Get all videos -const getVideos = (req, res) => { +export const getVideos = (req: Request, res: Response): void => { try { const videos = storageService.getVideos(); res.status(200).json(videos); @@ -242,7 +236,7 @@ const getVideos = (req, res) => { }; // Get video by ID -const getVideoById = (req, res) => { +export const getVideoById = (req: Request, res: Response): any => { try { const { id } = req.params; const video = storageService.getVideoById(id); @@ -259,7 +253,7 @@ const getVideoById = (req, res) => { }; // Delete video -const deleteVideo = (req, res) => { +export const deleteVideo = (req: Request, res: Response): any => { try { const { id } = req.params; const success = storageService.deleteVideo(id); @@ -278,7 +272,7 @@ const deleteVideo = (req, res) => { }; // Get download status -const getDownloadStatus = (req, res) => { +export const getDownloadStatus = (req: Request, res: Response): void => { try { const status = storageService.getDownloadStatus(); res.status(200).json(status); @@ -289,7 +283,7 @@ const getDownloadStatus = (req, res) => { }; // Check Bilibili parts -const checkBilibiliParts = async (req, res) => { +export const checkBilibiliParts = async (req: Request, res: Response): Promise => { try { const { url } = req.query; @@ -297,12 +291,12 @@ const checkBilibiliParts = async (req, res) => { return res.status(400).json({ error: "URL is required" }); } - if (!isBilibiliUrl(url)) { + if (!isBilibiliUrl(url as string)) { return res.status(400).json({ error: "Not a valid Bilibili URL" }); } // Resolve shortened URLs (like b23.tv) - let videoUrl = url; + let videoUrl = url as string; if (videoUrl.includes("b23.tv")) { videoUrl = await resolveShortUrl(videoUrl); console.log("Resolved shortened URL to:", videoUrl); @@ -323,7 +317,7 @@ const checkBilibiliParts = async (req, res) => { const result = await downloadService.checkBilibiliVideoParts(videoId); res.status(200).json(result); - } catch (error) { + } catch (error: any) { console.error("Error checking Bilibili video parts:", error); res.status(500).json({ error: "Failed to check Bilibili video parts", @@ -333,7 +327,7 @@ const checkBilibiliParts = async (req, res) => { }; // Check if Bilibili URL is a collection or series -const checkBilibiliCollection = async (req, res) => { +export const checkBilibiliCollection = async (req: Request, res: Response): Promise => { try { const { url } = req.query; @@ -341,12 +335,12 @@ const checkBilibiliCollection = async (req, res) => { return res.status(400).json({ error: "URL is required" }); } - if (!isBilibiliUrl(url)) { + if (!isBilibiliUrl(url as string)) { return res.status(400).json({ error: "Not a valid Bilibili URL" }); } // Resolve shortened URLs (like b23.tv) - let videoUrl = url; + let videoUrl = url as string; if (videoUrl.includes("b23.tv")) { videoUrl = await resolveShortUrl(videoUrl); console.log("Resolved shortened URL to:", videoUrl); @@ -368,7 +362,7 @@ const checkBilibiliCollection = async (req, res) => { const result = await downloadService.checkBilibiliCollectionOrSeries(videoId); res.status(200).json(result); - } catch (error) { + } catch (error: any) { console.error("Error checking Bilibili collection/series:", error); res.status(500).json({ error: "Failed to check Bilibili collection/series", @@ -376,14 +370,3 @@ const checkBilibiliCollection = async (req, res) => { }); } }; - -module.exports = { - searchVideos, - downloadVideo, - getVideos, - getVideoById, - deleteVideo, - getDownloadStatus, - checkBilibiliParts, - checkBilibiliCollection, -}; diff --git a/backend/src/routes/api.js b/backend/src/routes/api.ts similarity index 80% rename from backend/src/routes/api.js rename to backend/src/routes/api.ts index 4cc9260..25c6fa9 100644 --- a/backend/src/routes/api.js +++ b/backend/src/routes/api.ts @@ -1,7 +1,8 @@ -const express = require("express"); +import express from "express"; +import * as collectionController from "../controllers/collectionController"; +import * as videoController from "../controllers/videoController"; + const router = express.Router(); -const videoController = require("../controllers/videoController"); -const collectionController = require("../controllers/collectionController"); // Video routes router.get("/search", videoController.searchVideos); @@ -19,4 +20,4 @@ router.post("/collections", collectionController.createCollection); router.put("/collections/:id", collectionController.updateCollection); router.delete("/collections/:id", collectionController.deleteCollection); -module.exports = router; +export default router; diff --git a/backend/server.js b/backend/src/server.ts similarity index 64% rename from backend/server.js rename to backend/src/server.ts index dd2a1b5..8270a05 100644 --- a/backend/server.js +++ b/backend/src/server.ts @@ -1,13 +1,13 @@ // Load environment variables from .env file -require("dotenv").config(); +import dotenv from "dotenv"; +dotenv.config(); -const express = require("express"); -const cors = require("cors"); -const path = require("path"); -const VERSION = require("./version"); -const apiRoutes = require("./src/routes/api"); -const storageService = require("./src/services/storageService"); -const { VIDEOS_DIR, IMAGES_DIR } = require("./src/config/paths"); +import cors from "cors"; +import express from "express"; +import { IMAGES_DIR, VIDEOS_DIR } from "./config/paths"; +import apiRoutes from "./routes/api"; +import * as storageService from "./services/storageService"; +import { VERSION } from "./version"; // Display version information VERSION.displayVersion(); diff --git a/backend/src/services/downloadManager.js b/backend/src/services/downloadManager.ts similarity index 63% rename from backend/src/services/downloadManager.js rename to backend/src/services/downloadManager.ts index 6dd02e3..aae4c99 100644 --- a/backend/src/services/downloadManager.js +++ b/backend/src/services/downloadManager.ts @@ -1,6 +1,18 @@ -const storageService = require("./storageService"); +import * as storageService from "./storageService"; + +interface DownloadTask { + downloadFn: () => Promise; + id: string; + title: string; + resolve: (value: any) => void; + reject: (reason?: any) => void; +} class DownloadManager { + private queue: DownloadTask[]; + private activeDownloads: number; + private maxConcurrentDownloads: number; + constructor() { this.queue = []; this.activeDownloads = 0; @@ -9,14 +21,18 @@ class DownloadManager { /** * Add a download task to the manager - * @param {Function} downloadFn - Async function that performs the download - * @param {string} id - Unique ID for the download - * @param {string} title - Title of the video being downloaded - * @returns {Promise} - Resolves when the download is complete + * @param downloadFn - Async function that performs the download + * @param id - Unique ID for the download + * @param title - Title of the video being downloaded + * @returns - Resolves when the download is complete */ - async addDownload(downloadFn, id, title) { + async addDownload( + downloadFn: () => Promise, + id: string, + title: string + ): Promise { return new Promise((resolve, reject) => { - const task = { + const task: DownloadTask = { downloadFn, id, title, @@ -32,7 +48,7 @@ class DownloadManager { /** * Process the download queue */ - async processQueue() { + private async processQueue(): Promise { if ( this.activeDownloads >= this.maxConcurrentDownloads || this.queue.length === 0 @@ -41,6 +57,8 @@ class DownloadManager { } const task = this.queue.shift(); + if (!task) return; + this.activeDownloads++; // Update status in storage @@ -49,14 +67,14 @@ class DownloadManager { try { console.log(`Starting download: ${task.title} (${task.id})`); const result = await task.downloadFn(); - + // Download complete storageService.removeActiveDownload(task.id); this.activeDownloads--; task.resolve(result); } catch (error) { console.error(`Error downloading ${task.title}:`, error); - + // Download failed storageService.removeActiveDownload(task.id); this.activeDownloads--; @@ -70,7 +88,7 @@ class DownloadManager { /** * Get current status */ - getStatus() { + getStatus(): { active: number; queued: number } { return { active: this.activeDownloads, queued: this.queue.length, @@ -79,4 +97,4 @@ class DownloadManager { } // Export a singleton instance -module.exports = new DownloadManager(); +export default new DownloadManager(); diff --git a/backend/src/services/downloadService.js b/backend/src/services/downloadService.ts similarity index 84% rename from backend/src/services/downloadService.js rename to backend/src/services/downloadService.ts index f5c8956..ade2bdc 100644 --- a/backend/src/services/downloadService.js +++ b/backend/src/services/downloadService.ts @@ -1,22 +1,75 @@ -const fs = require("fs-extra"); -const path = require("path"); -const youtubedl = require("youtube-dl-exec"); -const axios = require("axios"); -const { downloadByVedioPath } = require("bilibili-save-nodejs"); -const { VIDEOS_DIR, IMAGES_DIR } = require("../config/paths"); -const { - sanitizeFilename, - extractBilibiliVideoId, -} = require("../utils/helpers"); -const storageService = require("./storageService"); +import axios from "axios"; +import fs from "fs-extra"; +import path from "path"; +import youtubedl from "youtube-dl-exec"; +// @ts-ignore +import { downloadByVedioPath } from "bilibili-save-nodejs"; +import { IMAGES_DIR, VIDEOS_DIR } from "../config/paths"; +import { + extractBilibiliVideoId, + sanitizeFilename, +} from "../utils/helpers"; +import * as storageService from "./storageService"; +import { Collection, Video } from "./storageService"; + +interface BilibiliVideoInfo { + title: string; + author: string; + date: string; + thumbnailUrl: string | null; + thumbnailSaved: boolean; + error?: string; +} + +interface BilibiliPartsCheckResult { + success: boolean; + videosNumber: number; + title?: string; +} + +interface BilibiliCollectionCheckResult { + success: boolean; + type: 'collection' | 'series' | 'none'; + id?: number; + title?: string; + count?: number; + mid?: number; +} + +interface BilibiliVideoItem { + bvid: string; + title: string; + aid: number; +} + +interface BilibiliVideosResult { + success: boolean; + videos: BilibiliVideoItem[]; +} + +interface DownloadResult { + success: boolean; + videoData?: Video; + error?: string; +} + +interface CollectionDownloadResult { + success: boolean; + collectionId?: string; + videosDownloaded?: number; + error?: string; +} // Helper function to download Bilibili video -async function downloadBilibiliVideo(url, videoPath, thumbnailPath) { +export async function downloadBilibiliVideo( + url: string, + videoPath: string, + thumbnailPath: string +): Promise { + const tempDir = path.join(VIDEOS_DIR, `temp_${Date.now()}_${Math.floor(Math.random() * 10000)}`); + try { // Create a unique temporary directory for the download - const timestamp = Date.now(); - const random = Math.floor(Math.random() * 10000); - const tempDir = path.join(VIDEOS_DIR, `temp_${timestamp}_${random}`); fs.ensureDirSync(tempDir); console.log("Downloading Bilibili video to temp directory:", tempDir); @@ -34,7 +87,7 @@ async function downloadBilibiliVideo(url, videoPath, thumbnailPath) { const files = fs.readdirSync(tempDir); console.log("Files in temp directory:", files); - const videoFile = files.find((file) => file.endsWith(".mp4")); + const videoFile = files.find((file: string) => file.endsWith(".mp4")); if (!videoFile) { throw new Error("Downloaded video file not found"); @@ -56,7 +109,7 @@ async function downloadBilibiliVideo(url, videoPath, thumbnailPath) { // Try to get thumbnail from Bilibili let thumbnailSaved = false; - let thumbnailUrl = null; + let thumbnailUrl: string | null = null; const videoId = extractBilibiliVideoId(url); console.log("Extracted video ID:", videoId); @@ -92,7 +145,7 @@ async function downloadBilibiliVideo(url, videoPath, thumbnailPath) { const thumbnailWriter = fs.createWriteStream(thumbnailPath); thumbnailResponse.data.pipe(thumbnailWriter); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { thumbnailWriter.on("finish", () => { thumbnailSaved = true; resolve(); @@ -126,7 +179,7 @@ async function downloadBilibiliVideo(url, videoPath, thumbnailPath) { thumbnailUrl: null, thumbnailSaved: false, }; - } catch (error) { + } catch (error: any) { console.error("Error in downloadBilibiliVideo:", error); // Make sure we clean up the temp directory if it exists @@ -147,7 +200,7 @@ async function downloadBilibiliVideo(url, videoPath, thumbnailPath) { } // Helper function to check if a Bilibili video has multiple parts -async function checkBilibiliVideoParts(videoId) { +export async function checkBilibiliVideoParts(videoId: string): Promise { try { // Try to get video info from Bilibili API const apiUrl = `https://api.bilibili.com/x/web-interface/view?bvid=${videoId}`; @@ -176,7 +229,7 @@ async function checkBilibiliVideoParts(videoId) { } // Helper function to check if a Bilibili video belongs to a collection or series -async function checkBilibiliCollectionOrSeries(videoId) { +export async function checkBilibiliCollectionOrSeries(videoId: string): Promise { try { const apiUrl = `https://api.bilibili.com/x/web-interface/view?bvid=${videoId}`; console.log("Checking if video belongs to collection/series:", apiUrl); @@ -218,9 +271,9 @@ async function checkBilibiliCollectionOrSeries(videoId) { } // Helper function to get all videos from a Bilibili collection -async function getBilibiliCollectionVideos(mid, seasonId) { +export async function getBilibiliCollectionVideos(mid: number, seasonId: number): Promise { try { - const allVideos = []; + const allVideos: BilibiliVideoItem[] = []; let pageNum = 1; const pageSize = 30; let hasMore = true; @@ -253,7 +306,7 @@ async function getBilibiliCollectionVideos(mid, seasonId) { console.log(`Got ${archives.length} videos from page ${pageNum}`); - archives.forEach(video => { + archives.forEach((video: any) => { allVideos.push({ bvid: video.bvid, title: video.title, @@ -279,9 +332,9 @@ async function getBilibiliCollectionVideos(mid, seasonId) { } // Helper function to get all videos from a Bilibili series -async function getBilibiliSeriesVideos(mid, seriesId) { +export async function getBilibiliSeriesVideos(mid: number, seriesId: number): Promise { try { - const allVideos = []; + const allVideos: BilibiliVideoItem[] = []; let pageNum = 1; const pageSize = 30; let hasMore = true; @@ -313,7 +366,7 @@ async function getBilibiliSeriesVideos(mid, seriesId) { console.log(`Got ${archives.length} videos from page ${pageNum}`); - archives.forEach(video => { + archives.forEach((video: any) => { allVideos.push({ bvid: video.bvid, title: video.title, @@ -339,12 +392,12 @@ async function getBilibiliSeriesVideos(mid, seriesId) { } // Helper function to download a single Bilibili part -async function downloadSingleBilibiliPart( - url, - partNumber, - totalParts, - seriesTitle -) { +export async function downloadSingleBilibiliPart( + url: string, + partNumber: number, + totalParts: number, + seriesTitle: string +): Promise { try { console.log( `Downloading Bilibili part ${partNumber}/${totalParts}: ${url}` @@ -416,7 +469,7 @@ async function downloadSingleBilibiliPart( } // Create metadata for the video - const videoData = { + const videoData: Video = { id: timestamp.toString(), title: videoTitle, author: videoAuthor, @@ -424,8 +477,8 @@ async function downloadSingleBilibiliPart( source: "bilibili", sourceUrl: url, videoFilename: finalVideoFilename, - thumbnailFilename: thumbnailSaved ? finalThumbnailFilename : null, - thumbnailUrl: thumbnailUrl, + thumbnailFilename: thumbnailSaved ? finalThumbnailFilename : undefined, + thumbnailUrl: thumbnailUrl || undefined, videoPath: `/videos/${finalVideoFilename}`, thumbnailPath: thumbnailSaved ? `/images/${finalThumbnailFilename}` @@ -434,6 +487,7 @@ async function downloadSingleBilibiliPart( partNumber: partNumber, totalParts: totalParts, seriesTitle: seriesTitle, + createdAt: new Date().toISOString(), }; // Save the video using storage service @@ -442,7 +496,7 @@ async function downloadSingleBilibiliPart( console.log(`Part ${partNumber}/${totalParts} added to database`); return { success: true, videoData }; - } catch (error) { + } catch (error: any) { console.error( `Error downloading Bilibili part ${partNumber}/${totalParts}:`, error @@ -452,11 +506,11 @@ async function downloadSingleBilibiliPart( } // Helper function to download all videos from a Bilibili collection or series -async function downloadBilibiliCollection( - collectionInfo, - collectionName, - downloadId -) { +export async function downloadBilibiliCollection( + collectionInfo: BilibiliCollectionCheckResult, + collectionName: string, + downloadId: string +): Promise { try { const { type, id, mid, title, count } = collectionInfo; @@ -471,10 +525,10 @@ async function downloadBilibiliCollection( } // Fetch all videos from the collection/series - let videosResult; - if (type === 'collection') { + let videosResult: BilibiliVideosResult; + if (type === 'collection' && mid && id) { videosResult = await getBilibiliCollectionVideos(mid, id); - } else if (type === 'series') { + } else if (type === 'series' && mid && id) { videosResult = await getBilibiliSeriesVideos(mid, id); } else { throw new Error(`Unknown type: ${type}`); @@ -488,11 +542,12 @@ async function downloadBilibiliCollection( console.log(`Found ${videos.length} videos to download`); // Create a MyTube collection for these videos - const mytubeCollection = { + const mytubeCollection: Collection = { id: Date.now().toString(), - name: collectionName || title, + name: collectionName || title || "Collection", videos: [], createdAt: new Date().toISOString(), + title: collectionName || title || "Collection", }; storageService.saveCollection(mytubeCollection); const mytubeCollectionId = mytubeCollection.id; @@ -523,13 +578,13 @@ async function downloadBilibiliCollection( videoUrl, videoNumber, videos.length, - title + title || "Collection" ); // If download was successful, add to collection if (result.success && result.videoData) { storageService.atomicUpdateCollection(mytubeCollectionId, (collection) => { - collection.videos.push(result.videoData.id); + collection.videos.push(result.videoData!.id); return collection; }); @@ -558,7 +613,7 @@ async function downloadBilibiliCollection( collectionId: mytubeCollectionId, videosDownloaded: videos.length }; - } catch (error) { + } catch (error: any) { console.error(`Error downloading ${collectionInfo.type}:`, error); if (downloadId) { storageService.removeActiveDownload(downloadId); @@ -571,14 +626,14 @@ async function downloadBilibiliCollection( } // Helper function to download remaining Bilibili parts in sequence -async function downloadRemainingBilibiliParts( - baseUrl, - startPart, - totalParts, - seriesTitle, - collectionId, - downloadId -) { +export async function downloadRemainingBilibiliParts( + baseUrl: string, + startPart: number, + totalParts: number, + seriesTitle: string, + collectionId: string, + downloadId: string +): Promise { try { // Add to active downloads if ID is provided if (downloadId) { @@ -609,7 +664,7 @@ async function downloadRemainingBilibiliParts( if (result.success && collectionId && result.videoData) { try { storageService.atomicUpdateCollection(collectionId, (collection) => { - collection.videos.push(result.videoData.id); + collection.videos.push(result.videoData!.id); return collection; }); @@ -644,7 +699,7 @@ async function downloadRemainingBilibiliParts( } // Search for videos on YouTube -async function searchYouTube(query) { +export async function searchYouTube(query: string): Promise { console.log("Processing search request for query:", query); // Use youtube-dl to search for videos @@ -654,14 +709,14 @@ async function searchYouTube(query) { noCallHome: true, skipDownload: true, playlistEnd: 5, // Limit to 5 results - }); + } as any); - if (!searchResults || !searchResults.entries) { + if (!searchResults || !(searchResults as any).entries) { return []; } // Format the search results - const formattedResults = searchResults.entries.map((entry) => ({ + const formattedResults = (searchResults as any).entries.map((entry: any) => ({ id: entry.id, title: entry.title, author: entry.uploader, @@ -680,9 +735,8 @@ async function searchYouTube(query) { } // Download YouTube video -async function downloadYouTubeVideo(videoUrl) { +export async function downloadYouTubeVideo(videoUrl: string): Promise