diff --git a/.editorconfig b/.editorconfig
index 18fd5bc..9951268 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,6 +1,7 @@
root = true
[*]
+charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
@@ -9,3 +10,12 @@ trim_trailing_whitespace = true
[{*.nix,*.yml}]
indent_size = 2
+
+# autogenerated files
+[{drizzle/**, package-lock.json}]
+charset = unset
+end_of_line = unset
+indent_style = unset
+indent_size = unset
+insert_final_newline = unset
+trim_trailing_whitespace = unset
diff --git a/.env.example b/.env.example
index 2b9f41f..7426019 100644
--- a/.env.example
+++ b/.env.example
@@ -2,5 +2,6 @@ MEDIA_USER_TOKEN=RE8gTk9UIFRSVVNUIFRIRU0uIFRIRVJFIElTIFNPTUVUSElORyBISURJTkcgT04
ITUA=US
WIDEVINE_CLIENT_ID=YTg1OGx2NmdpM3M1eWQ1YW0zaGtsN3FxOTM5Mzg3MjBrdjcxc3B4aXM1MnRscHViOGJkazl2ZGE2ZGN4dWFwYzJxMXo3ZzN6bWVsMjVuMnhhazc2cjdobHlxa2FkZjdibGYybXA4cWZkanZ6aGUydWI5bWF6ejcyajVkbmthbHA=
WIDEVINE_PRIVATE_KEY=aGFpaWlpaWlpaWlpaSBtZW93IDozMzMgd2Fzc3VwCg==
+MIGRATIONS_DIR=./drizzle
VIEWS_DIR=./views
PUBLIC_DIR=./public
diff --git a/.gitignore b/.gitignore
index 1dde58d..a2744d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+# build stuff
/dist
/result
/node_modules
@@ -5,5 +6,5 @@
.env
config.toml
-# the cache directory for songs
+# database stuff
/cache
diff --git a/README.md b/README.md
index 24678a6..e467abd 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ thank you to [gamdl](https://github.com/glomatico/gamdl) for inspiring this proj
`WIDEVINE_PRIVATE_KEY` is essentially the same process of obtainment, you'll get it from the same guide!! i'm not sure how to easily find one of these on the web, but i'm sure you end users (user count: 0 (
)) can pull through. this is also in base64 (`cat private_key.pem | base64 -w 0`)
-`PUBLIC_DIR` and `VIEWS_DIR` should typically not need to be set by the user if using this repository as the working directory. blank values will result in simply `views` and `public` being grabbed from the cwd, which also so happens to be the default in [`.env.example`](./.env.example). set this manually to your own value if you get full runtime errors when accessing pages relating to templates being missing, assets having unexpected 404 issues, etc. this value is also recommended for packagers, to prevent the users having to copy over views and public--see how the nix build works!
+`MIGRATIONS_DIR`, `PUBLIC_DIR`, and `VIEWS_DIR` should typically not need to be set by the user if using this repository as the working directory. blank values will result in simply `drizzle`, `views`, and `public` being grabbed from the cwd, which also so happens to be the default in [`.env.example`](./.env.example). set this manually to your own value if you get full runtime errors when accessing pages relating to templates being missing, assets having unexpected 404 issues, etc. this value is also recommended for packagers, to prevent the users having to copy over views and public--see how the nix build works!
### config
@@ -34,6 +34,14 @@ a system module is provided for your convenience, and the main output is `nixosM
after importing this module, the option `services.amdl` will show up, which is documented in [`flake.nix`](./flake.nix) somewhat well. everything under the `config` tree follows the `config.toml` well, along with everything under the `env` tree. defaults are provided for everything that isn't the ITUA inside of the env section. make sure to set those!!
+#### nginx information
+
+a decent amount of nginx setups (and ones on nixos using `recommendedProxySettings`) have proxy buffering on, i recommend turning that off (if the whole file isnt downloaded before the read timeout, then it will just drop the file)
+
+```nginx
+proxy_buffering off;
+```
+
## limitations / the formats
currently you can only get basic widevine ones, everything related to playready and fairplay encryption methods are not supported, sorry!! someday i will get this working, at least for playready. it's just that no one has written a library yet but has for python (yuck!!) lossless audio is unfortunately out of the question currently. it will be a while till someone breaks fairplay drm
diff --git a/config.example.toml b/config.example.toml
index 42bf08b..4ad13a2 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -8,7 +8,7 @@ port = 2000
# max 25, min 5
search_count = 5
# displayed codecs, recommended to use default
-# see src/downloader/index.ts for a list of codecs
+# see src/constants/codecs.ts for a list of codecs
displayed_codecs = ["aac_legacy", "aac_he_legacy"]
[downloader]
@@ -23,8 +23,10 @@ ytdlp_path = "yt-dlp"
# where to store downloaded files (music, lyrics, etc.)
# this directory will be created if it does not exist
directory = "cache"
+# where to store the database file
+database = "file:cache/cache.sqlite"
# how long to keep downloaded files (in seconds)
-ttl = 3600 # (1 hour)
+file_ttl = 3600 # (1 hour)
[downloader.api]
# two letter language code (ISO 639-1), followed by a dash (-) and a two letter country code (ISO 3166-1 alpha-2)
diff --git a/drizzle.config.ts b/drizzle.config.ts
new file mode 100644
index 0000000..86571cc
--- /dev/null
+++ b/drizzle.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from "drizzle-kit";
+import toml from "toml";
+import fs from "fs";
+
+export default defineConfig({
+ out: "./drizzle", // TODO: unhardcode
+ schema: "./src/database/schema.ts",
+ dialect: "sqlite",
+ dbCredentials: {
+ url: toml.parse(fs.readFileSync("config.toml", "utf-8")).downloader.cache.database // TODO: unscuff
+ }
+});
diff --git a/drizzle/0000_init.sql b/drizzle/0000_init.sql
new file mode 100644
index 0000000..896694c
--- /dev/null
+++ b/drizzle/0000_init.sql
@@ -0,0 +1,12 @@
+CREATE TABLE `file_cache` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `name` text NOT NULL,
+ `expiry` integer NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `key_cache` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `songId` text NOT NULL,
+ `codec` text NOT NULL,
+ `decryptionKey` text NOT NULL
+);
diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json
new file mode 100644
index 0000000..61d444e
--- /dev/null
+++ b/drizzle/meta/0000_snapshot.json
@@ -0,0 +1,87 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "b88a9929-0bda-4344-b012-87c3335389ed",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "tables": {
+ "file_cache": {
+ "name": "file_cache",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expiry": {
+ "name": "expiry",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "key_cache": {
+ "name": "key_cache",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "songId": {
+ "name": "songId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "codec": {
+ "name": "codec",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "decryptionKey": {
+ "name": "decryptionKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
new file mode 100644
index 0000000..2369033
--- /dev/null
+++ b/drizzle/meta/_journal.json
@@ -0,0 +1,13 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "6",
+ "when": 1756375000167,
+ "tag": "0000_init",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/flake.nix b/flake.nix
index 1da1fe6..68d430e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -25,7 +25,7 @@
# uncomment this and let the build fail, then get the current hash
# very scuffed but endorsed!
# npmDepsHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
- npmDepsHash = "sha256-lvueqcSBjtt9RSMwq2NWCAVT0NrZwDmhEYkjtdOs7js=";
+ npmDepsHash = "sha256-11AayHpPu7ocBPRB5k4SU7b99Aqc/dufAy2Yg5oPvGE=";
nativeBuildInputs = with pkgs; [ makeWrapper ];
@@ -35,12 +35,13 @@
runHook preInstall
mkdir -p $out
- mv node_modules dist views public $out/
+ mv dist drizzle public views node_modules $out/
makeWrapper ${pkgs.nodejs-slim}/bin/node $out/bin/amdl \
--prefix PATH : ${makeBinPath buildInputs} \
--add-flags "$out/dist/src/index.js" \
- --set VIEWS_DIR $out/views \
+ --set MIGRATIONS_DIR $out/drizzle \
--set PUBLIC_DIR $out/public \
+ --set VIEWS_DIR $out/views \
--set NODE_ENV production
runHook postInstall
diff --git a/package-lock.json b/package-lock.json
index fb61e08..5f6c4cf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,16 +9,20 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
+ "@libsql/client": "^0.15.12",
+ "archiver": "^7.0.1",
"axios": "^1.11.0",
"callsites": "^4.2.0",
"chalk": "^5.4.1",
"data-uri-to-buffer": "^6.0.2",
"dotenv": "^17.2.1",
+ "drizzle-orm": "^0.44.4",
"express": "^5.1.0",
"express-handlebars": "^8.0.3",
"format-duration": "^3.0.2",
"node-widevine": "^0.1.3",
"parse-hls": "^1.0.7",
+ "pretty-bytes": "^7.0.1",
"pssh-tools": "^1.2.0",
"source-map-support": "^0.5.21",
"swagger-ui-express": "^5.0.1",
@@ -30,16 +34,903 @@
},
"devDependencies": {
"@stylistic/eslint-plugin": "^3.1.0",
+ "@types/archiver": "^6.0.3",
"@types/express": "^5.0.3",
"@types/source-map-support": "^0.5.10",
"@types/swagger-ui-express": "^4.1.8",
"@typescript-eslint/parser": "^7.12.0",
"concurrently": "^9.2.0",
+ "drizzle-kit": "^0.31.4",
"eslint": "^8.57.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.1"
}
},
+ "node_modules/@drizzle-team/brocli": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
+ "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@esbuild-kit/core-utils": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
+ "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
+ "deprecated": "Merged into tsx: https://tsx.is",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.18.20",
+ "source-map-support": "^0.5.21"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+ "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+ "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+ "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+ "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+ "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+ "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+ "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+ "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+ "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+ "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+ "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+ "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+ "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+ "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+ "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+ "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+ "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+ "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+ "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+ "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.18.20",
+ "@esbuild/android-arm64": "0.18.20",
+ "@esbuild/android-x64": "0.18.20",
+ "@esbuild/darwin-arm64": "0.18.20",
+ "@esbuild/darwin-x64": "0.18.20",
+ "@esbuild/freebsd-arm64": "0.18.20",
+ "@esbuild/freebsd-x64": "0.18.20",
+ "@esbuild/linux-arm": "0.18.20",
+ "@esbuild/linux-arm64": "0.18.20",
+ "@esbuild/linux-ia32": "0.18.20",
+ "@esbuild/linux-loong64": "0.18.20",
+ "@esbuild/linux-mips64el": "0.18.20",
+ "@esbuild/linux-ppc64": "0.18.20",
+ "@esbuild/linux-riscv64": "0.18.20",
+ "@esbuild/linux-s390x": "0.18.20",
+ "@esbuild/linux-x64": "0.18.20",
+ "@esbuild/netbsd-x64": "0.18.20",
+ "@esbuild/openbsd-x64": "0.18.20",
+ "@esbuild/sunos-x64": "0.18.20",
+ "@esbuild/win32-arm64": "0.18.20",
+ "@esbuild/win32-ia32": "0.18.20",
+ "@esbuild/win32-x64": "0.18.20"
+ }
+ },
+ "node_modules/@esbuild-kit/esm-loader": {
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
+ "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
+ "deprecated": "Merged into tsx: https://tsx.is",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@esbuild-kit/core-utils": "^3.3.2",
+ "get-tsconfig": "^4.7.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
+ "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
+ "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
+ "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
+ "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
+ "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
+ "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
+ "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
+ "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
+ "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
+ "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
+ "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
+ "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
+ "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
+ "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
+ "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
+ "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
+ "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
+ "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
+ "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
+ "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
+ "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
@@ -285,6 +1176,182 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/@libsql/client": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.15.12.tgz",
+ "integrity": "sha512-JIqB0XsNrqYqBQZuhcgZdTcQoNOoQ5AMF+1yxc7vcZrLtm42QJwRazmTuBfyDwtWASEmVgjxeaLF4NT1iyVX8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@libsql/core": "^0.15.12",
+ "@libsql/hrana-client": "^0.7.0",
+ "js-base64": "^3.7.5",
+ "libsql": "^0.5.15",
+ "promise-limit": "^2.7.0"
+ }
+ },
+ "node_modules/@libsql/core": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.15.12.tgz",
+ "integrity": "sha512-S3tF6885ZizVjfym7f8SevL2VId/+DzxiKmP5zFbrhA8oMLh2XH8bYXChmhab7o9qUSHx+XjK4jCFpUwR5g+Ig==",
+ "license": "MIT",
+ "dependencies": {
+ "js-base64": "^3.7.5"
+ }
+ },
+ "node_modules/@libsql/darwin-arm64": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.17.tgz",
+ "integrity": "sha512-WTYG2skZsUnZmfZ2v7WFj7s3/5s2PfrYBZOWBKOnxHA8g4XCDc/4bFDaqob9Q2e88+GC7cWeJ8VNkVBFpD2Xxg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@libsql/darwin-x64": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.17.tgz",
+ "integrity": "sha512-ab0RlTR4KYrxgjNrZhAhY/10GibKoq6G0W4oi0kdm+eYiAv/Ip8GDMpSaZdAcoKA4T+iKR/ehczKHnMEB8MFxA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@libsql/hrana-client": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.7.0.tgz",
+ "integrity": "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==",
+ "license": "MIT",
+ "dependencies": {
+ "@libsql/isomorphic-fetch": "^0.3.1",
+ "@libsql/isomorphic-ws": "^0.1.5",
+ "js-base64": "^3.7.5",
+ "node-fetch": "^3.3.2"
+ }
+ },
+ "node_modules/@libsql/isomorphic-fetch": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.3.1.tgz",
+ "integrity": "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@libsql/isomorphic-ws": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz",
+ "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ws": "^8.5.4",
+ "ws": "^8.13.0"
+ }
+ },
+ "node_modules/@libsql/linux-arm-gnueabihf": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.17.tgz",
+ "integrity": "sha512-PcASh4k47RqC+kMWAbLUKf1y6Do0q8vnUGi0yhKY4ghJcimMExViBimjbjYRSa+WIb/zh3QxNoXOhQAXx3tiuw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@libsql/linux-arm-musleabihf": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.17.tgz",
+ "integrity": "sha512-vxOkSLG9Wspit+SNle84nuIzMtr2G2qaxFzW7BhsZBjlZ8+kErf9RXcT2YJQdJYxmBYRbsOrc91gg0jLEQVCqg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@libsql/linux-arm64-gnu": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.17.tgz",
+ "integrity": "sha512-L8jnaN01TxjBJlDuDTX2W2BKzBkAOhcnKfCOf3xzvvygblxnDOK0whkYwIXeTfwtd/rr4jN/d6dZD/bcHiDxEQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@libsql/linux-arm64-musl": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.17.tgz",
+ "integrity": "sha512-HfFD7TzQtmmTwyQsuiHhWZdMRtdNpKJ1p4tbMMTMRECk+971NFHrj69D64cc2ClVTAmn7fA9XibKPil7WN/Q7w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@libsql/linux-x64-gnu": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.17.tgz",
+ "integrity": "sha512-5l3XxWqUPVFrtX0xnZaXwqsXs0BFbP4w6ahRFTPSdXU50YBfUOajFznJRB6bJTMsCvraDSD0IkHhjSNfrE1CuQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@libsql/linux-x64-musl": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.17.tgz",
+ "integrity": "sha512-FvSpWlwc+dIeYIFYlsSv+UdQ/NiZWr+SstwVji+QZ//8NnvzwWQU9cgP+Vpps6Qiq4jyYQm9chJhTYOVT9Y3BA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@libsql/win32-x64-msvc": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.17.tgz",
+ "integrity": "sha512-f5bGH8+3A5sn6Lrqg8FsQ09a1pYXPnKGXGTFiAYlfQXVst1tUTxDTugnuWcJYKXyzDe/T7ccxyIZXeSmPOhq8A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@neon-rs/load": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz",
+ "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==",
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -323,6 +1390,16 @@
"node": ">= 8"
}
},
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -458,6 +1535,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/@types/archiver": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.3.tgz",
+ "integrity": "sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/readdir-glob": "*"
+ }
+ },
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -541,6 +1628,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/readdir-glob": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
+ "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
@@ -585,6 +1682,15 @@
"@types/serve-static": "*"
}
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/parser": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
@@ -994,6 +2100,18 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -1092,6 +2210,99 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/archiver": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
+ "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "archiver-utils": "^5.0.2",
+ "async": "^3.2.4",
+ "buffer-crc32": "^1.0.0",
+ "readable-stream": "^4.0.0",
+ "readdir-glob": "^1.1.2",
+ "tar-stream": "^3.0.0",
+ "zip-stream": "^6.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/archiver-utils": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz",
+ "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^10.0.0",
+ "graceful-fs": "^4.2.0",
+ "is-stream": "^2.0.1",
+ "lazystream": "^1.0.0",
+ "lodash": "^4.17.15",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/archiver-utils/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1109,6 +2320,12 @@
"node": ">=8"
}
},
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "license": "MIT"
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1126,12 +2343,45 @@
"proxy-from-env": "^1.1.0"
}
},
+ "node_modules/b4a": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
+ "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
+ "license": "Apache-2.0"
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/bare-events": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.1.tgz",
+ "integrity": "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@@ -1174,6 +2424,39 @@
"node": ">=8"
}
},
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
+ "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -1287,6 +2570,22 @@
"node": ">= 0.8"
}
},
+ "node_modules/compress-commons": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
+ "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
+ "license": "MIT",
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "crc32-stream": "^6.0.0",
+ "is-stream": "^2.0.1",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1405,6 +2704,37 @@
"node": ">=6.6.0"
}
},
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/crc32-stream": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz",
+ "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
+ "license": "MIT",
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1470,6 +2800,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
+ "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -1508,6 +2847,147 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/drizzle-kit": {
+ "version": "0.31.4",
+ "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.4.tgz",
+ "integrity": "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@drizzle-team/brocli": "^0.10.2",
+ "@esbuild-kit/esm-loader": "^2.5.5",
+ "esbuild": "^0.25.4",
+ "esbuild-register": "^3.5.0"
+ },
+ "bin": {
+ "drizzle-kit": "bin.cjs"
+ }
+ },
+ "node_modules/drizzle-orm": {
+ "version": "0.44.4",
+ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.4.tgz",
+ "integrity": "sha512-ZyzKFpTC/Ut3fIqc2c0dPZ6nhchQXriTsqTNs4ayRgl6sZcFlMs9QZKPSHXK4bdOf41GHGWf+FrpcDDYwW+W6Q==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@aws-sdk/client-rds-data": ">=3",
+ "@cloudflare/workers-types": ">=4",
+ "@electric-sql/pglite": ">=0.2.0",
+ "@libsql/client": ">=0.10.0",
+ "@libsql/client-wasm": ">=0.10.0",
+ "@neondatabase/serverless": ">=0.10.0",
+ "@op-engineering/op-sqlite": ">=2",
+ "@opentelemetry/api": "^1.4.1",
+ "@planetscale/database": ">=1.13",
+ "@prisma/client": "*",
+ "@tidbcloud/serverless": "*",
+ "@types/better-sqlite3": "*",
+ "@types/pg": "*",
+ "@types/sql.js": "*",
+ "@upstash/redis": ">=1.34.7",
+ "@vercel/postgres": ">=0.8.0",
+ "@xata.io/client": "*",
+ "better-sqlite3": ">=7",
+ "bun-types": "*",
+ "expo-sqlite": ">=14.0.0",
+ "gel": ">=2",
+ "knex": "*",
+ "kysely": "*",
+ "mysql2": ">=2",
+ "pg": ">=8",
+ "postgres": ">=3",
+ "sql.js": ">=1",
+ "sqlite3": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/client-rds-data": {
+ "optional": true
+ },
+ "@cloudflare/workers-types": {
+ "optional": true
+ },
+ "@electric-sql/pglite": {
+ "optional": true
+ },
+ "@libsql/client": {
+ "optional": true
+ },
+ "@libsql/client-wasm": {
+ "optional": true
+ },
+ "@neondatabase/serverless": {
+ "optional": true
+ },
+ "@op-engineering/op-sqlite": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@planetscale/database": {
+ "optional": true
+ },
+ "@prisma/client": {
+ "optional": true
+ },
+ "@tidbcloud/serverless": {
+ "optional": true
+ },
+ "@types/better-sqlite3": {
+ "optional": true
+ },
+ "@types/pg": {
+ "optional": true
+ },
+ "@types/sql.js": {
+ "optional": true
+ },
+ "@upstash/redis": {
+ "optional": true
+ },
+ "@vercel/postgres": {
+ "optional": true
+ },
+ "@xata.io/client": {
+ "optional": true
+ },
+ "better-sqlite3": {
+ "optional": true
+ },
+ "bun-types": {
+ "optional": true
+ },
+ "expo-sqlite": {
+ "optional": true
+ },
+ "gel": {
+ "optional": true
+ },
+ "knex": {
+ "optional": true
+ },
+ "kysely": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "postgres": {
+ "optional": true
+ },
+ "prisma": {
+ "optional": true
+ },
+ "sql.js": {
+ "optional": true
+ },
+ "sqlite3": {
+ "optional": true
+ }
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1594,6 +3074,61 @@
"node": ">= 0.4"
}
},
+ "node_modules/esbuild": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
+ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.9",
+ "@esbuild/android-arm": "0.25.9",
+ "@esbuild/android-arm64": "0.25.9",
+ "@esbuild/android-x64": "0.25.9",
+ "@esbuild/darwin-arm64": "0.25.9",
+ "@esbuild/darwin-x64": "0.25.9",
+ "@esbuild/freebsd-arm64": "0.25.9",
+ "@esbuild/freebsd-x64": "0.25.9",
+ "@esbuild/linux-arm": "0.25.9",
+ "@esbuild/linux-arm64": "0.25.9",
+ "@esbuild/linux-ia32": "0.25.9",
+ "@esbuild/linux-loong64": "0.25.9",
+ "@esbuild/linux-mips64el": "0.25.9",
+ "@esbuild/linux-ppc64": "0.25.9",
+ "@esbuild/linux-riscv64": "0.25.9",
+ "@esbuild/linux-s390x": "0.25.9",
+ "@esbuild/linux-x64": "0.25.9",
+ "@esbuild/netbsd-arm64": "0.25.9",
+ "@esbuild/netbsd-x64": "0.25.9",
+ "@esbuild/openbsd-arm64": "0.25.9",
+ "@esbuild/openbsd-x64": "0.25.9",
+ "@esbuild/openharmony-arm64": "0.25.9",
+ "@esbuild/sunos-x64": "0.25.9",
+ "@esbuild/win32-arm64": "0.25.9",
+ "@esbuild/win32-ia32": "0.25.9",
+ "@esbuild/win32-x64": "0.25.9"
+ }
+ },
+ "node_modules/esbuild-register": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
+ "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "peerDependencies": {
+ "esbuild": ">=0.12 <1"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1824,6 +3359,24 @@
"node": ">= 0.6"
}
},
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
"node_modules/express": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
@@ -1946,6 +3499,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "license": "MIT"
+ },
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -2000,6 +3559,29 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -2140,6 +3722,18 @@
"integrity": "sha512-pKzJDSRgK2lqAiPW3uizDaIJaJnataZclsahz25UMwfdryBGDa+1HlbXGjzpMvX/2kMh4O0sNevFXKaEfCjHsA==",
"license": "ISC"
},
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2221,6 +3815,19 @@
"node": ">= 0.4"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
+ "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -2449,6 +4056,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2571,6 +4198,24 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2592,6 +4237,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/js-base64": {
+ "version": "3.7.8",
+ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
+ "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -2636,6 +4287,48 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.6.3"
+ }
+ },
+ "node_modules/lazystream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/lazystream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/lazystream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -2650,6 +4343,38 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/libsql": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.17.tgz",
+ "integrity": "sha512-RRlj5XQI9+Wq+/5UY8EnugSWfRmHEw4hn3DKlPrkUgZONsge1PwTtHcpStP6MSNi8ohcbsRgEHJaymA33a8cBw==",
+ "cpu": [
+ "x64",
+ "arm64",
+ "wasm32",
+ "arm"
+ ],
+ "license": "MIT",
+ "os": [
+ "darwin",
+ "linux",
+ "win32"
+ ],
+ "dependencies": {
+ "@neon-rs/load": "^0.0.4",
+ "detect-libc": "2.0.2"
+ },
+ "optionalDependencies": {
+ "@libsql/darwin-arm64": "0.5.17",
+ "@libsql/darwin-x64": "0.5.17",
+ "@libsql/linux-arm-gnueabihf": "0.5.17",
+ "@libsql/linux-arm-musleabihf": "0.5.17",
+ "@libsql/linux-arm64-gnu": "0.5.17",
+ "@libsql/linux-arm64-musl": "0.5.17",
+ "@libsql/linux-x64-gnu": "0.5.17",
+ "@libsql/linux-x64-musl": "0.5.17",
+ "@libsql/win32-x64-msvc": "0.5.17"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2670,7 +4395,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
@@ -2774,7 +4498,6 @@
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -2832,6 +4555,53 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT"
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
+ "node_modules/node-fetch/node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/node-widevine": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/node-widevine/-/node-widevine-0.1.3.tgz",
@@ -2848,6 +4618,15 @@
"vscode": "^1.22.0"
}
},
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -3062,6 +4841,39 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/pretty-bytes": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.0.1.tgz",
+ "integrity": "sha512-285/jRCYIbMGDciDdrw0KPNC4LKEEwz/bwErcYNxSJOi4CpGUuLpb9gQpg3XJP0XYj9ldSRluXxih4lX2YN8Xw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/promise-limit": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
+ "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
+ "license": "ISC"
+ },
"node_modules/protobufjs": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.0.tgz",
@@ -3184,6 +4996,43 @@
"node": ">= 0.8"
}
},
+ "node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/readdir-glob": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.1.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -3204,6 +5053,16 @@
"node": ">=4"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -3541,6 +5400,28 @@
"node": ">= 0.8"
}
},
+ "node_modules/streamx": {
+ "version": "2.22.1",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz",
+ "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-fifo": "^1.3.2",
+ "text-decoder": "^1.1.0"
+ },
+ "optionalDependencies": {
+ "bare-events": "^2.2.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -3645,6 +5526,26 @@
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
+ "node_modules/tar-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
+ "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -4023,6 +5924,12 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -4032,6 +5939,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -4105,6 +6021,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -4157,6 +6094,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zip-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
+ "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==",
+ "license": "MIT",
+ "dependencies": {
+ "archiver-utils": "^5.0.0",
+ "compress-commons": "^6.0.2",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/zod": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.0.14.tgz",
diff --git a/package.json b/package.json
index 0ff254f..0f5bd54 100644
--- a/package.json
+++ b/package.json
@@ -9,19 +9,26 @@
"dev": "concurrently 'node --watch dist/src/index.js' 'tsc --watch'",
"build": "npm run lint && tsc",
"lint": "eslint .",
- "lint:fix": "eslint . --fix"
+ "lint:fix": "eslint . --fix",
+ "migrate": "drizzle-kit migrate",
+ "migrate:gen": "drizzle-kit generate",
+ "migrate:drop": "drizzle-kit drop"
},
"dependencies": {
+ "@libsql/client": "^0.15.12",
+ "archiver": "^7.0.1",
"axios": "^1.11.0",
"callsites": "^4.2.0",
"chalk": "^5.4.1",
"data-uri-to-buffer": "^6.0.2",
"dotenv": "^17.2.1",
+ "drizzle-orm": "^0.44.4",
"express": "^5.1.0",
"express-handlebars": "^8.0.3",
"format-duration": "^3.0.2",
"node-widevine": "^0.1.3",
"parse-hls": "^1.0.7",
+ "pretty-bytes": "^7.0.1",
"pssh-tools": "^1.2.0",
"source-map-support": "^0.5.21",
"swagger-ui-express": "^5.0.1",
@@ -33,11 +40,13 @@
},
"devDependencies": {
"@stylistic/eslint-plugin": "^3.1.0",
+ "@types/archiver": "^6.0.3",
"@types/express": "^5.0.3",
"@types/source-map-support": "^0.5.10",
"@types/swagger-ui-express": "^4.1.8",
"@typescript-eslint/parser": "^7.12.0",
"concurrently": "^9.2.0",
+ "drizzle-kit": "^0.31.4",
"eslint": "^8.57.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.1"
diff --git a/public/styles/main.css b/public/styles/main.css
index 0b10c4f..9895d63 100644
--- a/public/styles/main.css
+++ b/public/styles/main.css
@@ -123,10 +123,13 @@ footer {
width: 100%;
}
.result-info {
- display: flex;
- flex-direction: row;
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ grid-template-rows: auto;
+ grid-auto-flow: row;
align-items: center;
- gap: 1em;
+ gap: 0 1em;
+ padding-right: 1em;
}
.result-info img {
width: 4em;
diff --git a/src/appleMusicApi/index.ts b/src/appleMusicApi/index.ts
index 3935cce..1e80e9a 100644
--- a/src/appleMusicApi/index.ts
+++ b/src/appleMusicApi/index.ts
@@ -1,6 +1,6 @@
import axios, { type AxiosInstance } from "axios";
import { ampApiUrl, appleMusicHomepageUrl, licenseApiUrl, webplaybackApiUrl } from "../constants/urls.js";
-import type { GetPlaylistResponse, GetSongResponse, SearchResponse } from "./types/responses.js";
+import type { GetAlbumResponse, GetPlaylistResponse, GetSongResponse, SearchResponse } from "./types/responses.js";
import type { AlbumAttributesExtensionTypes, AnyAttributesExtensionTypes, SongAttributesExtensionTypes } from "./types/extensions.js";
import { getToken } from "./token.js";
import { config, env } from "../config.js";
@@ -41,8 +41,8 @@ export default class AppleMusicApi {
id: string,
extend: T = [] as unknown[] as T,
relationships: U = ["tracks"] as U
- ): Promise> {
- return (await this.http.get>(`/v1/catalog/${this.storefront}/albums/${id}`, {
+ ): Promise> {
+ return (await this.http.get>(`/v1/catalog/${this.storefront}/albums/${id}`, {
params: {
extend: extend.join(","),
include: relationships.join(",")
diff --git a/src/appleMusicApi/token.ts b/src/appleMusicApi/token.ts
index 522a086..4733020 100644
--- a/src/appleMusicApi/token.ts
+++ b/src/appleMusicApi/token.ts
@@ -27,7 +27,7 @@ export async function getToken(baseUrl: string): Promise {
throw new Error("could not find match for the api token in the index javascript file");
}
- log.debug("got api token");
+ log.info("got api token");
return token;
}
diff --git a/src/cache.ts b/src/cache.ts
index 3272db9..2a370d3 100644
--- a/src/cache.ts
+++ b/src/cache.ts
@@ -1,97 +1,131 @@
-import fs from "node:fs";
import path from "node:path";
-import timeago from "timeago.js";
import { config } from "./config.js";
+import { db } from "./database/index.js";
+import { fileCacheTable, keyCacheTable } from "./database/schema.js";
+import fsPromises from "fs/promises";
+import { and, eq } from "drizzle-orm";
import * as log from "./log.js";
+import prettyBytes from "pretty-bytes";
-// DO NOT READ FURTHER INTO THIS FILE
-// COGNITIVE DISSONANCE WARNING
-
-// TODO: hourly cache reports
-// TODO: swap to sqlite
-// TODO: make async fs calls
-// TODO: rework EVERYTHING
-// TODO: refresh cache timer on download
-
-interface CacheEntry {
- fileName: string;
- expiry: number; // milliseconds, not seconds
+// try creating cache if it doesn't exist
+// a bit scuffed but that ok
+try {
+ log.debug(`ensuring cache directory "${config.downloader.cache.directory}" exists`);
+ await fsPromises.mkdir(config.downloader.cache.directory, { recursive: true });
+} catch (err) {
+ log.error("failed to create cache directory!");
+ log.error(err);
+ process.exit(1);
}
-const cacheTtl = config.downloader.cache.ttl * 1000;
-const cacheFile = path.join(config.downloader.cache.directory, "cache.json");
+const fileTtl = config.downloader.cache.file_ttl * 1000;
+const timers = new Map();
-if (!fs.existsSync(config.downloader.cache.directory)) {
- log.debug("cache directory not found, creating it");
- fs.mkdirSync(config.downloader.cache.directory, { recursive: true });
-}
-if (!fs.existsSync(cacheFile)) {
- log.debug("cache file not found, creating it");
- fs.writeFileSync(cacheFile, JSON.stringify([]), { encoding: "utf-8" });
+try {
+ let entriesCleared = 0;
+ let entriesClearedBytes = 0;
+ log.debug("cache cleanup and expiry timers starting");
+
+ await Promise.all((await db.select().from(fileCacheTable)).map(async ({ name, expiry }) => {
+ if (expiry < Date.now()) {
+ entriesCleared++;
+ entriesClearedBytes += (await fsPromises.stat(path.join(config.downloader.cache.directory, name))).size;
+ await dropFile(name);
+ } else {
+ await scheduleDeletion(name, expiry);
+ }
+ }));
+
+ log.debug("cache cleanup complete!");
+ log.debug(`cleared ${entriesCleared} entr${entriesCleared === 1 ? "y" : "ies"}, freeing up ${prettyBytes(entriesClearedBytes)}!`);
+} catch (err) {
+ log.error("failed to run cache cleanup!");
+ log.error(err);
}
-let cache = JSON.parse(fs.readFileSync(cacheFile, { encoding: "utf-8" })) as CacheEntry[];
-
-// TODO: change how this works
-// this is so uncomfy
-cache.push = function(...items: CacheEntry[]): number {
- for (const entry of items) {
- log.debug(`cache entry ${entry.fileName} added, expires ${timeago.format(entry.expiry)}`);
- setTimeout(() => {
- log.debug(`cache entry ${entry.fileName} expired, cleaning`);
- removeCacheEntry(entry.fileName);
- rewriteCache();
- }, entry.expiry - Date.now());
+async function scheduleDeletion(name: string, expiry: number): Promise {
+ if (timers.has(name)) {
+ clearTimeout(timers.get(name) as NodeJS.Timeout);
}
- return Array.prototype.push.apply(this, items);
-};
+ const timeout = setTimeout(async () => {
+ await dropFile(name);
+ timers.delete(name);
+ }, expiry - Date.now());
-function rewriteCache(): void {
- // cache is in fact []. i checked
- fs.writeFileSync(cacheFile, JSON.stringify(cache), { encoding: "utf-8" });
+ timers.set(name, timeout);
}
-function removeCacheEntry(fileName: string): void {
- cache = cache.filter((entry) => { return entry.fileName !== fileName; });
- try {
- fs.unlinkSync(path.join(config.downloader.cache.directory, fileName));
- } catch (err) {
- if ((err as NodeJS.ErrnoException).code === "ENOENT") {
- log.debug(`file for cache entry ${fileName} missing, dropping`);
- } else {
- log.error(`could not remove cache entry ${fileName}!`);
+async function dropFile(name: string): Promise {
+ const size = (await fsPromises.stat(path.join(config.downloader.cache.directory, name))).size;
+ await fsPromises.unlink(path.join(config.downloader.cache.directory, name)).catch((err) => {
+ if (err.code !== "ENOENT") {
+ log.error(`failed to delete cached file ${name} for whatever reason!`);
+ log.error("manual removal may be necessary!");
log.error(err);
}
- }
-}
-
-// clear cache entries that are expired
-// this is for when the program is killed when cache entries are present
-// those could expire while the program is not running, therefore not being cleaned
-let expiryLogPrinted = false;
-for (const entry of cache) {
- if (entry.expiry < Date.now()) {
- if (!expiryLogPrinted) { log.info("old expired cache entries are present, cleaning them"); }
- expiryLogPrinted = true;
- log.debug(`cache entry ${entry.fileName} expired ${timeago.format(entry.expiry)}; cleaning`);
- removeCacheEntry(entry.fileName);
- rewriteCache();
- }
-}
-
-export function isCached(fileName: string): boolean {
- const entry = cache.find((e) => { return e.fileName === fileName; });
- const cached = entry !== undefined && entry.expiry > Date.now();
- if (cached) { log.debug(`cache HIT for ${fileName}`); }
- else { log.debug(`cache MISS for ${fileName}`); }
- return cached;
-}
-
-export function addToCache(fileName: string): void {
- cache.push({
- fileName: fileName,
- expiry: Date.now() + cacheTtl
});
- rewriteCache();
+
+ log.debug(`deleted file ${name} from cache, freeing up ${prettyBytes(size)}`);
+
+ await db.delete(fileCacheTable).where(eq(fileCacheTable.name, name));
+}
+
+export async function addFileToCache(fileName: string): Promise {
+ const expiry = Date.now() + fileTtl;
+ const existing = await db.select().from(fileCacheTable).where(eq(fileCacheTable.name, fileName)).get();
+
+ if (existing) {
+ await db.update(fileCacheTable).set({ expiry: expiry }).where(eq(fileCacheTable.name, fileName));
+ await scheduleDeletion(fileName, expiry);
+ } else {
+ await db.insert(fileCacheTable).values({name: fileName, expiry: expiry });
+ await scheduleDeletion(fileName, expiry);
+ }
+}
+
+export async function isFileCached(fileName: string): Promise {
+ const existing = await db.select().from(fileCacheTable).where(eq(fileCacheTable.name, fileName)).get();
+
+ if (existing !== undefined) {
+ await db.update(fileCacheTable).set({ expiry: Date.now() + fileTtl }).where(eq(fileCacheTable.name, fileName));
+ await scheduleDeletion(fileName, existing.expiry);
+
+ log.debug(`cache HIT for file ${fileName}, extending expiry`);
+ return true;
+ } else {
+ log.debug(`cache MISS for file ${fileName}`);
+ return false;
+ }
+}
+
+// TODO: add a key ttl? its probably not necessary but would be a nice to have
+// its pretty small anyway
+export async function addKeyToCache(songId: string, codec: string, decryptionKey: string): Promise {
+ const existing = await db.select().from(keyCacheTable).where(and(
+ eq(keyCacheTable.songId, songId),
+ eq(keyCacheTable.codec, codec),
+ eq(keyCacheTable.decryptionKey, decryptionKey)
+ )).get();
+
+ if (existing) {
+ return;
+ } else {
+ await db.insert(keyCacheTable).values({ songId: songId, codec: codec, decryptionKey: decryptionKey });
+ }
+}
+
+export async function getKeyFromCache(songId: string, codec: string): Promise {
+ const existing = await db.select().from(keyCacheTable).where(and(
+ eq(keyCacheTable.songId, songId),
+ eq(keyCacheTable.codec, codec)
+ )).get();
+
+ if (existing !== undefined) {
+ log.debug(`cache HIT for key of ${songId} (${codec})`);
+ return existing.decryptionKey;
+ } else {
+ log.debug(`cache MISS for key of ${songId} (${codec})`);
+ return undefined;
+ }
}
diff --git a/src/config.ts b/src/config.ts
index a1d8310..8fc5655 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -20,7 +20,8 @@ const configSchema = z.object({
ytdlp_path: z.string(),
cache: z.object({
directory: z.string(),
- ttl: z.number().int().min(0)
+ database: z.string(),
+ file_ttl: z.number().int().min(0)
}),
api: z.object({
language: z.string()
@@ -33,6 +34,7 @@ const envSchema = z.object({
ITUA: z.string(),
WIDEVINE_CLIENT_ID: z.string(),
WIDEVINE_PRIVATE_KEY: z.string(),
+ MIGRATIONS_DIR: z.string().default("./drizzle"),
VIEWS_DIR: z.string().default("./views"),
PUBLIC_DIR: z.string().default("./public")
});
diff --git a/src/database/index.ts b/src/database/index.ts
new file mode 100644
index 0000000..b33804d
--- /dev/null
+++ b/src/database/index.ts
@@ -0,0 +1,26 @@
+import { createClient } from "@libsql/client";
+import { config, env } from "../config.js";
+import { drizzle } from "drizzle-orm/libsql";
+import { migrate } from "drizzle-orm/libsql/migrator";
+import fsPromises from "fs/promises";
+import * as log from "../log.js";
+
+try {
+ if (config.downloader.cache.database.startsWith("file:")) {
+ const databaseDir = config.downloader.cache.database.split("file:")[1].split("/").slice(0, -1).join("/");
+ log.debug(`ensuring database directory "${databaseDir}" exists`);
+ await fsPromises.mkdir(databaseDir, { recursive: true });
+ }
+} catch (err) {
+ log.error("failed to create database directory!");
+ log.error(err);
+ process.exit(1);
+}
+
+// TODO: nice looking errors
+export const client = createClient({ url: config.downloader.cache.database });
+client.execute("PRAGMA foreign_keys = ON;");
+client.execute("PRAGMA journal_mode = WAL;");
+export const db = drizzle(config.downloader.cache.database);
+
+await migrate(db, { migrationsFolder: env.MIGRATIONS_DIR });
diff --git a/src/database/schema.ts b/src/database/schema.ts
new file mode 100644
index 0000000..0efa531
--- /dev/null
+++ b/src/database/schema.ts
@@ -0,0 +1,14 @@
+import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
+
+export const fileCacheTable = sqliteTable("file_cache", {
+ id: int().primaryKey({ autoIncrement: true }),
+ name: text().notNull(),
+ expiry: int().notNull()
+});
+
+export const keyCacheTable = sqliteTable("key_cache", {
+ id: int().primaryKey({ autoIncrement: true }),
+ songId: text().notNull(),
+ codec: text().notNull(),
+ decryptionKey: text().notNull()
+});
diff --git a/src/downloader/fileMetadata.ts b/src/downloader/fileMetadata.ts
index a713615..359a583 100644
--- a/src/downloader/fileMetadata.ts
+++ b/src/downloader/fileMetadata.ts
@@ -1,19 +1,19 @@
-import { createWriteStream } from "node:fs";
-import type { GetSongResponse } from "appleMusicApi/types/responses.js";
-import path from "node:path";
-import { config } from "../config.js";
-import { pipeline } from "node:stream/promises";
-import { addToCache, isCached } from "../cache.js";
+import type { GetSongResponse } from "../appleMusicApi/types/responses.js";
+import { stripAlbumGarbage } from "./format.js";
+import { downloadAlbumCover } from "./index.js";
+import type { AlbumAttributes, SongAttributes } from "../appleMusicApi/types/attributes.js";
// TODO: simply add more fields. ha!
// TODO: add lyrics (what format??)
+// TODO: where it does file name formatting to hit caches, i think we should normalize this throughout files in a function
export class FileMetadata {
+ private readonly trackAttributes: SongAttributes<[]>;
+ private readonly albumAttributes: AlbumAttributes<[]>;
public readonly artist: string;
public readonly title: string;
public readonly album: string;
public readonly albumArtist: string;
public readonly isPartOfCompilation: boolean;
- public readonly artwork: string;
public readonly track?: number;
public readonly disc?: number;
public readonly date?: string;
@@ -21,13 +21,14 @@ export class FileMetadata {
public readonly isrc?: string;
public readonly composer?: string;
- constructor(
+ private constructor(
+ trackAttributes: SongAttributes<[]>,
+ albumAttributes: AlbumAttributes<[]>,
artist: string,
title: string,
album: string,
albumArtist: string,
isPartOfCompilation: boolean,
- artwork: string,
track?: number,
disc?: number,
date?: string,
@@ -35,12 +36,13 @@ export class FileMetadata {
isrc?: string,
composer?: string
) {
+ this.trackAttributes = trackAttributes;
+ this.albumAttributes = albumAttributes;
this.artist = artist;
this.title = title;
- this.album = album.replace(/- (EP|Single)$/, "").trim();
+ this.album = stripAlbumGarbage(album);
this.albumArtist = albumArtist;
this.isPartOfCompilation = isPartOfCompilation;
- this.artwork = artwork;
this.track = track;
this.disc = disc;
this.date = date;
@@ -53,17 +55,14 @@ export class FileMetadata {
const trackAttributes = trackMetadata.data[0].attributes;
const albumAttributes = trackMetadata.data[0].relationships.albums.data[0].attributes;
- const artworkUrl = trackAttributes.artwork.url
- .replace("{w}", trackAttributes.artwork.width.toString())
- .replace("{h}", trackAttributes.artwork.height.toString());
-
return new FileMetadata(
+ trackAttributes,
+ albumAttributes,
trackAttributes.artistName,
trackAttributes.name,
albumAttributes.name,
albumAttributes.artistName,
albumAttributes.isCompilation,
- artworkUrl,
trackAttributes.trackNumber,
trackAttributes.discNumber,
trackAttributes.releaseDate,
@@ -73,32 +72,12 @@ export class FileMetadata {
);
}
- public async setupFfmpegInputs(encryptedPath: string): Promise {
- // url is in a weird format
- // only things we care about is the uuid and file extension i think?
- // i dont wanna use the original file name because what if. what if theres a collision
- const extension = this.artwork.slice(this.artwork.lastIndexOf(".") + 1);
- const uuid = this.artwork.split("/").at(-3);
-
- if (uuid === undefined) { throw new Error("could not get uuid from artwork url!"); }
-
- const imageFileName = `${uuid}.${extension}`;
- const imagePath = path.join(config.downloader.cache.directory, imageFileName);
-
- if (!isCached(imageFileName)) {
- const response = await fetch(this.artwork);
-
- if (!response.ok) { throw new Error(`failed to fetch artwork: ${response.status}`); }
- if (!response.body) { throw new Error("no response body for artwork!"); }
-
- await pipeline(response.body as ReadableStream, createWriteStream(imagePath));
-
- addToCache(imageFileName);
- }
+ public async setupFfmpegInputs(audioInput: string): Promise {
+ const albumCover = await downloadAlbumCover(this.albumAttributes);
return [
- "-i", encryptedPath,
- "-i", imagePath,
+ "-i", audioInput,
+ "-i", albumCover,
"-map", "0",
"-map", "1",
"-disposition:v", "attached_pic",
diff --git a/src/downloader/format.ts b/src/downloader/format.ts
index bd47042..c0ddc20 100644
--- a/src/downloader/format.ts
+++ b/src/downloader/format.ts
@@ -1,4 +1,8 @@
-import type { SongAttributes } from "../appleMusicApi/types/attributes.js";
+import type { AlbumAttributes, SongAttributes } from "../appleMusicApi/types/attributes.js";
+
+// TODO: make these configurable, too opinionated right now
+// eventually i'll make an account system? maybe you could do through there
+// or i'll just make it config on the server
const illegalCharReplacements: Record = {
"?": "?",
@@ -13,10 +17,11 @@ const illegalCharReplacements: Record = {
"|": "|"
};
-// TODO: make these configurable, too opinionated right now
-// eventually i'll make an account system? maybe you could do through there
-// or i'll just make it config on the server
-export function formatSong(trackAttributes: SongAttributes<[]>): string {
+export function stripAlbumGarbage(input: string): string {
+ return input.replace(/- (EP|Single)$/, "").trim();
+}
+
+export function formatSongForFs(trackAttributes: SongAttributes<[]>): string {
const title = trackAttributes.name.replace(/[?!*\/\\:"<>|]/g, (match) => illegalCharReplacements[match] || match);
const disc = trackAttributes.discNumber;
const track = trackAttributes.trackNumber;
@@ -26,3 +31,10 @@ export function formatSong(trackAttributes: SongAttributes<[]>): string {
return `${disc}-${track.toString().padStart(2, "0")} - ${title}`;
}
+
+export function formatAlbumForFs(albumAttributes: AlbumAttributes<[]>): string {
+ const artist = albumAttributes.artistName.replace(/[?!*\/\\:"<>|]/g, (match) => illegalCharReplacements[match] || match);
+ const album = stripAlbumGarbage(albumAttributes.name).replace(/[?!*\/\\:"<>|]/g, (match) => illegalCharReplacements[match] || match);
+
+ return `${artist} - ${album}`;
+}
diff --git a/src/downloader/index.ts b/src/downloader/index.ts
index 44f573f..7fee4c3 100644
--- a/src/downloader/index.ts
+++ b/src/downloader/index.ts
@@ -1,46 +1,35 @@
import { config } from "../config.js";
import { spawn } from "node:child_process";
import path from "node:path";
-import { addToCache, isCached } from "../cache.js";
+import { addFileToCache, isFileCached } from "../cache.js";
import type { RegularCodecType, WebplaybackCodecType } from "./codecType.js";
import type { GetSongResponse } from "../appleMusicApi/types/responses.js";
import { FileMetadata } from "./fileMetadata.js";
import { createDecipheriv } from "node:crypto";
-import * as log from "../log.js";
+import type { AlbumAttributes } from "../appleMusicApi/types/attributes.js";
+import { pipeline } from "node:stream/promises";
+import { createWriteStream } from "node:fs";
export async function downloadSongFile(streamUrl: string, decryptionKey: string, songCodec: RegularCodecType | WebplaybackCodecType, songResponse: GetSongResponse<[], ["albums"]>): Promise {
- log.debug("downloading song file and hopefully decrypting it");
- log.debug({ streamUrl: streamUrl, songCodec: songCodec });
-
let baseOutputName = streamUrl.match(/(?:.*\/)\s*(\S*?)[.?]/)?.[1];
if (!baseOutputName) { throw new Error("could not get base output name from stream url!"); }
baseOutputName += `_${songCodec}`;
- const encryptedName = baseOutputName + "_enc.mp4";
- const encryptedPath = path.join(config.downloader.cache.directory, encryptedName);
const decryptedName = baseOutputName + ".m4a";
const decryptedPath = path.join(config.downloader.cache.directory, decryptedName);
- if ( // TODO: remove check for encrypted file/cache for encrypted?
- isCached(encryptedName) &&
- isCached(decryptedName)
- ) { return decryptedPath; }
+ if (await isFileCached(decryptedName)) { return decryptedPath; }
- await new Promise((res, rej) => {
- const child = spawn(config.downloader.ytdlp_path, [
- "--quiet",
- "--no-warnings",
- "--allow-unplayable-formats",
- "--fixup", "never",
- "--paths", config.downloader.cache.directory,
- "--output", encryptedName,
- streamUrl
- ]);
- child.on("error", (err) => { rej(err); });
- child.stderr.on("data", (data) => { rej(new Error(data.toString().trim())); });
- child.on("exit", () => { res(); });
- });
-
- addToCache(encryptedName);
+ const ytdlp = spawn(config.downloader.ytdlp_path, [
+ "--quiet",
+ "--no-warnings",
+ "--allow-unplayable-formats",
+ "--fixup", "never",
+ "--paths", config.downloader.cache.directory,
+ "--output", "-",
+ streamUrl
+ ], { stdio: ["ignore", "pipe", "pipe"] });
+ ytdlp.on("error", (err) => { throw err; });
+ ytdlp.stderr.on("data", (data) => { throw new Error(data.toString().trim()); });
const fileMetadata = FileMetadata.fromSongResponse(songResponse);
@@ -49,17 +38,18 @@ export async function downloadSongFile(streamUrl: string, decryptionKey: string,
"-loglevel", "error",
"-y",
"-decryption_key", decryptionKey,
- ...await fileMetadata.setupFfmpegInputs(encryptedPath),
+ ...await fileMetadata.setupFfmpegInputs("pipe:0"),
...await fileMetadata.toFfmpegArgs(),
"-movflags", "+faststart",
decryptedPath
- ]);
+ ], { stdio: ["pipe", "pipe", "pipe"] });
+ ytdlp.stdout.pipe(child.stdin);
child.on("error", (err) => { rej(err); });
child.stderr.on("data", (data) => { rej(new Error(data.toString().trim())); });
child.on("exit", () => { res(); } );
});
- addToCache(decryptedName);
+ await addFileToCache(decryptedName);
return decryptedPath;
}
@@ -69,11 +59,7 @@ export async function downloadSongFile(streamUrl: string, decryptionKey: string,
// TODO: less mem alloc/access
// TODO: use actual atom scanning. what if the magic bytes appear in a sample
export async function fetchAndDecryptStreamSegment(segmentUrl: string, decryptionKey: string, fetchLength: number, offset: number): Promise {
- log.debug("downloading and hopefully decrypting stream segment");
- log.debug({ segmentUrl: segmentUrl, offset: offset, fetchLength: fetchLength });
-
const response = await fetch(segmentUrl, { headers: { "range": `bytes=${offset}-${offset + fetchLength - 1}` }});
-
const file = new Uint8Array(await response.arrayBuffer());
// this translates to "moof"
@@ -122,6 +108,31 @@ export async function fetchAndDecryptStreamSegment(segmentUrl: string, decryptio
return file;
}
+export async function downloadAlbumCover(albumAttributes: AlbumAttributes<[]>): Promise {
+ const url = albumAttributes.artwork.url
+ .replace("{w}", albumAttributes.artwork.width.toString())
+ .replace("{h}", albumAttributes.artwork.height.toString());
+ const name = albumAttributes.playParams?.id;
+ const extension = url.slice(url.lastIndexOf(".") + 1);
+
+ if (!name) { throw new Error("no artwork name found! this may indicate the album isnt acessable w/ your subscription!"); }
+
+ const imageFileName = `${name}.${extension}`;
+ const imagePath = path.join(config.downloader.cache.directory, imageFileName);
+
+ if (await isFileCached(imageFileName) === false) {
+ const response = await fetch(url);
+
+ if (!response.ok) { throw new Error(`failed to fetch artwork: ${response.status}`); }
+ if (!response.body) { throw new Error("no response body for artwork!"); }
+
+ await pipeline(response.body as ReadableStream, createWriteStream(imagePath));
+ await addFileToCache(imageFileName);
+ }
+
+ return imagePath;
+}
+
interface IvValue {
value: Buffer;
subsamples: Subsample[];
diff --git a/src/web/endpoints/back/convertPlaylist.ts b/src/web/endpoints/back/convertPlaylist.ts
index 562e236..485d4d1 100644
--- a/src/web/endpoints/back/convertPlaylist.ts
+++ b/src/web/endpoints/back/convertPlaylist.ts
@@ -4,11 +4,8 @@ import z from "zod";
import { CodecType, regularCodecTypeSchema, webplaybackCodecTypeSchema, type RegularCodecType, type WebplaybackCodecType } from "../../../downloader/codecType.js";
import { appleMusicApi } from "../../../appleMusicApi/index.js";
import StreamInfo from "../../../downloader/streamInfo.js";
-import hls from "parse-hls";
import { paths } from "../../openApi.js";
-type M3u8 = ReturnType;
-
const router = express.Router();
const path = "/convertPlaylist";
@@ -33,35 +30,17 @@ paths[path] = {
router.get(path, async (req, res, next) => {
try {
const { id, codec } = (await validate(req, schema)).query;
-
const codecType = new CodecType(codec);
- let m3u8Parsed: M3u8;
- let streamUrl: string;
+ const trackMetadata = await appleMusicApi.getSong(id);
+ const trackAttributes = trackMetadata.data[0].attributes;
+ const streamInfo = await (codecType.regularOrWebplayback === "regular"
+ ? StreamInfo.fromTrackMetadata(trackAttributes, codecType.codecType as RegularCodecType)
+ : StreamInfo.fromWebplayback(await appleMusicApi.getWebplayback(id), codecType.codecType as WebplaybackCodecType)
+ );
- if (codecType.regularOrWebplayback === "regular") {
- const regularCodec = codecType.codecType as RegularCodecType; // safe cast, zod
- const trackMetadata = await appleMusicApi.getSong(id);
- const trackAttributes = trackMetadata.data[0].attributes;
- const streamInfo = await StreamInfo.fromTrackMetadata(trackAttributes, regularCodec);
-
- m3u8Parsed = streamInfo.streamParsed;
- streamUrl = streamInfo.streamUrl;
- } else if (codecType.regularOrWebplayback === "webplayback") {
- const webplaybackCodec = codecType.codecType as WebplaybackCodecType; // safe cast, zod
- const webplaybackResponse = await appleMusicApi.getWebplayback(id);
- const streamInfo = await StreamInfo.fromWebplayback(webplaybackResponse, webplaybackCodec);
-
- m3u8Parsed = streamInfo.streamParsed;
- streamUrl = streamInfo.streamUrl;
- } else {
- // TODO: this is unreachable
- // typescript doesn't think so
- // i think its because of the "let"s why we need this
- // fucks up our planned de-dupe on every use of `regularOrWebplayback`
- // damn !
- throw new Error("invalid codec type!");
- }
+ const m3u8Parsed = streamInfo.streamParsed;
+ const streamUrl = streamInfo.streamUrl;
const ogMp4Name = m3u8Parsed.segments[0].uri;
const ogMp4Url = streamUrl.substring(0, streamUrl.lastIndexOf("/")) + "/" + ogMp4Name;
diff --git a/src/web/endpoints/back/download.ts b/src/web/endpoints/back/download.ts
index 1dbd3d9..b33ba5c 100644
--- a/src/web/endpoints/back/download.ts
+++ b/src/web/endpoints/back/download.ts
@@ -7,7 +7,8 @@ import { z } from "zod";
import { validate } from "../../validate.js";
import { CodecType, regularCodecTypeSchema, webplaybackCodecTypeSchema, type RegularCodecType, type WebplaybackCodecType } from "../../../downloader/codecType.js";
import { paths } from "../../openApi.js";
-import { formatSong } from "../../../downloader/format.js";
+import { formatSongForFs } from "../../../downloader/format.js";
+import { addKeyToCache, getKeyFromCache } from "../../../cache.js";
const router = express.Router();
@@ -39,44 +40,29 @@ router.get(path, async (req, res, next) => {
const codecType = new CodecType(codec);
- if (codecType.regularOrWebplayback === "regular") {
- const regularCodec = codecType.codecType as RegularCodecType; // safe cast, zod
- const trackMetadata = await appleMusicApi.getSong(id);
- const trackAttributes = trackMetadata.data[0].attributes;
- const streamInfo = await StreamInfo.fromTrackMetadata(trackAttributes, regularCodec);
+ const trackMetadata = await appleMusicApi.getSong(id);
+ const trackAttributes = trackMetadata.data[0].attributes;
+ const streamInfo = await (codecType.regularOrWebplayback === "regular"
+ ? StreamInfo.fromTrackMetadata(trackAttributes, codecType.codecType as RegularCodecType)
+ : StreamInfo.fromWebplayback(await appleMusicApi.getWebplayback(id), codecType.codecType as WebplaybackCodecType)
+ );
- if (streamInfo.widevinePssh !== undefined) {
- const decryptionKey = await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
-
- const filePath = await downloadSongFile(streamInfo.streamUrl, decryptionKey, regularCodec, trackMetadata);
- const fileExt = "." + filePath.split(".").at(-1) as string; // safe cast, filePath is always a valid path
- const fileName = formatSong(trackAttributes) + fileExt;
-
- res.attachment(fileName);
- res.sendFile(filePath, { root: "." });
- } else {
- throw new Error("no decryption key found for regular codec! this is typical. don't fret!");
- }
- } else if (codecType.regularOrWebplayback === "webplayback") {
- const webplaybackCodec = codecType.codecType as WebplaybackCodecType; // safe cast, zod
- const webplaybackResponse = await appleMusicApi.getWebplayback(id);
- const trackMetadata = await appleMusicApi.getSong(id);
- const trackAttributes = trackMetadata.data[0].attributes;
- const streamInfo = await StreamInfo.fromWebplayback(webplaybackResponse, webplaybackCodec);
-
- if (streamInfo.widevinePssh !== undefined) {
- const decryptionKey = await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
-
- const filePath = await downloadSongFile(streamInfo.streamUrl, decryptionKey, webplaybackCodec, trackMetadata);
- const fileExt = "." + filePath.split(".").at(-1) as string; // safe cast, filePath is always a valid path
- const fileName = formatSong(trackAttributes) + fileExt;
-
- res.attachment(fileName);
- res.sendFile(filePath, { root: "." });
- } else {
- throw new Error("no decryption key found for web playback! this should not happen..");
- }
+ if (streamInfo.widevinePssh === undefined) {
+ if (codecType.regularOrWebplayback === "regular") { throw new Error("failed to get widevine pssh, this is typical"); }
+ else { throw new Error("failed to get widevine pssh for web playback, this should not happen.."); }
}
+
+ const decryptionKey =
+ await getKeyFromCache(id, codecType.codecType) ||
+ await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
+ await addKeyToCache(id, codecType.codecType, decryptionKey);
+
+ const filePath = await downloadSongFile(streamInfo.streamUrl, decryptionKey, codecType.codecType, trackMetadata);
+ const fileExt = "." + filePath.split(".").at(-1) as string; // safe cast, filePath is always a valid path
+ const fileName = formatSongForFs(trackAttributes) + fileExt;
+
+ res.attachment(fileName);
+ res.sendFile(filePath, { root: "." });
} catch (err) {
next(err);
}
diff --git a/src/web/endpoints/back/downloadAlbum.ts b/src/web/endpoints/back/downloadAlbum.ts
new file mode 100644
index 0000000..b649bab
--- /dev/null
+++ b/src/web/endpoints/back/downloadAlbum.ts
@@ -0,0 +1,105 @@
+import express from "express";
+import { paths } from "../../openApi.js";
+import { CodecType, regularCodecTypeSchema, webplaybackCodecTypeSchema, type RegularCodecType, type WebplaybackCodecType } from "../../../downloader/codecType.js";
+import z from "zod";
+import { validate } from "../../validate.js";
+import StreamInfo from "../../../downloader/streamInfo.js";
+import { appleMusicApi } from "../../../appleMusicApi/index.js";
+import { getWidevineDecryptionKey } from "../../../downloader/keygen.js";
+import { downloadAlbumCover, downloadSongFile } from "../../../downloader/index.js";
+import { formatAlbumForFs, formatSongForFs } from "../../../downloader/format.js";
+import archiver from "archiver";
+import { addKeyToCache, getKeyFromCache } from "../../../cache.js";
+
+const router = express.Router();
+
+const path = "/downloadAlbum";
+const schema = z.object({
+ query: z.object({
+ id: z.string(),
+ codec: z.enum([...regularCodecTypeSchema.options, ...webplaybackCodecTypeSchema.options])
+ })
+});
+
+paths[path] = {
+ get: {
+ requestParams: { query: schema.shape.query },
+ responses: {
+ 200: { description: "returns an album in a zip" },
+ 400: { description: "bad request, invalid query parameters. sent as a zod error with details" },
+ default: { description: "upstream api error, or some other error" }
+ }
+ }
+};
+
+interface AlbumEntry {
+ path: string;
+ name: string;
+}
+
+// TODO: include album art?
+router.get(path, async (req, res, next) => {
+ try {
+ const { id, codec } = (await validate(req, schema)).query;
+
+ const files: AlbumEntry[] = [];
+
+ const albumMetadata = await appleMusicApi.getAlbum(id);
+ const albumAttributes = albumMetadata.data[0].attributes;
+ const tracks = albumMetadata.data[0].relationships.tracks.data;
+
+ for (const track of tracks) {
+ const trackId = track.attributes.playParams?.id;
+ if (trackId === undefined) { throw new Error("track id gone, this may indicate your song isn't accessable w/ your subscription!"); }
+
+ const codecType = new CodecType(codec);
+
+ const trackMetadata = await appleMusicApi.getSong(trackId);
+ const trackAttributes = trackMetadata.data[0].attributes;
+ const streamInfo = await (codecType.regularOrWebplayback === "regular"
+ ? StreamInfo.fromTrackMetadata(trackAttributes, codecType.codecType as RegularCodecType)
+ : StreamInfo.fromWebplayback(await appleMusicApi.getWebplayback(trackId), codecType.codecType as WebplaybackCodecType)
+ );
+
+ if (streamInfo.widevinePssh === undefined) {
+ if (codecType.regularOrWebplayback === "regular") { throw new Error("failed to get widevine pssh, this is typical"); }
+ else { throw new Error("failed to get widevine pssh for web playback, this should not happen.."); }
+ }
+
+ const decryptionKey =
+ await getKeyFromCache(trackId, codecType.codecType) ||
+ await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
+ await addKeyToCache(trackId, codecType.codecType, decryptionKey);
+
+ const filePath = await downloadSongFile(streamInfo.streamUrl, decryptionKey, codecType.codecType, trackMetadata);
+ const fileExt = "." + filePath.split(".").at(-1) as string; // safe cast, filePath is always a valid path
+ const fileName = formatSongForFs(trackAttributes) + fileExt;
+
+ files.push({
+ path: filePath,
+ name: fileName
+ });
+ }
+
+ const fileName = formatAlbumForFs(albumAttributes) + ".zip";
+ const zipArchiver = archiver("zip");
+
+ zipArchiver.on("error", (err) => { throw err; });
+ zipArchiver.pipe(res);
+
+ for (const file of files) {
+ zipArchiver.file(file.path, { name: file.name });
+ }
+
+ const albumCover = await downloadAlbumCover(albumAttributes);
+ const albumCoverExt = albumCover.slice(albumCover.lastIndexOf(".") + 1);
+ zipArchiver.file(await downloadAlbumCover(albumAttributes), { name: `cover.${albumCoverExt}` });
+ zipArchiver.finalize();
+
+ res.attachment(fileName);
+ } catch (err) {
+ next(err);
+ }
+});
+
+export default router;
diff --git a/src/web/endpoints/back/downloadSegment.ts b/src/web/endpoints/back/downloadSegment.ts
index 76acd4d..2a97e1f 100644
--- a/src/web/endpoints/back/downloadSegment.ts
+++ b/src/web/endpoints/back/downloadSegment.ts
@@ -7,6 +7,7 @@ import StreamInfo from "../../../downloader/streamInfo.js";
import { appleMusicApi } from "../../../appleMusicApi/index.js";
import { getWidevineDecryptionKey } from "../../../downloader/keygen.js";
import { fetchAndDecryptStreamSegment } from "../../../downloader/index.js";
+import { addKeyToCache, getKeyFromCache } from "../../../cache.js";
const router = express.Router();
@@ -54,40 +55,28 @@ router.get(path, async (req, res, next) => {
const codecType = new CodecType(codec);
- if (codecType.regularOrWebplayback === "regular") {
- const regularCodec = codecType.codecType as RegularCodecType; // safe cast, zod
- const trackMetadata = await appleMusicApi.getSong(id);
- const trackAttributes = trackMetadata.data[0].attributes;
- const streamInfo = await StreamInfo.fromTrackMetadata(trackAttributes, regularCodec);
+ const trackMetadata = await appleMusicApi.getSong(id);
+ const trackAttributes = trackMetadata.data[0].attributes;
+ const streamInfo = await (codecType.regularOrWebplayback === "regular"
+ ? StreamInfo.fromTrackMetadata(trackAttributes, codecType.codecType as RegularCodecType)
+ : StreamInfo.fromWebplayback(await appleMusicApi.getWebplayback(id), codecType.codecType as WebplaybackCodecType)
+ );
- if (streamInfo.widevinePssh !== undefined) {
- const decryptionKey = await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
- const file = await fetchAndDecryptStreamSegment(originalMp4, decryptionKey, end - start + 1, start);
-
- res.setHeader("Content-Type", "application/mp4");
- res.setHeader("Content-Range", `bytes ${start}-${end}/*`);
- res.setHeader("Accept-Ranges", "bytes");
- res.status(206).send(file);
- } else {
- throw new Error("no decryption key found for regular codec! this is typical. don't fret!");
- }
- } else if (codecType.regularOrWebplayback === "webplayback") {
- const webplaybackCodec = codecType.codecType as WebplaybackCodecType; // safe cast, zod
- const webplaybackResponse = await appleMusicApi.getWebplayback(id);
- const streamInfo = await StreamInfo.fromWebplayback(webplaybackResponse, webplaybackCodec);
-
- if (streamInfo.widevinePssh !== undefined) {
- const decryptionKey = await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
- const file = await fetchAndDecryptStreamSegment(originalMp4, decryptionKey, end - start + 1, start);
-
- res.setHeader("Content-Type", "application/mp4");
- res.setHeader("Content-Range", `bytes ${start}-${end}/*`);
- res.setHeader("Accept-Ranges", "bytes");
- res.status(206).send(file);
- } else {
- throw new Error("no decryption key found for web playback! this should not happen..");
- }
+ if (streamInfo.widevinePssh === undefined) {
+ if (codecType.regularOrWebplayback === "regular") { throw new Error("failed to get widevine pssh, this is typical"); }
+ else { throw new Error("failed to get widevine pssh for web playback, this should not happen.."); }
}
+
+ const decryptionKey =
+ await getKeyFromCache(id, codecType.codecType) ||
+ await getWidevineDecryptionKey(streamInfo.widevinePssh, streamInfo.trackId);
+ await addKeyToCache(id, codecType.codecType, decryptionKey);
+
+ const file = await fetchAndDecryptStreamSegment(originalMp4, decryptionKey, end - start + 1, start);
+ res.setHeader("Content-Type", "application/mp4");
+ res.setHeader("Content-Range", `bytes ${start}-${end}/*`);
+ res.setHeader("Accept-Ranges", "bytes");
+ res.status(206).send(file);
} catch (err) {
next(err);
}
diff --git a/src/web/endpoints/front/download.ts b/src/web/endpoints/front/download.ts
index e240a07..d05cebe 100644
--- a/src/web/endpoints/front/download.ts
+++ b/src/web/endpoints/front/download.ts
@@ -16,7 +16,7 @@ router.get("/download", async (req, res, next) => {
const { id } = (await validate(req, schema)).query;
res.render("download", {
- title: "download",
+ title: "download track",
codecs: config.server.frontend.displayed_codecs,
id: id
});
diff --git a/src/web/endpoints/front/downloadAlbum.ts b/src/web/endpoints/front/downloadAlbum.ts
new file mode 100644
index 0000000..79706ad
--- /dev/null
+++ b/src/web/endpoints/front/downloadAlbum.ts
@@ -0,0 +1,28 @@
+import express from "express";
+import { validate } from "../../validate.js";
+import { z } from "zod";
+import { config } from "../../../config.js";
+
+const router = express.Router();
+
+const schema = z.object({
+ query: z.object({
+ id: z.string()
+ })
+});
+
+router.get("/downloadAlbum", async (req, res, next) => {
+ try {
+ const { id } = (await validate(req, schema)).query;
+
+ res.render("downloadAlbum", {
+ title: "download album",
+ codecs: config.server.frontend.displayed_codecs,
+ id: id
+ });
+ } catch (err) {
+ next(err);
+ }
+});
+
+export default router;
diff --git a/src/web/endpoints/front/search.ts b/src/web/endpoints/front/search.ts
index 9d2da03..4fc2ff9 100644
--- a/src/web/endpoints/front/search.ts
+++ b/src/web/endpoints/front/search.ts
@@ -42,6 +42,8 @@ router.get("/", async (req, res, next) => {
name: name,
artists: [artistName],
cover: cover,
+ id: result.attributes.playParams?.id,
+ isAlbum: true,
tracks: tracks.map((track) => {
const { artistName, name, durationInMillis, discNumber, trackNumber } = track.attributes;
@@ -52,7 +54,8 @@ router.get("/", async (req, res, next) => {
artists: [artistName],
duration: durationInMillis,
cover: cover,
- id: track.attributes.playParams?.id
+ id: track.attributes.playParams?.id,
+ isAlbum: false
};
})
};
diff --git a/src/web/endpoints/index.ts b/src/web/endpoints/index.ts
index 9c3e5b2..5db5440 100644
--- a/src/web/endpoints/index.ts
+++ b/src/web/endpoints/index.ts
@@ -1,15 +1,18 @@
import documentation from "./front/documentation.js";
import frontDownload from "./front/download.js";
+import frontDownloadAlbum from "./front/downloadAlbum.js";
import search from "./front/search.js";
export const front = [
documentation,
frontDownload,
+ frontDownloadAlbum,
search
];
import backDownload from "./back/download.js";
import convertPlaylist from "./back/convertPlaylist.js";
import downloadSegment from "./back/downloadSegment.js";
+import downloadAlbum from "./back/downloadAlbum.js";
import getAlbumMetadata from "./back/getAlbumMetadata.js";
import getPlaylistMetadata from "./back/getPlaylistMetadata.js";
import getTrackMetadata from "./back/getTrackMetadata.js";
@@ -17,6 +20,7 @@ export const back = [
backDownload,
convertPlaylist,
downloadSegment,
+ downloadAlbum,
getAlbumMetadata,
getPlaylistMetadata,
getTrackMetadata
diff --git a/views/downloadAlbum.handlebars b/views/downloadAlbum.handlebars
new file mode 100644
index 0000000..550f926
--- /dev/null
+++ b/views/downloadAlbum.handlebars
@@ -0,0 +1,9 @@
+
diff --git a/views/partials/download.handlebars b/views/partials/download.handlebars
index ebd43be..9f3697d 100644
--- a/views/partials/download.handlebars
+++ b/views/partials/download.handlebars
@@ -1,5 +1,9 @@
{{#if id}}
- dl
+ {{#if isAlbum}}
+ dl
+ {{else}}
+ dl
+ {{/if}}
{{else}}
dl
{{/if}}
diff --git a/views/partials/result.handlebars b/views/partials/result.handlebars
index 0e3d8cd..a04dce7 100644
--- a/views/partials/result.handlebars
+++ b/views/partials/result.handlebars
@@ -5,6 +5,7 @@
{{name}}
{{arrayJoin artists ", "}}
+ {{> download}}