diff --git a/package-lock.json b/package-lock.json index 2de7474..d43fb16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "data-uri-to-buffer": "^6.0.2", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-handlebars": "^8.0.1", + "git-rev-sync": "^3.0.2", "node-widevine": "https://github.com/wangziyingwen/node-widevine", "parse-hls": "^1.0.7", "pssh-tools": "^1.2.0", @@ -26,6 +28,7 @@ }, "devDependencies": { "@types/express": "^5.0.0", + "@types/git-rev-sync": "^2.0.2", "@types/source-map-support": "^0.5.10", "@typescript-eslint/parser": "^7.12.0", "concurrently": "^9.1.2", @@ -373,6 +376,102 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -522,6 +621,13 @@ "@types/send": "*" } }, + "node_modules/@types/git-rev-sync": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/git-rev-sync/-/git-rev-sync-2.0.2.tgz", + "integrity": "sha512-ygFM5I5q4VJjU+xrb2MSzgj4BpC6HUzMnmfWp4d8bgAw/XFkJTiKn1uaNpOOT1gw+IxELyfY97JA6sRBv7J9sA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -861,7 +967,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -871,7 +976,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -942,7 +1046,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/body-parser": { @@ -988,7 +1091,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -1094,7 +1196,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1107,7 +1208,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -1126,7 +1226,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concurrently": { @@ -1241,7 +1340,6 @@ "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", @@ -1365,6 +1463,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1375,7 +1479,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -1708,6 +1811,58 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-handlebars": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-8.0.1.tgz", + "integrity": "sha512-mdas0PTbgQnwSyAjcYM7OMaftM8nJ3Kqz6yAyK4iCFvMOGGvh6pv42IHwcE5PBpS6ffYeZRSsgAdYUMG4CSjhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^11.0.0", + "graceful-fs": "^4.2.11", + "handlebars": "^4.7.8" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/express-handlebars/node_modules/glob": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", + "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/express-handlebars/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1902,6 +2057,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1924,7 +2095,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/function-bind": { @@ -1983,12 +2153,37 @@ "node": ">= 0.4" } }, + "node_modules/git-rev-sync": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/git-rev-sync/-/git-rev-sync-3.0.2.tgz", + "integrity": "sha512-Nd5RiYpyncjLv0j6IONy0lGzAqdRXUaBctuGBbrEA2m6Bn4iDrN/9MeQTXuiquw8AEKL9D2BW0nw5m/lQvxqnQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "1.0.5", + "graceful-fs": "4.1.15", + "shelljs": "0.8.5" + } + }, + "node_modules/git-rev-sync/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/git-rev-sync/node_modules/graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "license": "ISC" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -2022,7 +2217,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2033,7 +2227,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2091,6 +2284,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2098,6 +2297,27 @@ "dev": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2217,7 +2437,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -2230,6 +2449,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2239,6 +2467,21 @@ "node": ">= 0.10" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2253,7 +2496,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2296,9 +2538,23 @@ "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/jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2393,6 +2649,15 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2502,6 +2767,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2524,6 +2807,12 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -2572,7 +2861,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -2628,6 +2916,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2680,7 +2974,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2690,12 +2983,33 @@ "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" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -2857,6 +3171,17 @@ "node": ">= 0.8" } }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2867,6 +3192,26 @@ "node": ">=0.10.0" } }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3051,7 +3396,6 @@ "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" @@ -3064,7 +3408,6 @@ "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" @@ -3083,6 +3426,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -3155,6 +3515,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3197,7 +3569,21 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3212,7 +3598,19 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3247,6 +3645,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3397,6 +3807,19 @@ } } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -3444,7 +3867,6 @@ "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" @@ -3466,6 +3888,12 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -3484,11 +3912,28 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/y18n": { diff --git a/package.json b/package.json index 2907789..a32a75b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "amdl", "type": "module", "scripts": { - "dev": "concurrently 'node --watch dist/index.js' 'tsc --watch'", + "dev": "concurrently --prefix none 'node --watch dist/index.js' 'tsc --watch'", "build": "npm run lint && tsc", "lint": "eslint .", "lint:fix": "eslint . --fix" @@ -18,6 +18,8 @@ "data-uri-to-buffer": "^6.0.2", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-handlebars": "^8.0.1", + "git-rev-sync": "^3.0.2", "node-widevine": "https://github.com/wangziyingwen/node-widevine", "parse-hls": "^1.0.7", "pssh-tools": "^1.2.0", @@ -29,6 +31,7 @@ }, "devDependencies": { "@types/express": "^5.0.0", + "@types/git-rev-sync": "^2.0.2", "@types/source-map-support": "^0.5.10", "@typescript-eslint/parser": "^7.12.0", "concurrently": "^9.1.2", diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..70ad317 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/styles/main.css b/public/styles/main.css new file mode 100644 index 0000000..0ae6eb5 --- /dev/null +++ b/public/styles/main.css @@ -0,0 +1,116 @@ +:root { + font-family: system-ui; + font-size: 1em; + line-height: 1.25; + + color: var(--foreground); + background-color: var(--background); + + --foreground: #cdd6f4; + --foreground-muted: #a6adc8; + --background: #1e1e2e; + --background-surface: #313244; + --shadow: #11111b; +} + +body { + display: grid; + grid-template-columns: auto; + grid-template-rows: min-content auto min-content; + grid-auto-flow: column; + + width: 100%; + height: 100%; + min-width: 100vw; + min-height: 100vh; +} + +main { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1em; + padding: 1em; + width: 100%; + height: 100%; +} + +h1, h2, h3, h4, h5, h6, p { + margin-top: .8em; + margin-bottom: .8em; +} + +/* this isn't great in terms of accessibility */ +/* makes sense for most the stuff we got tho */ +a { + color: var(--foreground); + text-decoration: none; +} +a:hover { + color: var(--foreground-muted); + text-decoration: underline; +} + +.light { + color: var(--foreground-muted); +} + +input::placeholder { + color: var(--foreground-muted); +} + +input[type="search"] { + color: var(--foreground); + background-color: var(--background-surface); + border: 0; + border-radius: 0.5em; + padding: 0.5em 1em; + box-shadow: 0 0 2em var(--shadow); +} + +header { + background-color: var(--background-surface); + padding-left: 1em; + padding-right: 1em; + box-shadow: 0 0 2em var(--shadow); +} +header h1 { + font-size: 1.2em; +} + +footer { + background-color: var(--background-surface); + padding: 1em; + box-shadow: 0 0 2em var(--shadow); +} + +.result { + display: flex; + flex-direction: column; + width: 60%; + gap: 1em; + padding: 1em; + background-color: var(--background-surface); + box-shadow: 0 0 2em var(--shadow); + border-radius: 0.5em; +} +.result-info { + display: flex; + flex-direction: row; + gap: 1em; +} +.result-info img { + width: 5.5em; + height: 5.5em; + border-radius: 0.5em; +} +.result-text { + display: flex; + flex-direction: column; +} +.result-text h2 { + font-size: 1em; + margin-top: 0; + margin-bottom: 0; +} diff --git a/public/styles/normalize.css b/public/styles/normalize.css new file mode 100644 index 0000000..b0a14ee --- /dev/null +++ b/public/styles/normalize.css @@ -0,0 +1,223 @@ +/*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */ + +/* +Document +======== +*/ + +/** +Use a better box model (opinionated). +*/ + +*, +::before, +::after { + box-sizing: border-box; +} + +/** +1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) +2. Correct the line height in all browsers. +3. Prevent adjustments of font size after orientation changes in iOS. +4. Use a more readable tab size (opinionated). +*/ + +html { + font-family: + system-ui, + 'Segoe UI', + Roboto, + Helvetica, + Arial, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji'; /* 1 */ + line-height: 1.15; /* 2 */ + -webkit-text-size-adjust: 100%; /* 3 */ + tab-size: 4; /* 4 */ +} + +/* +Sections +======== +*/ + +/** +Remove the margin in all browsers. +*/ + +body { + margin: 0; +} + +/* +Text-level semantics +==================== +*/ + +/** +Add the correct font weight in Chrome and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/** +1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) +2. Correct the odd 'em' font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: + ui-monospace, + SFMono-Regular, + Consolas, + 'Liberation Mono', + Menlo, + monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/** +Prevent 'sub' and 'sup' elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +Tabular data +============ +*/ + +/** +Correct table border color inheritance in Chrome and Safari. (https://issues.chromium.org/issues/40615503, https://bugs.webkit.org/show_bug.cgi?id=195016) +*/ + +table { + border-color: currentcolor; +} + +/* +Forms +===== +*/ + +/** +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** +Correct the inability to style clickable types in iOS and Safari. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} + +/** +Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers. +*/ + +legend { + padding: 0; +} + +/** +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/** +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/** +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to 'inherit' in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* +Interactive +=========== +*/ + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} diff --git a/src/downloader/index.ts b/src/downloader/index.ts index a46e0b6..431a8d3 100644 --- a/src/downloader/index.ts +++ b/src/downloader/index.ts @@ -3,10 +3,9 @@ import { spawn } from "node:child_process"; import path from "node:path"; import { addToCache, isCached } from "../cache.js"; -// TODO: make this have a return type (file path) // TODO: refresh cache timer on download // TODO: remux to m4a? -export async function downloadSong(streamUrl: string, decryptionKey: string, songCodec: RegularCodec | WebplaybackCodec): Promise { +export async function downloadSong(streamUrl: string, decryptionKey: string, songCodec: RegularCodec | WebplaybackCodec): Promise { let baseOutputName = streamUrl.split("/").at(-1)?.split("?").at(0)?.split(".").splice(0, 1).join(".")?.trim(); if (!baseOutputName) { throw "could not get base output name from stream url"; } baseOutputName += `_${songCodec}`; @@ -18,7 +17,7 @@ export async function downloadSong(streamUrl: string, decryptionKey: string, son if ( // TODO: remove check for encrypted file/cache for encrypted? isCached(encryptedName) && isCached(decryptedName) - ) { return; } + ) { return decryptedPath; } await new Promise((res, rej) => { const child = spawn(config.downloader.ytdlp_path, [ @@ -50,6 +49,8 @@ export async function downloadSong(streamUrl: string, decryptionKey: string, son addToCache(encryptedName); addToCache(decryptedName); + + return decryptedPath; } // TODO: find a better spot for this diff --git a/src/downloader/streamInfo.ts b/src/downloader/streamInfo.ts index 2c95f79..91715c8 100644 --- a/src/downloader/streamInfo.ts +++ b/src/downloader/streamInfo.ts @@ -1,13 +1,11 @@ -import { appleMusicApi } from "../api/index.js"; import * as log from "../log.js"; import type { SongAttributes } from "../api/types/appleMusic/attributes.js"; import hls, { Item } from "parse-hls"; import axios from "axios"; -import { getWidevineDecryptionKey } from "./keygen.js"; import { widevine, playready, fairplay } from "../constants/keyFormats.js"; import { songCodecRegex } from "../constants/codecs.js"; import type { WebplaybackResponse } from "api/appleMusicApi.js"; -import { downloadSong, RegularCodec, WebplaybackCodec } from "./index.js"; +import { RegularCodec, WebplaybackCodec } from "./index.js"; // why is this private // i wish pain on the person who wrote this /j :smile: @@ -188,31 +186,3 @@ function getDrmData(drmInfos: DrmInfos, drmIds: string[], drmKey: string): strin const getWidevinePssh = (drmInfos: DrmInfos, drmIds: string[]): string | undefined => getDrmData(drmInfos, drmIds, widevine); const getPlayreadyPssh = (drmInfos: DrmInfos, drmIds: string[]): string | undefined => getDrmData(drmInfos, drmIds, playready); const getFairplayKey = (drmInfos: DrmInfos, drmIds: string[]): string | undefined => getDrmData(drmInfos, drmIds, fairplay); - -// TODO: remove later, this is just for testing -// const streamInfo2 = await StreamInfo.fromTrackMetadata((await appleMusicApi.getSong("1615276490")).data[0].attributes, RegularCodec.Aac); - -const streamCodec1 = WebplaybackCodec.AacLegacy; -const streamInfo1 = await StreamInfo.fromWebplayback(await appleMusicApi.getWebplayback("1705366148"), streamCodec1); -if (streamInfo1.widevinePssh !== undefined) { - await downloadSong( - streamInfo1.streamUrl, - await getWidevineDecryptionKey(streamInfo1.widevinePssh, streamInfo1.trackId), - streamCodec1 - ); -} - -// try { -// const streamCodec2 = RegularCodec.AacHe; -// const streamInfo2 = await StreamInfo.fromTrackMetadata((await appleMusicApi.getSong("1705366148")).data[0].attributes, streamCodec2); -// if (streamInfo2.widevinePssh !== undefined) { -// await downloadSong( -// streamInfo2.streamUrl, -// await getWidevineDecryptionKey(streamInfo2.widevinePssh, streamInfo2.trackId), -// streamCodec2 -// ); -// } -// } catch (err) { -// log.error("failed to download song"); -// log.error(err); -// } diff --git a/src/index.ts b/src/index.ts index 8806097..b2c1544 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,43 +1,8 @@ import { config } from "./config.js"; -import express, { type NextFunction, type Request, type Response } from "express"; import process from "node:process"; import * as log from "./log.js"; import { appleMusicApi } from "./api/index.js"; - -export class HttpException extends Error { - public readonly status?: number; - - constructor(status: number, message: string) { - super(message); - this.status = status; - this.message = message; - } -} - -const app = express(); - -app.disable("x-powered-by"); - -app.set("trust proxy", ["loopback", "uniquelocal"]); - -app.use("/", express.static("public")); - -app.use("/data", express.static(config.downloader.cache.directory, { extensions: ["mp4"] })); - -app.use((req, _res, next) => { - next(new HttpException(404, `${req.path} not found`)); -}); - -app.use((err: HttpException, _req: Request, res: Response, _next: NextFunction) => { - if (!err.status || err.status % 500 < 100) { - log.error(err); - } - - const status = err.status ?? 500; - const message = err.message; - - res.status(status).send(message); -}); +import { app } from "./web/index.js"; await appleMusicApi.login().catch((err) => { log.error("failed to login to apple music api"); diff --git a/src/web/endpoints/back/dlTrackMetadata.ts b/src/web/endpoints/back/dlTrackMetadata.ts new file mode 100644 index 0000000..9b40abc --- /dev/null +++ b/src/web/endpoints/back/dlTrackMetadata.ts @@ -0,0 +1,35 @@ +import { getWidevineDecryptionKey } from "../../../downloader/keygen.js"; +import { downloadSong, RegularCodec } from "../../../downloader/index.js"; +import express from "express"; +import StreamInfo from "../../../downloader/streamInfo.js"; +import { appleMusicApi } from "../../../api/index.js"; + +const router = express.Router(); + +// TODO: support more encryption schemes +// TODO: some type of agnostic-ness for the encryption keys +router.get("/dlTrackMetadata", async (req, res, next) => { + try { + const { trackId, codec } = req.query; + if (typeof trackId !== "string") { res.status(400).send("trackId is required and must be a string!"); return; } + if (typeof codec !== "string") { res.status(400).send("codec is required and must be a string!"); return; } + + const c = Object.values(RegularCodec).find((c) => { return c === codec; }); + if (c === undefined) { res.status(400).send("codec is invalid!"); return; } + + const trackMetadata = await appleMusicApi.getSong(trackId); + const trackAttributes = trackMetadata.data[0].attributes; + const streamInfo = await StreamInfo.fromTrackMetadata(trackAttributes, c); + if (streamInfo.widevinePssh !== undefined) { + const decryptionKey = await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId); + const filePath = await downloadSong(streamInfo.streamUrl, decryptionKey, c); + res.download(filePath); + } else { + res.status(400).send("no decryption key found!"); + } + } catch (err) { + next(err); + } +}); + +export default router; diff --git a/src/web/endpoints/back/dlWebplayback.ts b/src/web/endpoints/back/dlWebplayback.ts new file mode 100644 index 0000000..c2cd751 --- /dev/null +++ b/src/web/endpoints/back/dlWebplayback.ts @@ -0,0 +1,34 @@ +import { getWidevineDecryptionKey } from "../../../downloader/keygen.js"; +import { downloadSong, WebplaybackCodec } from "../../../downloader/index.js"; +import express from "express"; +import StreamInfo from "../../../downloader/streamInfo.js"; +import { appleMusicApi } from "../../../api/index.js"; + +const router = express.Router(); + +router.get("/dlWebplayback", async (req, res, next) => { + try { + const { trackId, codec } = req.query; + if (typeof trackId !== "string") { res.status(400).send("trackId is required and must be a string!"); return; } + if (typeof codec !== "string") { res.status(400).send("codec is required and must be a string!"); return; } + + const c = Object.values(WebplaybackCodec).find((c) => { return c === codec; }); + if (c === undefined) { res.status(400).send("codec is invalid!"); return; } + + // TODO: check if this returns an error + const webplaybackResponse = await appleMusicApi.getWebplayback(trackId); + console.log(webplaybackResponse); + const streamInfo = await StreamInfo.fromWebplayback(webplaybackResponse, c); + if (streamInfo.widevinePssh !== undefined) { + const decryptionKey = await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId); + const filePath = await downloadSong(streamInfo.streamUrl, decryptionKey, c); + res.download(filePath); + } else { + res.status(400).send("no decryption key found!"); + } + } catch (err) { + next(err); + } +}); + +export default router; diff --git a/src/web/endpoints/back/getTrackMetadata.ts b/src/web/endpoints/back/getTrackMetadata.ts new file mode 100644 index 0000000..a16e436 --- /dev/null +++ b/src/web/endpoints/back/getTrackMetadata.ts @@ -0,0 +1,23 @@ +import { appleMusicApi } from "../../../api/index.js"; +import express from "express"; + +const router = express.Router(); + +// this endpoint isn't actually used for anything by us +// it's for people who want to implement apple music downloading into their own apps +// it makes it a bit easier to get the metadata for a track knowing the trackId +router.get("/getTrackMetadata", async (req, res, next) => { + try { + const { trackId } = req.query; + if (typeof trackId !== "string") { res.status(400).send("trackId is required and must be a string!"); return; } + + const trackMetadata = await appleMusicApi.getSong(trackId); + const trackAttributes = trackMetadata.data[0].attributes; + + res.json(trackAttributes); + } catch (err) { + next(err); + } +}); + +export default router; diff --git a/src/web/endpoints/front/search.ts b/src/web/endpoints/front/search.ts new file mode 100644 index 0000000..3a7fcb3 --- /dev/null +++ b/src/web/endpoints/front/search.ts @@ -0,0 +1,45 @@ +import express from "express"; +import gitRepoInfo from "git-rev-sync"; + +// TODO: move this into a helper or whatever? +// i don't wanna do this for every single page lol +const hash = gitRepoInfo.short("./"); +const dirty = gitRepoInfo.isDirty(); + +const router = express.Router(); + +// TODO: implement this +// TODO: show tracks +// TODO: add a download button +router.get("/", (req, res) => { + res.render("search", { + title: "search", + hash: hash, + dirty: dirty, + query: req.query.q, + results: [ + { + name: "Revengeseekerz", + artists: ["Jane Remover"], + cover: "https://is1-ssl.mzstatic.com/image/thumb/Music221/v4/18/cf/f6/18cff6df-c7b6-0ca1-8067-83743f6c1f8a/193436418720_coverGriffinMcMahon.jpg/592x592bb.webp" + } + // { + // name: "Carousel (An Examination of the Shadow, Creekflow, And its Life as an Afterthought) ", + // artists: ["Vylet Pony"], + // tracks: [ + // { + // artists: ["Vylet Pony"], + // name: "Carousel" + // }, + // { + // artists: ["Vylet Pony", "Namii"], + // name: "The Shadow" + // } + // ], + // cover: "https://is1-ssl.mzstatic.com/image/thumb/Music116/v4/7c/f0/94/7cf09429-4942-a9cb-1287-b8bbc53a4d61/artwork.jpg/592x592bb.webp" + // } + ] + }); +}); + +export default router; diff --git a/src/web/index.ts b/src/web/index.ts new file mode 100644 index 0000000..657b069 --- /dev/null +++ b/src/web/index.ts @@ -0,0 +1,50 @@ +import * as log from "../log.js"; +import express, { type NextFunction, type Request, type Response } from "express"; +import { engine } from "express-handlebars"; + +import dlTrackMetadata from "./endpoints/back/dlTrackMetadata.js"; +import dlWebplayback from "./endpoints/back/dlWebplayback.js"; +import getTrackMetadata from "./endpoints/back/getTrackMetadata.js"; +import search from "./endpoints/front/search.js"; + +export class HttpException extends Error { + public readonly status?: number; + + constructor(status: number, message: string) { + super(message); + this.status = status; + this.message = message; + } +} + +const app = express(); + +app.set("trust proxy", ["loopback", "uniquelocal"]); + +app.engine("handlebars", engine()); +app.set("view engine", "handlebars"); +app.set("views", "./views"); + +app.use("/", express.static("public")); + +app.use(dlTrackMetadata); +app.use(dlWebplayback); +app.use(getTrackMetadata); +app.use(search); + +app.use((req, _res, next) => { + next(new HttpException(404, `${req.path} not found`)); +}); + +app.use((err: HttpException, _req: Request, res: Response, _next: NextFunction) => { + if (!err.status || err.status % 500 < 100) { + log.error(err); + } + + const status = err.status ?? 500; + const message = err.message; + + res.status(status).send(message); +}); + +export { app }; diff --git a/views/index.handlebars b/views/index.handlebars new file mode 100644 index 0000000..49e8431 --- /dev/null +++ b/views/index.handlebars @@ -0,0 +1 @@ +{{> search}} diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars new file mode 100644 index 0000000..fee94d1 --- /dev/null +++ b/views/layouts/main.handlebars @@ -0,0 +1,18 @@ + + + + + + + + + amdl - {{title}} + + + {{> header}} +
+ {{{body}}} +
+ {{> footer}} + + diff --git a/views/partials/footer.handlebars b/views/partials/footer.handlebars new file mode 100644 index 0000000..fa11963 --- /dev/null +++ b/views/partials/footer.handlebars @@ -0,0 +1,5 @@ + diff --git a/views/partials/header.handlebars b/views/partials/header.handlebars new file mode 100644 index 0000000..c82fd9f --- /dev/null +++ b/views/partials/header.handlebars @@ -0,0 +1,3 @@ +
+

amdl

+
diff --git a/views/partials/result.handlebars b/views/partials/result.handlebars new file mode 100644 index 0000000..903653c --- /dev/null +++ b/views/partials/result.handlebars @@ -0,0 +1,11 @@ +
+
+ +
+

{{name}}

+ {{#each artists as |artist|}} + {{artist}}{{#unless @last}},{{/unless}} + {{/each}} +
+
+
diff --git a/views/partials/search.handlebars b/views/partials/search.handlebars new file mode 100644 index 0000000..cd94fdd --- /dev/null +++ b/views/partials/search.handlebars @@ -0,0 +1,3 @@ +
+ +
diff --git a/views/search.handlebars b/views/search.handlebars new file mode 100644 index 0000000..6e23baf --- /dev/null +++ b/views/search.handlebars @@ -0,0 +1,4 @@ +{{> search query=query}} +{{#each results as |result|}} + {{> result name=result.name}} +{{/each}}