diff --git a/backend/Dockerfile b/backend/Dockerfile index 7081703..7c8ccaf 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,8 +7,6 @@ WORKDIR /app COPY package*.json ./ # Skip Puppeteer download during build as we only need to compile TS ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 -# Skip Python check for youtube-dl-exec during build -ENV YOUTUBE_DL_SKIP_PYTHON_CHECK=1 RUN npm ci COPY . . @@ -57,7 +55,7 @@ COPY --from=builder /app/drizzle ./drizzle COPY --from=builder /app/bgutil-ytdlp-pot-provider /app/bgutil-ytdlp-pot-provider # Create necessary directories -RUN mkdir -p uploads/videos uploads/images data +RUN mkdir -p uploads/videos uploads/images uploads/subtitles data EXPOSE 5551 diff --git a/backend/package-lock.json b/backend/package-lock.json index cf8ad8a..0394154 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,7 +13,6 @@ "axios": "^1.8.1", "bcryptjs": "^3.0.3", "better-sqlite3": "^12.4.6", - "bilibili-save-nodejs": "^1.0.0", "cheerio": "^1.1.2", "cors": "^2.8.5", "dotenv": "^16.4.7", @@ -23,8 +22,7 @@ "multer": "^1.4.5-lts.1", "node-cron": "^4.2.1", "puppeteer": "^24.31.0", - "uuid": "^13.0.0", - "youtube-dl-exec": "^2.4.17" + "uuid": "^13.0.0" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", @@ -2150,21 +2148,6 @@ "node": ">= 14" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2203,15 +2186,6 @@ "node": ">= 8" } }, - "node_modules/app-root-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", - "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -2477,49 +2451,6 @@ "node": "20.x || 22.x || 23.x || 24.x || 25.x" } }, - "node_modules/bilibili-save-nodejs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bilibili-save-nodejs/-/bilibili-save-nodejs-1.0.0.tgz", - "integrity": "sha512-QpRWudBaKiJOGC1V1i26jCOVFbvmWU9TIRsSVDpBGIqP9AzhpxO14Ul5n624JS/gASVucXkGdRP6yXzRLyGHsg==", - "license": "ISC", - "dependencies": { - "app-root-path": "^3.0.0", - "axios": "^0.26.1", - "cheerio": "^1.0.0-rc.10", - "inquirer": "^8.2.2", - "minimist": "^1.2.6" - }, - "bin": { - "bili-download": "bin/cli.js" - } - }, - "node_modules/bilibili-save-nodejs/node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, - "node_modules/bin-version-check": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-6.0.0.tgz", - "integrity": "sha512-k9TS/pADINX9UlErjAkbkxDer8C+WlguMwySI8sLMGLUMDvwuHmDx00yoHe7nxshgwtLBcMWQgrlwjzscUeQKg==", - "deprecated": "Renamed to binary-version-check: https://www.npmjs.com/package/binary-version-check", - "license": "MIT", - "dependencies": { - "binary-version": "^7.1.0", - "semver": "^7.6.0", - "semver-truncate": "^3.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2533,22 +2464,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/binary-version": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/binary-version/-/binary-version-7.1.0.tgz", - "integrity": "sha512-Iy//vPc3ANPNlIWd242Npqc8MK0a/i4kVcHDlDA6HNMv5zMxz4ulIFhOSYJVKw/8AbHdHy0CnGYEt1QqSXxPsw==", - "license": "MIT", - "dependencies": { - "execa": "^8.0.1", - "find-versions": "^6.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -2761,49 +2676,6 @@ "node": ">=18" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "license": "MIT" - }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -2900,39 +2772,6 @@ "devtools-protocol": "*" } }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "license": "ISC", - "engines": { - "node": ">= 10" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2964,15 +2803,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3056,18 +2886,6 @@ "node": ">= 0.6" } }, - "node_modules/convert-hrtime": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", - "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -3146,6 +2964,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3184,15 +3003,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/dargs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", - "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -3245,18 +3055,6 @@ "node": ">=4.0.0" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -3796,15 +3594,6 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/escodegen": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", @@ -3885,29 +3674,6 @@ "bare-events": "^2.7.0" } }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -3973,20 +3739,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -4067,21 +3819,6 @@ "pend": "~1.2.0" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -4119,22 +3856,6 @@ "node": ">= 0.8" } }, - "node_modules/find-versions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-6.0.0.tgz", - "integrity": "sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==", - "license": "MIT", - "dependencies": { - "semver-regex": "^4.0.5", - "super-regex": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -4268,18 +3989,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function-timeout": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz", - "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4326,18 +4035,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-tsconfig": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", @@ -4647,15 +4344,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4723,32 +4411,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, - "node_modules/inquirer": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", - "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^6.0.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -4818,15 +4480,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4837,39 +4490,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unix": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/is-unix/-/is-unix-2.0.10.tgz", - "integrity": "sha512-CcasZSEOQUoE7JHy56se4wyRhdJfjohuMWYmceSTaDY4naKyd1fpLiY8rJsIT6AKfVstQAhHJOfPx7jcUxK61Q==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -4880,6 +4500,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -5053,28 +4674,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -5163,12 +4762,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -5211,18 +4804,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -5315,12 +4896,6 @@ "node": ">= 6.0.0" } }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "license": "ISC" - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5449,33 +5024,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -5530,53 +5078,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pac-proxy-agent": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", @@ -5731,6 +5232,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6176,49 +5678,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", @@ -6261,24 +5720,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6317,33 +5758,6 @@ "node": ">=10" } }, - "node_modules/semver-regex": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", - "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semver-truncate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", - "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -6408,6 +5822,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6420,6 +5835,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6508,6 +5924,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -6779,18 +6196,6 @@ "node": ">=8" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -6820,22 +6225,6 @@ "dev": true, "license": "MIT" }, - "node_modules/super-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.0.0.tgz", - "integrity": "sha512-CY8u7DtbvucKuquCmOFEKhr9Besln7n9uN8eFbwcoGYWXOMW07u2o8njWaiXt11ylS3qoGF55pILjRmPlbodyg==", - "license": "MIT", - "dependencies": { - "function-timeout": "^1.0.1", - "time-span": "^5.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/superagent": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", @@ -6997,27 +6386,6 @@ "b4a": "^1.6.4" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "license": "MIT" - }, - "node_modules/time-span": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", - "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", - "license": "MIT", - "dependencies": { - "convert-hrtime": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -7100,15 +6468,6 @@ "node": ">=14.0.0" } }, - "node_modules/tinyspawn": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/tinyspawn/-/tinyspawn-1.2.14.tgz", - "integrity": "sha512-/PYwakpVcbTLgUmElZGTsSICm7g1YuBtgX8y3sigivlgpFuVvxsIx82xruW3iUOJF+u2gj/6nbtGcC2pAVsvyg==", - "license": "MIT", - "engines": { - "node": ">= 18" - } - }, "node_modules/tinyspy": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", @@ -7119,18 +6478,6 @@ "node": ">=14.0.0" } }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7225,18 +6572,6 @@ "node": "*" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -7654,15 +6989,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/webdriver-bidi-protocol": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.9.tgz", @@ -7706,6 +7032,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7734,20 +7061,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", @@ -7859,23 +7172,6 @@ "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", - "integrity": "sha512-VwNCZKGt4QRuDPR5hfR7Xg2oecgv1/3/ncXElQPIfYjLb2pcIeRsf74vT+46kaFcYaCTi6TPDuiXEKWAO49HTg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bin-version-check": "~6.0.0", - "dargs": "~7.0.0", - "is-unix": "~2.0.10", - "simple-get": "~4.0.1", - "tinyspawn": "~1.2.6" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/backend/package.json b/backend/package.json index b926b5b..82fed66 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,8 +28,7 @@ "multer": "^1.4.5-lts.1", "node-cron": "^4.2.1", "puppeteer": "^24.31.0", - "uuid": "^13.0.0", - "youtube-dl-exec": "^2.4.17" + "uuid": "^13.0.0" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", diff --git a/backend/src/__tests__/services/commentService.test.ts b/backend/src/__tests__/services/commentService.test.ts index acee58f..0babe7f 100644 --- a/backend/src/__tests__/services/commentService.test.ts +++ b/backend/src/__tests__/services/commentService.test.ts @@ -1,85 +1,87 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import youtubedl from 'youtube-dl-exec'; -import { getComments } from '../../services/commentService'; -import * as storageService from '../../services/storageService'; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { getComments } from "../../services/commentService"; +import * as storageService from "../../services/storageService"; +import * as ytDlpUtils from "../../utils/ytDlpUtils"; -vi.mock('../../services/storageService'); -vi.mock('youtube-dl-exec'); +vi.mock("../../services/storageService"); +vi.mock("../../utils/ytDlpUtils"); -describe('CommentService', () => { +describe("CommentService", () => { beforeEach(() => { vi.clearAllMocks(); }); - describe('getComments', () => { - it('should return comments when video exists and youtube-dl succeeds', async () => { + describe("getComments", () => { + it("should return comments when video exists and youtube-dl succeeds", async () => { const mockVideo = { - id: 'video1', - sourceUrl: 'https://youtube.com/watch?v=123', + id: "video1", + sourceUrl: "https://youtube.com/watch?v=123", }; (storageService.getVideoById as any).mockReturnValue(mockVideo); const mockOutput = { comments: [ { - id: 'c1', - author: 'User1', - text: 'Great video!', + id: "c1", + author: "User1", + text: "Great video!", timestamp: 1600000000, }, { - id: 'c2', - author: '@User2', - text: 'Nice!', + id: "c2", + author: "@User2", + text: "Nice!", timestamp: 1600000000, }, ], }; - (youtubedl as any).mockResolvedValue(mockOutput); + (ytDlpUtils.executeYtDlpJson as any).mockResolvedValue(mockOutput); - const comments = await getComments('video1'); + const comments = await getComments("video1"); expect(comments).toHaveLength(2); expect(comments[0]).toEqual({ - id: 'c1', - author: 'User1', - content: 'Great video!', + id: "c1", + author: "User1", + content: "Great video!", date: expect.any(String), }); - expect(comments[1].author).toBe('User2'); // Check @ removal + expect(comments[1].author).toBe("User2"); // Check @ removal }); - it('should return empty array if video not found', async () => { + it("should return empty array if video not found", async () => { (storageService.getVideoById as any).mockReturnValue(null); - const comments = await getComments('non-existent'); + const comments = await getComments("non-existent"); expect(comments).toEqual([]); - expect(youtubedl).not.toHaveBeenCalled(); + expect(ytDlpUtils.executeYtDlpJson).not.toHaveBeenCalled(); }); - it('should return empty array if youtube-dl fails', async () => { + it("should return empty array if youtube-dl fails", async () => { const mockVideo = { - id: 'video1', - sourceUrl: 'https://youtube.com/watch?v=123', + id: "video1", + sourceUrl: "https://youtube.com/watch?v=123", }; (storageService.getVideoById as any).mockReturnValue(mockVideo); - (youtubedl as any).mockRejectedValue(new Error('Download failed')); + (ytDlpUtils.executeYtDlpJson as any).mockRejectedValue( + new Error("Download failed") + ); - const comments = await getComments('video1'); + const comments = await getComments("video1"); expect(comments).toEqual([]); }); - it('should return empty array if no comments in output', async () => { + it("should return empty array if no comments in output", async () => { const mockVideo = { - id: 'video1', - sourceUrl: 'https://youtube.com/watch?v=123', + id: "video1", + sourceUrl: "https://youtube.com/watch?v=123", }; (storageService.getVideoById as any).mockReturnValue(mockVideo); - (youtubedl as any).mockResolvedValue({}); + (ytDlpUtils.executeYtDlpJson as any).mockResolvedValue({}); - const comments = await getComments('video1'); + const comments = await getComments("video1"); expect(comments).toEqual([]); }); diff --git a/backend/src/server.ts b/backend/src/server.ts index 00fc434..767505c 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -7,7 +7,7 @@ import express from "express"; import { IMAGES_DIR, SUBTITLES_DIR, VIDEOS_DIR } from "./config/paths"; import { runMigrations } from "./db/migrate"; import apiRoutes from "./routes/api"; -import settingsRoutes from './routes/settingsRoutes'; +import settingsRoutes from "./routes/settingsRoutes"; import downloadManager from "./services/downloadManager"; import * as storageService from "./services/storageService"; import { VERSION } from "./version"; @@ -38,27 +38,56 @@ const startServer = async () => { // Initialize download manager (restore queued tasks) downloadManager.initialize(); - // Serve static files - app.use("/videos", express.static(VIDEOS_DIR)); + // Serve static files with proper MIME types + app.use( + "/videos", + express.static(VIDEOS_DIR, { + setHeaders: (res, path) => { + if (path.endsWith(".mp4")) { + res.setHeader("Content-Type", "video/mp4"); + } else if (path.endsWith(".webm")) { + res.setHeader("Content-Type", "video/webm"); + } + }, + }) + ); app.use("/images", express.static(IMAGES_DIR)); - app.use("/subtitles", express.static(SUBTITLES_DIR)); + app.use( + "/subtitles", + express.static(SUBTITLES_DIR, { + setHeaders: (res, path) => { + if (path.endsWith(".vtt")) { + res.setHeader("Content-Type", "text/vtt"); + res.setHeader("Access-Control-Allow-Origin", "*"); + } + }, + }) + ); // API Routes app.use("/api", apiRoutes); - app.use('/api/settings', settingsRoutes); + app.use("/api/settings", settingsRoutes); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); - + // Start subscription scheduler - import("./services/subscriptionService").then(({ subscriptionService }) => { - subscriptionService.startScheduler(); - }).catch(err => console.error("Failed to start subscription service:", err)); + import("./services/subscriptionService") + .then(({ subscriptionService }) => { + subscriptionService.startScheduler(); + }) + .catch((err) => + console.error("Failed to start subscription service:", err) + ); // Run duration backfill in background - import("./services/metadataService").then(service => { - service.backfillDurations(); - }).catch(err => console.error("Failed to start metadata service:", err)); + import("./services/metadataService") + .then((service) => { + service.backfillDurations(); + }) + .catch((err) => + console.error("Failed to start metadata service:", err) + ); }); } catch (error) { console.error("Failed to start server:", error); @@ -67,4 +96,3 @@ const startServer = async () => { }; startServer(); - diff --git a/backend/src/services/commentService.ts b/backend/src/services/commentService.ts index a2d6a31..ee7362b 100644 --- a/backend/src/services/commentService.ts +++ b/backend/src/services/commentService.ts @@ -1,4 +1,4 @@ -import youtubedl from "youtube-dl-exec"; +import { executeYtDlpJson } from "../utils/ytDlpUtils"; import * as storageService from "./storageService"; export interface Comment { @@ -17,44 +17,43 @@ export const getComments = async (videoId: string): Promise => { throw new Error("Video not found"); } - // Use youtube-dl for both Bilibili and YouTube as it's more reliable - return await getCommentsWithYoutubeDl(video.sourceUrl); + // Use yt-dlp for both Bilibili and YouTube as it's more reliable + return await getCommentsWithYtDlp(video.sourceUrl); } catch (error) { console.error("Error fetching comments:", error); return []; } }; -// Fetch comments using youtube-dl (works for YouTube and Bilibili) -const getCommentsWithYoutubeDl = async (url: string): Promise => { +// Fetch comments using yt-dlp (works for YouTube and Bilibili) +const getCommentsWithYtDlp = async (url: string): Promise => { try { - console.log(`[CommentService] Fetching comments using youtube-dl for: ${url}`); - const output = await youtubedl(url, { - getComments: true, - dumpSingleJson: true, + console.log(`[CommentService] Fetching comments using yt-dlp for: ${url}`); + const info = await executeYtDlpJson(url, { + writeComments: true, // Include comments in JSON output noWarnings: true, playlistEnd: 1, // Ensure we only process one video extractorArgs: "youtube:max_comments=20,all_comments=false", - } as any); + }); - const info = output as any; - if (info.comments) { - // Sort by date (newest first) and take top 10 - // Note: youtube-dl comments structure might vary - return info.comments - .slice(0, 10) - .map((comment: any) => ({ - id: comment.id, - author: comment.author.startsWith('@') ? comment.author.substring(1) : comment.author, - content: comment.text, - date: comment.timestamp ? new Date(comment.timestamp * 1000).toISOString().split('T')[0] : 'Unknown', - })); + // Sort by date (newest first) and take top 10 + // Note: yt-dlp comments structure might vary + return info.comments.slice(0, 10).map((comment: any) => ({ + id: comment.id || comment.comment_id || String(Math.random()), + author: comment.author?.startsWith("@") + ? comment.author.substring(1) + : comment.author || "Unknown", + content: comment.text || comment.content || "", + date: comment.timestamp + ? new Date(comment.timestamp * 1000).toISOString().split("T")[0] + : comment.time || "Unknown", + })); } return []; } catch (error) { - console.error("Error fetching comments with youtube-dl:", error); + console.error("Error fetching comments with yt-dlp:", error); return []; } }; diff --git a/backend/src/services/downloaders/BilibiliDownloader.ts b/backend/src/services/downloaders/BilibiliDownloader.ts index 04ec591..ea8d6e1 100644 --- a/backend/src/services/downloaders/BilibiliDownloader.ts +++ b/backend/src/services/downloaders/BilibiliDownloader.ts @@ -719,7 +719,45 @@ export class BilibiliDownloader { createdAt: new Date().toISOString(), }; - // Save the video using storage service + // Check if video with same sourceUrl already exists + const existingVideo = storageService.getVideoBySourceUrl(url); + + if (existingVideo) { + // Update existing video with new subtitle information and file paths + console.log( + "Video with same sourceUrl exists, updating subtitle information" + ); + + // Use existing video's ID and preserve other fields + videoData.id = existingVideo.id; + videoData.addedAt = existingVideo.addedAt; + videoData.createdAt = existingVideo.createdAt; + + const updatedVideo = storageService.updateVideo(existingVideo.id, { + subtitles: subtitles.length > 0 ? subtitles : undefined, + videoFilename: finalVideoFilename, + videoPath: `/videos/${finalVideoFilename}`, + thumbnailFilename: thumbnailSaved + ? finalThumbnailFilename + : existingVideo.thumbnailFilename, + thumbnailPath: thumbnailSaved + ? `/images/${finalThumbnailFilename}` + : existingVideo.thumbnailPath, + duration: duration, + fileSize: fileSize, + title: videoData.title, // Update title in case it changed + description: videoData.description, // Update description in case it changed + }); + + if (updatedVideo) { + console.log( + `Part ${partNumber}/${totalParts} updated in database with new subtitles` + ); + return { success: true, videoData: updatedVideo }; + } + } + + // Save the video (new video) storageService.saveVideo(videoData); console.log(`Part ${partNumber}/${totalParts} added to database`); diff --git a/backend/src/services/downloaders/YtDlpDownloader.ts b/backend/src/services/downloaders/YtDlpDownloader.ts index 02c089e..ad3c486 100644 --- a/backend/src/services/downloaders/YtDlpDownloader.ts +++ b/backend/src/services/downloaders/YtDlpDownloader.ts @@ -486,7 +486,43 @@ export class YtDlpDownloader { console.error("Failed to get file size:", e); } - // Save the video + // Check if video with same sourceUrl already exists + const existingVideo = storageService.getVideoBySourceUrl(videoUrl); + + if (existingVideo) { + // Update existing video with new subtitle information and file paths + console.log( + "Video with same sourceUrl exists, updating subtitle information" + ); + + // Use existing video's ID and preserve other fields + videoData.id = existingVideo.id; + videoData.addedAt = existingVideo.addedAt; + videoData.createdAt = existingVideo.createdAt; + + const updatedVideo = storageService.updateVideo(existingVideo.id, { + subtitles: subtitles.length > 0 ? subtitles : undefined, + videoFilename: finalVideoFilename, + videoPath: `/videos/${finalVideoFilename}`, + thumbnailFilename: thumbnailSaved + ? finalThumbnailFilename + : existingVideo.thumbnailFilename, + thumbnailPath: thumbnailSaved + ? `/images/${finalThumbnailFilename}` + : existingVideo.thumbnailPath, + duration: videoData.duration, + fileSize: videoData.fileSize, + title: videoData.title, // Update title in case it changed + description: videoData.description, // Update description in case it changed + }); + + if (updatedVideo) { + console.log("Video updated in database with new subtitles"); + return updatedVideo; + } + } + + // Save the video (new video) storageService.saveVideo(videoData); console.log("Video added to database"); diff --git a/backend/src/services/storageService.ts b/backend/src/services/storageService.ts index 106ab40..2429ee2 100644 --- a/backend/src/services/storageService.ts +++ b/backend/src/services/storageService.ts @@ -350,16 +350,22 @@ export function initializeStorage(): void { // Backfill video_id in download_history for existing records try { - const result = sqlite.prepare(` + const result = sqlite + .prepare( + ` UPDATE download_history SET video_id = (SELECT id FROM videos WHERE videos.source_url = download_history.source_url) WHERE video_id IS NULL AND status = 'success' AND source_url IS NOT NULL - `).run(); - if (result.changes > 0) { - console.log(`Backfilled video_id for ${result.changes} download history items.`); - } + ` + ) + .run(); + if (result.changes > 0) { + console.log( + `Backfilled video_id for ${result.changes} download history items.` + ); + } } catch (error) { - console.error("Error backfilling video_id in download history:", error); + console.error("Error backfilling video_id in download history:", error); } } catch (error) { console.error( @@ -487,10 +493,7 @@ export function getDownloadStatus(): DownloadStatus { const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; db.delete(downloads) .where( - and( - lt(downloads.timestamp, oneDayAgo), - eq(downloads.status, "active") - ) + and(lt(downloads.timestamp, oneDayAgo), eq(downloads.status, "active")) ) .run(); @@ -818,6 +821,28 @@ export function getVideos(): Video[] { } } +export function getVideoBySourceUrl(sourceUrl: string): Video | undefined { + try { + const result = db + .select() + .from(videos) + .where(eq(videos.sourceUrl, sourceUrl)) + .get(); + + if (result) { + return { + ...result, + tags: result.tags ? JSON.parse(result.tags) : [], + subtitles: result.subtitles ? JSON.parse(result.subtitles) : undefined, + } as Video; + } + return undefined; + } catch (error) { + console.error("Error getting video by sourceUrl:", error); + return undefined; + } +} + export function getVideoById(id: string): Video | undefined { try { const video = db.select().from(videos).where(eq(videos.id, id)).get(); @@ -838,217 +863,279 @@ export function getVideoById(id: string): Video | undefined { /** * Format legacy filenames to the new standard format: Title-Author-YYYY */ -export function formatLegacyFilenames(): { - processed: number; - renamed: number; - errors: number; - details: string[] +export function formatLegacyFilenames(): { + processed: number; + renamed: number; + errors: number; + details: string[]; } { - const results = { - processed: 0, - renamed: 0, - errors: 0, - details: [] as string[] - }; + const results = { + processed: 0, + renamed: 0, + errors: 0, + details: [] as string[], + }; - try { - const allVideos = getVideos(); - console.log(`Starting legacy filename formatting for ${allVideos.length} videos...`); + try { + const allVideos = getVideos(); + console.log( + `Starting legacy filename formatting for ${allVideos.length} videos...` + ); - for (const video of allVideos) { - results.processed++; - - try { - // Generate new filename - const newBaseFilename = formatVideoFilename(video.title, video.author || "Unknown", video.date); - - // preserve subdirectory if it exists (e.g. for collections) - // We rely on videoPath because videoFilename is usually just the basename - let subdirectory = ""; - if (video.videoPath) { - // videoPath is like "/videos/SubDir/file.mp4" or "/videos/file.mp4" - const relPath = video.videoPath.replace(/^\/videos\//, ""); - const dir = path.dirname(relPath); - if (dir && dir !== ".") { - subdirectory = dir; - } - } + for (const video of allVideos) { + results.processed++; - // New filename (basename only) - const newVideoFilename = `${newBaseFilename}.mp4`; - const newThumbnailFilename = `${newBaseFilename}.jpg`; + try { + // Generate new filename + const newBaseFilename = formatVideoFilename( + video.title, + video.author || "Unknown", + video.date + ); - // Calculate full paths for checks - // For the check we need to know if the resulting full path is different - // But the check "video.videoFilename === newVideoFilename" only checks basename. - // If basename matches, we might still want to rename if we were normalizing something else, - // but usually if format matches, we skip. - if (video.videoFilename === newVideoFilename) { - continue; - } - - console.log(`Renaming video ${video.id}: ${video.videoFilename} -> ${newVideoFilename} (Subdir: ${subdirectory})`); - - // Paths - // Old path must be constructed using the subdirectory derived from videoPath - const oldVideoPath = path.join(VIDEOS_DIR, subdirectory, video.videoFilename || ""); - const newVideoPath = path.join(VIDEOS_DIR, subdirectory, newVideoFilename); - - // Handle thumbnail subdirectory - let thumbSubdir = ""; - if (video.thumbnailPath) { - const relPath = video.thumbnailPath.replace(/^\/images\//, ""); - const dir = path.dirname(relPath); - if (dir && dir !== ".") { - thumbSubdir = dir; - } - } - - const oldThumbnailPath = video.thumbnailFilename ? path.join(IMAGES_DIR, thumbSubdir, video.thumbnailFilename) : null; - const newThumbnailPath = path.join(IMAGES_DIR, thumbSubdir, newThumbnailFilename); - - // Rename video file - if (fs.existsSync(oldVideoPath)) { - if (fs.existsSync(newVideoPath) && oldVideoPath !== newVideoPath) { - // Destination exists, append timestamp to avoid collision - const uniqueSuffix = `_${Date.now()}`; - const uniqueBase = `${newBaseFilename}${uniqueSuffix}`; - - const uniqueVideoBase = `${uniqueBase}.mp4`; - const uniqueThumbBase = `${uniqueBase}.jpg`; - - // Full paths for rename - const uniqueVideoPath = path.join(VIDEOS_DIR, subdirectory, uniqueVideoBase); - const uniqueThumbPath = path.join(IMAGES_DIR, thumbSubdir, uniqueThumbBase); // Use thumbSubdir - - console.log(`Destination exists, using unique suffix: ${uniqueVideoBase}`); - - fs.renameSync(oldVideoPath, uniqueVideoPath); - - if (oldThumbnailPath && fs.existsSync(oldThumbnailPath)) { - fs.renameSync(oldThumbnailPath, uniqueThumbPath); - } - - // Handle subtitles (Keep in their original folder, assuming root or derived from path if available) - if (video.subtitles && video.subtitles.length > 0) { - const newSubtitles = []; - for (const subtitle of video.subtitles) { - // Subtitles usually in SUBTITLES_DIR root, checking... - const oldSubPath = path.join(SUBTITLES_DIR, subtitle.filename); - - // If we ever supported subdirs for subtitles, we'd need to parse subtitle.path here too - // For now assuming existing structure matches simple join - - if (fs.existsSync(oldSubPath)) { - const newSubFilename = `${uniqueBase}.${subtitle.language}.vtt`; - const newSubPath = path.join(SUBTITLES_DIR, newSubFilename); - fs.renameSync(oldSubPath, newSubPath); - newSubtitles.push({ - ...subtitle, - filename: newSubFilename, - path: `/subtitles/${newSubFilename}` - }); - } else { - newSubtitles.push(subtitle); - } - } - // Update video record with unique names - // videoFilename should be BASENAME only - // videoPath should be FULL WEB PATH including subdir - db.update(videos) - .set({ - videoFilename: uniqueVideoBase, - thumbnailFilename: video.thumbnailFilename ? uniqueThumbBase : undefined, - videoPath: `/videos/${subdirectory ? subdirectory + '/' : ''}${uniqueVideoBase}`, - thumbnailPath: video.thumbnailFilename ? `/images/${thumbSubdir ? thumbSubdir + '/' : ''}${uniqueThumbBase}` : null, - subtitles: JSON.stringify(newSubtitles) - }) - .where(eq(videos.id, video.id)) - .run(); - } else { - // Update video record with unique names - db.update(videos) - .set({ - videoFilename: uniqueVideoBase, - thumbnailFilename: video.thumbnailFilename ? uniqueThumbBase : undefined, - videoPath: `/videos/${subdirectory ? subdirectory + '/' : ''}${uniqueVideoBase}`, - thumbnailPath: video.thumbnailFilename ? `/images/${thumbSubdir ? thumbSubdir + '/' : ''}${uniqueThumbBase}` : null, - }) - .where(eq(videos.id, video.id)) - .run(); - } - - results.renamed++; - results.details.push(`Renamed (unique): ${video.title}`); - } else { - // Rename normally - fs.renameSync(oldVideoPath, newVideoPath); - - if (oldThumbnailPath && fs.existsSync(oldThumbnailPath)) { - // Check if new thumbnail path exists (it shouldn't if specific to this video, but safety check) - if (fs.existsSync(newThumbnailPath) && oldThumbnailPath !== newThumbnailPath) { - fs.unlinkSync(newThumbnailPath); - } - fs.renameSync(oldThumbnailPath, newThumbnailPath); - } - - // Handle subtitles - const updatedSubtitles = []; - if (video.subtitles && video.subtitles.length > 0) { - for (const subtitle of video.subtitles) { - const oldSubPath = path.join(SUBTITLES_DIR, subtitle.filename); - if (fs.existsSync(oldSubPath)) { - // Keep subtitles in their current location (usually root SUBTITLES_DIR) - const newSubFilename = `${newBaseFilename}.${subtitle.language}.vtt`; - const newSubPath = path.join(SUBTITLES_DIR, newSubFilename); - - // Remove dest if exists - if (fs.existsSync(newSubPath)) fs.unlinkSync(newSubPath); - - fs.renameSync(oldSubPath, newSubPath); - updatedSubtitles.push({ - ...subtitle, - filename: newSubFilename, - path: `/subtitles/${newSubFilename}` - }); - } else { - updatedSubtitles.push(subtitle); - } - } - } - - // Update DB - db.update(videos) - .set({ - videoFilename: newVideoFilename, - thumbnailFilename: video.thumbnailFilename ? newThumbnailFilename : undefined, - videoPath: `/videos/${subdirectory ? subdirectory + '/' : ''}${newVideoFilename}`, - thumbnailPath: video.thumbnailFilename ? `/images/${thumbSubdir ? thumbSubdir + '/' : ''}${newThumbnailFilename}` : null, - subtitles: updatedSubtitles.length > 0 ? JSON.stringify(updatedSubtitles) : (video.subtitles ? JSON.stringify(video.subtitles) : undefined) - }) - .where(eq(videos.id, video.id)) - .run(); - - results.renamed++; - } - } else { - results.details.push(`Skipped (file missing): ${video.title}`); - // results.errors++; // Not necessarily an error, maybe just missing file - } - - } catch (err: any) { - console.error(`Error renaming video ${video.id}:`, err); - results.errors++; - results.details.push(`Error: ${video.title} - ${err.message}`); - } + // preserve subdirectory if it exists (e.g. for collections) + // We rely on videoPath because videoFilename is usually just the basename + let subdirectory = ""; + if (video.videoPath) { + // videoPath is like "/videos/SubDir/file.mp4" or "/videos/file.mp4" + const relPath = video.videoPath.replace(/^\/videos\//, ""); + const dir = path.dirname(relPath); + if (dir && dir !== ".") { + subdirectory = dir; + } } - return results; + // New filename (basename only) + const newVideoFilename = `${newBaseFilename}.mp4`; + const newThumbnailFilename = `${newBaseFilename}.jpg`; - } catch (error: any) { - console.error("Error in formatLegacyFilenames:", error); - throw error; + // Calculate full paths for checks + // For the check we need to know if the resulting full path is different + // But the check "video.videoFilename === newVideoFilename" only checks basename. + // If basename matches, we might still want to rename if we were normalizing something else, + // but usually if format matches, we skip. + if (video.videoFilename === newVideoFilename) { + continue; + } + + console.log( + `Renaming video ${video.id}: ${video.videoFilename} -> ${newVideoFilename} (Subdir: ${subdirectory})` + ); + + // Paths + // Old path must be constructed using the subdirectory derived from videoPath + const oldVideoPath = path.join( + VIDEOS_DIR, + subdirectory, + video.videoFilename || "" + ); + const newVideoPath = path.join( + VIDEOS_DIR, + subdirectory, + newVideoFilename + ); + + // Handle thumbnail subdirectory + let thumbSubdir = ""; + if (video.thumbnailPath) { + const relPath = video.thumbnailPath.replace(/^\/images\//, ""); + const dir = path.dirname(relPath); + if (dir && dir !== ".") { + thumbSubdir = dir; + } + } + + const oldThumbnailPath = video.thumbnailFilename + ? path.join(IMAGES_DIR, thumbSubdir, video.thumbnailFilename) + : null; + const newThumbnailPath = path.join( + IMAGES_DIR, + thumbSubdir, + newThumbnailFilename + ); + + // Rename video file + if (fs.existsSync(oldVideoPath)) { + if (fs.existsSync(newVideoPath) && oldVideoPath !== newVideoPath) { + // Destination exists, append timestamp to avoid collision + const uniqueSuffix = `_${Date.now()}`; + const uniqueBase = `${newBaseFilename}${uniqueSuffix}`; + + const uniqueVideoBase = `${uniqueBase}.mp4`; + const uniqueThumbBase = `${uniqueBase}.jpg`; + + // Full paths for rename + const uniqueVideoPath = path.join( + VIDEOS_DIR, + subdirectory, + uniqueVideoBase + ); + const uniqueThumbPath = path.join( + IMAGES_DIR, + thumbSubdir, + uniqueThumbBase + ); // Use thumbSubdir + + console.log( + `Destination exists, using unique suffix: ${uniqueVideoBase}` + ); + + fs.renameSync(oldVideoPath, uniqueVideoPath); + + if (oldThumbnailPath && fs.existsSync(oldThumbnailPath)) { + fs.renameSync(oldThumbnailPath, uniqueThumbPath); + } + + // Handle subtitles (Keep in their original folder, assuming root or derived from path if available) + if (video.subtitles && video.subtitles.length > 0) { + const newSubtitles = []; + for (const subtitle of video.subtitles) { + // Subtitles usually in SUBTITLES_DIR root, checking... + const oldSubPath = path.join(SUBTITLES_DIR, subtitle.filename); + + // If we ever supported subdirs for subtitles, we'd need to parse subtitle.path here too + // For now assuming existing structure matches simple join + + if (fs.existsSync(oldSubPath)) { + const newSubFilename = `${uniqueBase}.${subtitle.language}.vtt`; + const newSubPath = path.join(SUBTITLES_DIR, newSubFilename); + fs.renameSync(oldSubPath, newSubPath); + newSubtitles.push({ + ...subtitle, + filename: newSubFilename, + path: `/subtitles/${newSubFilename}`, + }); + } else { + newSubtitles.push(subtitle); + } + } + // Update video record with unique names + // videoFilename should be BASENAME only + // videoPath should be FULL WEB PATH including subdir + db.update(videos) + .set({ + videoFilename: uniqueVideoBase, + thumbnailFilename: video.thumbnailFilename + ? uniqueThumbBase + : undefined, + videoPath: `/videos/${ + subdirectory ? subdirectory + "/" : "" + }${uniqueVideoBase}`, + thumbnailPath: video.thumbnailFilename + ? `/images/${ + thumbSubdir ? thumbSubdir + "/" : "" + }${uniqueThumbBase}` + : null, + subtitles: JSON.stringify(newSubtitles), + }) + .where(eq(videos.id, video.id)) + .run(); + } else { + // Update video record with unique names + db.update(videos) + .set({ + videoFilename: uniqueVideoBase, + thumbnailFilename: video.thumbnailFilename + ? uniqueThumbBase + : undefined, + videoPath: `/videos/${ + subdirectory ? subdirectory + "/" : "" + }${uniqueVideoBase}`, + thumbnailPath: video.thumbnailFilename + ? `/images/${ + thumbSubdir ? thumbSubdir + "/" : "" + }${uniqueThumbBase}` + : null, + }) + .where(eq(videos.id, video.id)) + .run(); + } + + results.renamed++; + results.details.push(`Renamed (unique): ${video.title}`); + } else { + // Rename normally + fs.renameSync(oldVideoPath, newVideoPath); + + if (oldThumbnailPath && fs.existsSync(oldThumbnailPath)) { + // Check if new thumbnail path exists (it shouldn't if specific to this video, but safety check) + if ( + fs.existsSync(newThumbnailPath) && + oldThumbnailPath !== newThumbnailPath + ) { + fs.unlinkSync(newThumbnailPath); + } + fs.renameSync(oldThumbnailPath, newThumbnailPath); + } + + // Handle subtitles + const updatedSubtitles = []; + if (video.subtitles && video.subtitles.length > 0) { + for (const subtitle of video.subtitles) { + const oldSubPath = path.join(SUBTITLES_DIR, subtitle.filename); + if (fs.existsSync(oldSubPath)) { + // Keep subtitles in their current location (usually root SUBTITLES_DIR) + const newSubFilename = `${newBaseFilename}.${subtitle.language}.vtt`; + const newSubPath = path.join(SUBTITLES_DIR, newSubFilename); + + // Remove dest if exists + if (fs.existsSync(newSubPath)) fs.unlinkSync(newSubPath); + + fs.renameSync(oldSubPath, newSubPath); + updatedSubtitles.push({ + ...subtitle, + filename: newSubFilename, + path: `/subtitles/${newSubFilename}`, + }); + } else { + updatedSubtitles.push(subtitle); + } + } + } + + // Update DB + db.update(videos) + .set({ + videoFilename: newVideoFilename, + thumbnailFilename: video.thumbnailFilename + ? newThumbnailFilename + : undefined, + videoPath: `/videos/${ + subdirectory ? subdirectory + "/" : "" + }${newVideoFilename}`, + thumbnailPath: video.thumbnailFilename + ? `/images/${ + thumbSubdir ? thumbSubdir + "/" : "" + }${newThumbnailFilename}` + : null, + subtitles: + updatedSubtitles.length > 0 + ? JSON.stringify(updatedSubtitles) + : video.subtitles + ? JSON.stringify(video.subtitles) + : undefined, + }) + .where(eq(videos.id, video.id)) + .run(); + + results.renamed++; + } + } else { + results.details.push(`Skipped (file missing): ${video.title}`); + // results.errors++; // Not necessarily an error, maybe just missing file + } + } catch (err: any) { + console.error(`Error renaming video ${video.id}:`, err); + results.errors++; + results.details.push(`Error: ${video.title} - ${err.message}`); + } } + + return results; + } catch (error: any) { + console.error("Error in formatLegacyFilenames:", error); + throw error; + } } export function saveVideo(videoData: Video): Video { @@ -1079,8 +1166,16 @@ export function updateVideo(id: string, updates: Partial