From 28c825b04be36dc3bf8754d0889853499c683e62 Mon Sep 17 00:00:00 2001 From: Eclypsed Date: Fri, 21 Jun 2024 03:35:00 -0400 Subject: [PATCH] Database overhall with knex.js, some things untested --- package-lock.json | 608 +++++------------- package.json | 5 +- src/app.d.ts | 82 ++- src/hooks.server.ts | 35 +- src/lib/components/media/artistList.svelte | 17 +- src/lib/components/media/lazyImage.svelte | 9 +- src/lib/components/media/mediaPlayer.svelte | 50 +- src/lib/components/navbar/navTab.svelte | 23 - src/lib/components/util/mixTab.svelte | 42 ++ src/lib/components/util/navTab.svelte | 39 ++ src/lib/components/util/slider.svelte | 6 +- src/lib/server/api-helper.ts | 48 ++ src/lib/server/connections.ts | 27 - src/lib/server/db.ts | 287 +++++---- src/lib/server/musicBrainz.ts | 77 --- src/lib/server/youtube-music.ts | 50 +- src/routes/(app)/+layout.svelte | 48 +- src/routes/(app)/library/+layout.svelte | 2 +- src/routes/(app)/library/+page.server.ts | 5 + src/routes/(app)/library/+page.svelte | 19 +- src/routes/(app)/library/albums/+page.svelte | 10 +- .../(app)/library/albums/albumCard.svelte | 25 +- src/routes/(app)/user/+page.server.ts | 24 +- src/routes/api/audio/+server.ts | 4 +- src/routes/api/connections/+server.ts | 25 +- .../[connectionId]/album/+server.ts | 4 +- .../album/[albumId]/items/+server.ts | 7 +- .../[connectionId]/playlist/+server.ts | 4 +- .../playlist/[playlistId]/items/+server.ts | 4 +- src/routes/api/remoteImage/+server.ts | 50 ++ src/routes/api/search/+server.ts | 23 +- .../api/users/[userId]/connections/+server.ts | 23 +- .../users/[userId]/library/albums/+server.ts | 6 +- .../users/[userId]/library/artists/+server.ts | 6 +- .../[userId]/library/playlists/+server.ts | 6 +- .../users/[userId]/recommendations/+server.ts | 25 +- src/routes/api/v1/mixes/[mixId]/+server.ts | 41 ++ .../api/v1/mixes/[mixId]/items/+server.ts | 28 + .../api/v1/users/[userId]/mixes/+server.ts | 30 + src/routes/login/+page.server.ts | 18 +- 40 files changed, 941 insertions(+), 901 deletions(-) delete mode 100644 src/lib/components/navbar/navTab.svelte create mode 100644 src/lib/components/util/mixTab.svelte create mode 100644 src/lib/components/util/navTab.svelte create mode 100644 src/lib/server/api-helper.ts delete mode 100644 src/lib/server/connections.ts delete mode 100644 src/lib/server/musicBrainz.ts create mode 100644 src/routes/(app)/library/+page.server.ts create mode 100644 src/routes/api/v1/mixes/[mixId]/+server.ts create mode 100644 src/routes/api/v1/mixes/[mixId]/items/+server.ts create mode 100644 src/routes/api/v1/users/[userId]/mixes/+server.ts diff --git a/package-lock.json b/package-lock.json index 6388d93..a450d6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,15 @@ "version": "0.0.1", "dependencies": { "@fortawesome/fontawesome-free": "^6.5.1", - "@types/better-sqlite3": "^7.6.8", + "@types/better-sqlite3": "^7.6.10", "@types/jsonwebtoken": "^9.0.5", "bcrypt-ts": "^5.0.1", "better-sqlite3": "^9.3.0", "fast-average-color": "^9.4.0", "googleapis": "^133.0.0", "jsonwebtoken": "^9.0.2", - "musicbrainz-api": "^0.15.0" + "knex": "^3.1.0", + "zod": "^3.23.8" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.0.0", @@ -748,17 +749,6 @@ "win32" ] }, - "node_modules/@sindresorhus/is": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-6.3.0.tgz", - "integrity": "sha512-bOSPck7aIJjASXIg1qvXSIjXhVBpIEKdl2Wxg4pVqoTRPL8wWExKBrnGIh6CEnhkFQHfc36k7APhO3uXV4g5xg==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@sveltejs/adapter-auto": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.1.1.tgz", @@ -842,30 +832,14 @@ "vite": "^5.0.0" } }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, "node_modules/@types/better-sqlite3": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.8.tgz", - "integrity": "sha512-ASndM4rdGrzk7iXXqyNC4fbwt4UEjpK0i3j4q4FyeQrLAthfB6s7EF135ZJE0qQxtKIMFwmyT6x0switET7uIw==", + "version": "7.6.10", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.10.tgz", + "integrity": "sha512-TZBjD+yOsyrUJGmcUj6OS3JADk3+UZcNv3NOBqGkM09bZdi28fNZw8ODqbMOLfKCu7RYCO62/ldq1iHbzxqoPw==", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/caseless": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", - "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" - }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -878,11 +852,6 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" - }, "node_modules/@types/jsonwebtoken": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", @@ -905,35 +874,6 @@ "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", "dev": true }, - "node_modules/@types/request": { - "version": "2.48.12", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", - "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - } - }, - "node_modules/@types/request-promise-native": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/@types/request-promise-native/-/request-promise-native-1.0.21.tgz", - "integrity": "sha512-NJ1M6iqWTEUT+qdP+OmXsRZ6tSdkoBdblHKatIWTVP1HdYpHU3IkfpLPf4MWb0+CC4Nl3TtLpYhDlhjZxytDIA==", - "dependencies": { - "@types/request": "*" - } - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" - }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" - }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -1015,11 +955,6 @@ "dequal": "^2.0.3" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, "node_modules/autoprefixer": { "version": "10.4.17", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", @@ -1155,12 +1090,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1235,58 +1170,6 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request": { - "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", - "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacheable-request/node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -1324,9 +1207,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001579", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", - "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", + "version": "1.0.30001636", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", + "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", "dev": true, "funding": [ { @@ -1343,11 +1226,6 @@ } ] }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1411,16 +1289,10 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" }, "node_modules/commander": { "version": "4.1.1", @@ -1532,14 +1404,6 @@ "node": ">=0.10.0" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "engines": { - "node": ">=10" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1556,14 +1420,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1709,7 +1565,14 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", "engines": { "node": ">=6" } @@ -1781,9 +1644,9 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -1808,27 +1671,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/form-data-encoder": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", - "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==", - "engines": { - "node": ">= 18" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -1919,17 +1761,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8.0.0" } }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -2053,30 +1897,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/got/-/got-14.2.1.tgz", - "integrity": "sha512-KOaPMremmsvx6l9BLC04LYE6ZFW4x7e4HkTe3LwBmtuYYQwpeS4XKqzhubTIkaQ1Nr+eXxeori0zuwupXMovBQ==", - "dependencies": { - "@sindresorhus/is": "^6.1.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.14", - "decompress-response": "^6.0.0", - "form-data-encoder": "^4.0.2", - "get-stream": "^8.0.1", - "http2-wrapper": "^2.2.1", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^4.0.1", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2158,28 +1978,6 @@ "node": ">= 0.4" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" - }, - "node_modules/http-status-codes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", - "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==" - }, - "node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", @@ -2257,6 +2055,14 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2273,7 +2079,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -2381,24 +2186,6 @@ "bignumber.js": "^9.0.0" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, - "node_modules/jsontoxml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/jsontoxml/-/jsontoxml-1.0.1.tgz", - "integrity": "sha512-dtKGq0K8EWQBRqcAaePSgKR4Hyjfsz/LkurHSV3Cxk4H+h2fWDeaN2jzABz+ZmOJylgXS7FGeWmbZ6jgYUMdJQ==", - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -2439,14 +2226,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dependencies": { - "json-buffer": "3.0.1" - } - }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -2456,6 +2235,72 @@ "node": ">=6" } }, + "node_modules/knex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", + "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/knex/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -2477,6 +2322,11 @@ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "dev": true }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2512,17 +2362,6 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", @@ -2572,25 +2411,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -2680,29 +2500,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/musicbrainz-api": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/musicbrainz-api/-/musicbrainz-api-0.15.0.tgz", - "integrity": "sha512-PaL/it5DqreQPQBusxPuIIwRWedn51cK9NatIdHn654BS/DCHmsam2s65piD0gcjBokzKCAYFvEgJm4yLDRLBw==", - "dependencies": { - "@types/caseless": "^0.12.1", - "@types/request-promise-native": "^1.0.17", - "@types/uuid": "^9.0.0", - "caseless": "^0.12.0", - "debug": "^4.3.4", - "got": "^14.2.1", - "http-status-codes": "^2.1.4", - "json-stringify-safe": "^5.0.1", - "jsontoxml": "^1.0.1", - "rate-limit-threshold": "^0.1.5", - "source-map-support": "^0.5.16", - "tough-cookie": "^4.1.3", - "uuid": "^9.0.0" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - } - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -2791,17 +2588,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-url": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", - "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2836,14 +2622,6 @@ "wrappy": "1" } }, - "node_modules/p-cancelable": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", - "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", - "engines": { - "node": ">=14.16" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2877,8 +2655,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.10.1", @@ -2907,6 +2684,11 @@ "is-reference": "^3.0.0" } }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3208,11 +2990,6 @@ } } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -3236,11 +3013,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3261,25 +3033,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rate-limit-threshold": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/rate-limit-threshold/-/rate-limit-threshold-0.1.5.tgz", - "integrity": "sha512-75vpvXC/ZqQJrFDp0dVtfoXZi8kxQP2eBuxVYFvGDfnHhcgE+ZG870u4ItQhWQh54Y6nNwOaaq5g3AL9n27lTg==", - "engines": { - "node": "^14.13.1 || >=16.0.0" - } - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -3328,16 +3081,21 @@ "node": ">=8.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -3350,11 +3108,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3364,20 +3117,6 @@ "node": ">=4" } }, - "node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3669,14 +3408,6 @@ "sorcery": "bin/sorcery" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -3686,15 +3417,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3891,7 +3613,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4096,6 +3817,14 @@ "node": ">=6" } }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -4117,6 +3846,14 @@ "node": ">=0.8" } }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, "node_modules/tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", @@ -4148,28 +3885,6 @@ "node": ">=6" } }, - "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -4216,14 +3931,6 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -4254,15 +3961,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", @@ -4492,6 +4190,14 @@ "engines": { "node": ">= 14" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 433f9b2..bd65740 100644 --- a/package.json +++ b/package.json @@ -28,13 +28,14 @@ "type": "module", "dependencies": { "@fortawesome/fontawesome-free": "^6.5.1", - "@types/better-sqlite3": "^7.6.8", + "@types/better-sqlite3": "^7.6.10", "@types/jsonwebtoken": "^9.0.5", "bcrypt-ts": "^5.0.1", "better-sqlite3": "^9.3.0", "fast-average-color": "^9.4.0", "googleapis": "^133.0.0", "jsonwebtoken": "^9.0.2", - "musicbrainz-api": "^0.15.0" + "knex": "^3.1.0", + "zod": "^3.23.8" } } diff --git a/src/app.d.ts b/src/app.d.ts index 470aaad..0efefb6 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -18,6 +18,13 @@ declare global { // Do not store data from other services in the database, only the data necessary to fetch whatever you need. // This avoid syncronization issues. E.g. Store userId, and urlOrigin to fetch the user's name and profile picture. + // Note to self: POST vs PUT vs PATCH + // Use POST when a new resource is being created + // Use PUT when a resource is being replaced. Semantically, PUT means the entire replacement resource needs to be provided in the request + // Use PATCH when a resource is being changed or updated. Semantically, PATCH means only a partial resource needs to be provided in the request (The parts being updated/changed) + + type ConnectionType = 'jellyfin' | 'youtube-music' + type User = { id: string username: string @@ -59,57 +66,61 @@ declare global { public readonly id: string /** Retireves general information about the connection */ - getConnectionInfo: () => Promise + getConnectionInfo(): Promise /** Get's the user's recommendations from the corresponding service */ - getRecommendations: () => Promise<(Song | Album | Artist | Playlist)[]> + getRecommendations(): Promise<(Song | Album | Artist | Playlist)[]> /** - * @param searchTerm The string of text to query - * @param filter Optional. A string of either 'song', 'album', 'artist', or 'playlist' to filter the kind of media items queried - * @returns A promise of an array of media items + * @param {string} searchTerm The string of text to query + * @param {'song' | 'album' | 'artist' | 'playlist'} filter Optional. A string of either 'song', 'album', 'artist', or 'playlist' to filter the kind of media items queried + * @returns {Promise<(Song | Album | Artist | Playlist)[]>} A promise of an array of media items */ - search: (searchTerm: string, filter?: T) => Promise[]> + search(searchTerm: string, filter?: T): Promise[]> /** - * @param id The id of the requested song - * @param headers The request headers sent by the Lazuli client that need to be relayed to the connection's request to the server (e.g. 'range'). - * @returns A promise of response object containing the audio stream for the specified byte range + * @param {string} id The id of the requested song + * @param {Headers} headers The request headers sent by the Lazuli client that need to be relayed to the connection's request to the server (e.g. 'range'). + * @returns {Promise} A promise of response object containing the audio stream for the specified byte range * * Fetches the audio stream for a song. Will return an response containing the audio stream if the fetch was successfull, otherwise throw an error. */ - getAudioStream: (id: string, headers: Headers) => Promise + getAudioStream(id: string, headers: Headers): Promise /** - * @param id The id of an album - * @returns A promise of the album as an Album object + * @param {string} id The id of an album + * @returns {Promise} A promise of the album as an Album object */ - getAlbum: (id: string) => Promise + getAlbum(id: string): Promise /** - * @param id The id of an album - * @returns A promise of the songs in the album as and array of Song objects + * @param {string} id The id of an album + * @returns {Promise} A promise of the songs in the album as and array of Song objects */ - getAlbumItems: (id: string) => Promise + getAlbumItems(id: string): Promise /** - * @param id The id of a playlist - * @returns A promise of the playlist of as a Playlist object + * @param {string} id The id of a playlist + * @returns {Promise} A promise of the playlist of as a Playlist object */ - getPlaylist: (id: string) => Promise + getPlaylist(id: string): Promise /** - * @param id The id of a playlist - * @param startIndex The index to start at (0 based). All playlist items with a lower index will be dropped from the results - * @param limit The maximum number of playlist items to return - * @returns A promise of the songs in the playlist as and array of Song objects + * @param {string} id The id of a playlist + * @param {number} startIndex The index to start at (0 based). All playlist items with a lower index will be dropped from the results + * @param {number} limit The maximum number of playlist items to return + * @returns {Promise} A promise of the songs in the playlist as and array of Song objects */ - getPlaylistItems: (id: string, options?: { startIndex?: number, limit?: number }) => Promise + getPlaylistItems(id: string, options?: { startIndex?: number, limit?: number }): Promise + + public readonly songs?: { // Optional because YouTube Music can't be asked to provide an actually useful API. + songs(ids: string[]): Promise + } public readonly library: { - albums: () => Promise - artists: () => Promise - playlists: () => Promise + albums(): Promise + artists(): Promise + playlists(): Promise } } @@ -123,7 +134,7 @@ declare global { type Song = { connection: { id: string - type: 'jellyfin' | 'youtube-music' + type: ConnectionType } id: string name: string @@ -150,7 +161,7 @@ declare global { type Album = { connection: { id: string - type: 'jellyfin' | 'youtube-music' + type: ConnectionType } id: string name: string @@ -167,7 +178,7 @@ declare global { type Artist = { connection: { id: string - type: 'jellyfin' | 'youtube-music' + type: ConnectionType } id: string name: string @@ -178,7 +189,7 @@ declare global { type Playlist = { // Keep Playlist items seperate from the playlist itself. What's really nice is playlist items can just be an ordered array of Songs connection: { id: string - type: 'jellyfin' | 'youtube-music' + type: ConnectionType } id: string name: string @@ -190,6 +201,15 @@ declare global { } } + type Mix = { + id: string + name: string + thumbnail?: string + description?: string + trackCount: number + duration: number + } + type HasDefinedProperty = T & { [P in K]-?: Exclude }; } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index b060e64..e0d9419 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,6 @@ -import { redirect, type Handle, type HandleFetch, type RequestEvent } from '@sveltejs/kit' +import { redirect, type Handle, type RequestEvent } from '@sveltejs/kit' import { SECRET_INTERNAL_API_KEY, SECRET_JWT_KEY } from '$env/static/private' +import { userExists, mixExists } from '$lib/server/api-helper' import jwt from 'jsonwebtoken' function verifyAuthToken(event: RequestEvent) { @@ -14,18 +15,30 @@ function verifyAuthToken(event: RequestEvent) { } } +const unauthorizedResponse = new Response('Unauthorized.', { status: 401 }) +const userNotFoundResponse = new Response('User not found.', { status: 404 }) +const mixNotFoundResponse = new Response('Mix not found.', { status: 404 }) + +// * Custom Handle specifically for requests made to the API endpoint. Handles authorization and any other middleware verifications +const handleAPIRequest: Handle = async ({ event, resolve }) => { + const authorized = event.request.headers.get('apikey') === SECRET_INTERNAL_API_KEY || event.url.searchParams.get('apikey') === SECRET_INTERNAL_API_KEY || verifyAuthToken(event) + if (!authorized) unauthorizedResponse + + const userId = event.params.userId + if (userId && !(await userExists(userId))) return userNotFoundResponse + + const mixId = event.params.mixId + if (mixId && !(await mixExists(mixId))) return mixNotFoundResponse + + return resolve(event) +} + export const handle: Handle = async ({ event, resolve }) => { const urlpath = event.url.pathname if (urlpath.startsWith('/login')) return resolve(event) - if (urlpath.startsWith('/api')) { - if (event.request.headers.get('apikey') === SECRET_INTERNAL_API_KEY || event.url.searchParams.get('apikey') === SECRET_INTERNAL_API_KEY || verifyAuthToken(event)) { - return resolve(event) - } - - return new Response('Unauthorized', { status: 401 }) - } + if (urlpath.startsWith('/api')) return handleAPIRequest({ event, resolve }) const authToken = event.cookies.get('lazuli-auth') if (!authToken) throw redirect(303, `/login?redirect=${urlpath}`) @@ -39,9 +52,3 @@ export const handle: Handle = async ({ event, resolve }) => { return resolve(event) } - -export const handleFetch: HandleFetch = async ({ event, request, fetch }) => { - const authorized = verifyAuthToken(event) - - return authorized ? fetch(request) : new Response('Unauthorized', { status: 401 }) -} diff --git a/src/lib/components/media/artistList.svelte b/src/lib/components/media/artistList.svelte index 31f81ef..edb19a9 100644 --- a/src/lib/components/media/artistList.svelte +++ b/src/lib/components/media/artistList.svelte @@ -12,18 +12,16 @@ export let linked = true -
+
{#if 'artists' in mediaItem && mediaItem.artists && typeof mediaItem.artists === 'string'} {mediaItem.artists} {:else if 'artists' in mediaItem && mediaItem.artists && typeof mediaItem.artists !== 'string' && mediaItem.artists.length > 0} {#each mediaItem.artists as artist, index} + {@const needsComma = index < mediaItem.artists.length - 1} {#if linked} - {artist.name} + {artist.name} {:else} - {artist.name} - {/if} - {#if index < mediaItem.artists.length - 1} - , + {artist.name} {/if} {/each} {:else if 'uploader' in mediaItem && mediaItem.uploader} @@ -40,3 +38,10 @@ {/if} {/if}
+ + diff --git a/src/lib/components/media/lazyImage.svelte b/src/lib/components/media/lazyImage.svelte index beae5d2..6279e8f 100644 --- a/src/lib/components/media/lazyImage.svelte +++ b/src/lib/components/media/lazyImage.svelte @@ -22,11 +22,12 @@ let imageContainer: HTMLDivElement + // TODO: Implement auto-resizing function updateImage(newThumbnailURL: string) { if (!imageContainer) return - const width = imageContainer.clientWidth - const height = imageContainer.clientHeight + const width = imageContainer.clientWidth * 1.5 // 1.5x is a good compromise between sharpness and performance + const height = imageContainer.clientHeight * 1.5 const newImage = new Image(width, height) imageContainer.appendChild(newImage) @@ -57,8 +58,8 @@ } newImage.onerror = () => { - removeOldImage() - newImage.style.opacity = '1' + console.error(`Image from url: ${newThumbnailURL} failed to update`) + imageContainer.removeChild(newImage) } } diff --git a/src/lib/components/media/mediaPlayer.svelte b/src/lib/components/media/mediaPlayer.svelte index f660c2a..dcd3271 100644 --- a/src/lib/components/media/mediaPlayer.svelte +++ b/src/lib/components/media/mediaPlayer.svelte @@ -10,6 +10,10 @@ import ScrollingText from '$lib/components/util/scrollingText.svelte' import ArtistList from './artistList.svelte' + // NEW IDEA: Only have the miniplayer for controls and for the expanded view just make it one large Videoplayer. + // That way we can target the player to be the size of YouTube's default player. Then move the Queue view to it's own + // dedicated sidebar like in spotify. + $: currentlyPlaying = $queue.current let expanded = false @@ -45,15 +49,15 @@ navigator.mediaSession.metadata = new MediaMetadata({ title: media.name, - artist: media.artists?.map((artist) => artist.name).join(', ') || media.uploader?.name, + artist: media.artists?.map((artist) => artist.name).join(', ') ?? media.uploader?.name, album: media.album?.name, artwork: [ - { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=96`, sizes: '96x96', type: 'image/png' }, - { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=128`, sizes: '128x128', type: 'image/png' }, - { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=192`, sizes: '192x192', type: 'image/png' }, - { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=256`, sizes: '256x256', type: 'image/png' }, - { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=384`, sizes: '384x384', type: 'image/png' }, - { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=512`, sizes: '512x512', type: 'image/png' }, + { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=96`, sizes: '96x96' }, + { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=128`, sizes: '128x128' }, + { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=192`, sizes: '192x192' }, + { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=256`, sizes: '256x256' }, + { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=384`, sizes: '384x384' }, + { src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=512`, sizes: '512x512' }, ], }) } @@ -103,7 +107,7 @@ {#if !expanded}
-
+
@@ -121,16 +125,14 @@ $queue.previous()}> -
- (paused = !paused)}> -
- {#if waiting} - - {:else} - - {/if} -
-
+
+ {#if waiting} + + {:else} + (paused = !paused)}> + + + {/if}
$queue.clear()}> @@ -156,7 +158,10 @@
-
+
+ (volume = volume > 0 ? 0 : Number(localStorage.getItem('volume')))}> + + 0) localStorage.setItem('volume', volume.toString()) }} /> - (volume = volume > 0 ? 0 : Number(localStorage.getItem('volume')))}> - -
(shuffled ? $queue.reorder() : $queue.shuffle())}> @@ -213,7 +215,7 @@
-
+
- import { goto } from '$app/navigation' - - export let icon: string - export let label: string - export let redirect: string - - export let disabled: boolean - - - - - diff --git a/src/lib/components/util/mixTab.svelte b/src/lib/components/util/mixTab.svelte new file mode 100644 index 0000000..38e7087 --- /dev/null +++ b/src/lib/components/util/mixTab.svelte @@ -0,0 +1,42 @@ + + + + + diff --git a/src/lib/components/util/navTab.svelte b/src/lib/components/util/navTab.svelte new file mode 100644 index 0000000..f9a14fd --- /dev/null +++ b/src/lib/components/util/navTab.svelte @@ -0,0 +1,39 @@ + + + + + diff --git a/src/lib/components/util/slider.svelte b/src/lib/components/util/slider.svelte index 97e3b45..52716c1 100644 --- a/src/lib/components/util/slider.svelte +++ b/src/lib/components/util/slider.svelte @@ -26,7 +26,7 @@
dispatch('seeking', { value: event.currentTarget.value })} on:change={(event) => dispatch('seeked', { value: event.currentTarget.value })} type="range" - class="absolute z-10 h-1.5 w-full" + class="absolute z-10 h-1 w-full" step="any" min="0" {max} @@ -48,7 +48,7 @@ aria-hidden="true" aria-disabled="true" /> - +
diff --git a/src/lib/server/api-helper.ts b/src/lib/server/api-helper.ts new file mode 100644 index 0000000..a8cabcb --- /dev/null +++ b/src/lib/server/api-helper.ts @@ -0,0 +1,48 @@ +import { DB, type DBSchemas } from './db' +import { Jellyfin } from './jellyfin' +import { YouTubeMusic } from './youtube-music' + +export async function userExists(userId: string): Promise { + return Boolean(await DB.users.where('id', userId).first(DB.knex.raw('EXISTS(SELECT 1)'))) +} + +export async function mixExists(mixId: string): Promise { + return Boolean(await DB.mixes.where('id', mixId).first(DB.knex.raw('EXISTS(SELECT 1)'))) +} + +function connectionBuilder(schema: DBSchemas.Connections): Connection { + const { id, userId, type, serviceUserId, accessToken } = schema + switch (type) { + case 'jellyfin': + return new Jellyfin(id, userId, serviceUserId, schema.serverUrl, accessToken) + case 'youtube-music': + return new YouTubeMusic(id, userId, serviceUserId, accessToken, schema.refreshToken, schema.expiry) + } +} + +/** + * Queries the database for a specific connection. + * + * @param id The id of the connection + * @returns An instance of a Connection + * @throws ReferenceError if there is no connection with an id matches the one passed + */ +export async function buildConnection(id: string): Promise { + const schema = await DB.connections.where('id', id).first() + if (!schema) throw ReferenceError(`Connection of Id ${id} does not exist`) + + return connectionBuilder(schema) +} + +/** + * Queries the database for all connections belong to a user of the specified id. + * + * @param userId The id of a user + * @returns An array of connection instances for each of the user's connections + * @throws ReferenceError if there is no user with an id matches the one passed + */ +export async function buildUserConnections(userId: string): Promise { + if (!(await userExists(userId))) throw ReferenceError(`User of Id ${userId} does not exist`) + + return (await DB.connections.where('userId', userId).select('*')).map(connectionBuilder) +} diff --git a/src/lib/server/connections.ts b/src/lib/server/connections.ts deleted file mode 100644 index 9e486fd..0000000 --- a/src/lib/server/connections.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DB } from './db' -import { Jellyfin } from './jellyfin' -import { YouTubeMusic } from './youtube-music' - -const constructConnection = (connectionInfo: ReturnType): Connection | undefined => { - if (!connectionInfo) return undefined - - const { id, userId, type, service, tokens } = connectionInfo - switch (type) { - case 'jellyfin': - return new Jellyfin(id, userId, service.userId, service.serverUrl, tokens.accessToken) - case 'youtube-music': - return new YouTubeMusic(id, userId, service.userId, tokens.accessToken, tokens.refreshToken, tokens.expiry) - } -} -function getConnection(id: string): Connection | undefined { - return constructConnection(DB.getConnectionInfo(id)) -} - -const getUserConnections = (userId: string): Connection[] | undefined => { - return DB.getUserConnectionInfo(userId)?.map((info) => constructConnection(info)!) -} - -export const Connections = { - getConnection, - getUserConnections, -} diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index 5c30769..c980e27 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -1,132 +1,187 @@ -import Database from 'better-sqlite3' -import type { Database as Sqlite3DB } from 'better-sqlite3' -import { generateUUID } from '$lib/utils' +import knex from 'knex' +import { SqliteError } from 'better-sqlite3' -interface DBConnectionsTableSchema { - id: string - userId: string - type: string - service?: string - tokens?: string -} +const connectionTypes = ['jellyfin', 'youtube-music'] -export type ConnectionRow = { - id: string - userId: string -} & ( - | { - type: 'jellyfin' - service: { - userId: string - serverUrl: string - } - tokens: { - accessToken: string - } - } - | { - type: 'youtube-music' - service: { - userId: string - } - tokens: { - accessToken: string - refreshToken: string - expiry: number - } - } -) - -class Storage { - private readonly database: Sqlite3DB - - constructor(database: Sqlite3DB) { - this.database = database - this.database.pragma('foreign_keys = ON') - this.database.exec(`CREATE TABLE IF NOT EXISTS Users( - id VARCHAR(36) PRIMARY KEY, - username VARCHAR(30) UNIQUE NOT NULL, - passwordHash VARCHAR(72) NOT NULL - )`) - this.database.exec(`CREATE TABLE IF NOT EXISTS Connections( - id VARCHAR(36) PRIMARY KEY, - userId VARCHAR(36) NOT NULL, - type VARCHAR(36) NOT NULL, - service TEXT, - tokens TEXT, - FOREIGN KEY(userId) REFERENCES Users(id) - )`) - this.database.exec(`CREATE TABLE IF NOT EXISTS Playlists( - id VARCHAR(36) PRIMARY KEY, - userId VARCHAR(36) NOT NULL, - name TEXT NOT NULL, - description TEXT, - items TEXT, - FOREIGN KEY(userId) REFERENCES Users(id) - )`) +export declare namespace DBSchemas { + interface Users { + id: string + username: string + passwordHash: string } - public getUser = (id: string): User | undefined => { - const user = this.database.prepare(`SELECT * FROM Users WHERE id = ? LIMIT 1`).get(id) as User | undefined - return user + interface JellyfinConnection { + id: string + userId: string + type: 'jellyfin' + serviceUserId: string + serverUrl: string + accessToken: string } - public getUsername = (username: string): User | undefined => { - const user = this.database.prepare(`SELECT * FROM Users WHERE lower(username) = ? LIMIT 1`).get(username.toLowerCase()) as User | undefined - return user + interface YouTubeMusicConnection { + id: string + userId: string + type: 'youtube-music' + serviceUserId: string + accessToken: string + refreshToken: string + expiry: number } - public addUser = (username: string, passwordHash: string): User => { - const userId = generateUUID() - this.database.prepare(`INSERT INTO Users(id, username, passwordHash) VALUES(?, ?, ?)`).run(userId, username, passwordHash) - return this.getUser(userId)! + type Connections = JellyfinConnection | YouTubeMusicConnection + + interface Mixes { + id: string + userId: string + name: string + thumbnailTag?: string + description?: string + trackCount: number + duration: number } - public deleteUser = (id: string): void => { - const commandInfo = this.database.prepare(`DELETE FROM Users WHERE id = ?`).run(id) - if (commandInfo.changes === 0) throw new Error(`User with id ${id} does not exist`) + interface MixItems { + mixId: string + connectionId: string + connectionType: ConnectionType + id: string + index: number } - public getConnectionInfo = (id: string): ConnectionRow | undefined => { - const result = this.database.prepare(`SELECT * FROM Connections WHERE id = ? LIMIT 1`).get(id) as DBConnectionsTableSchema | undefined - if (!result) return undefined - - const { userId, type, service, tokens } = result - const parsedService = service ? JSON.parse(service) : undefined - const parsedTokens = tokens ? JSON.parse(tokens) : undefined - return { id, userId, type: type as ConnectionRow['type'], service: parsedService, tokens: parsedTokens } - } - - public getUserConnectionInfo = (userId: string): ConnectionRow[] | undefined => { - const user = this.getUser(userId) - if (!user) return undefined - - const connectionRows = this.database.prepare(`SELECT * FROM Connections WHERE userId = ?`).all(userId) as DBConnectionsTableSchema[] - const connections: ConnectionRow[] = [] - for (const { id, type, service, tokens } of connectionRows) { - const parsedService = service ? JSON.parse(service) : undefined - const parsedTokens = tokens ? JSON.parse(tokens) : undefined - connections.push({ id, userId, type: type as ConnectionRow['type'], service: parsedService, tokens: parsedTokens }) + interface Songs { + connectionId: string + connectionType: ConnectionType + id: string + name: string + duration: number + thumbnailUrl: string + releaseDate?: string + artists?: { + id: string + name: string + }[] + album?: { + id: string + name: string } - return connections - } - - public addConnectionInfo = (connectionInfo: Omit): string => { - const { userId, type, service, tokens } = connectionInfo - const connectionId = generateUUID() - this.database.prepare(`INSERT INTO Connections(id, userId, type, service, tokens) VALUES(?, ?, ?, ?, ?)`).run(connectionId, userId, type, JSON.stringify(service), JSON.stringify(tokens)) - return connectionId - } - - public deleteConnectionInfo = (id: string): void => { - const commandInfo = this.database.prepare(`DELETE FROM Connections WHERE id = ?`).run(id) - if (commandInfo.changes === 0) throw new Error(`Connection with id: ${id} does not exist`) - } - - public updateTokens = (id: string, tokens: ConnectionRow['tokens']): void => { - const commandInfo = this.database.prepare(`UPDATE Connections SET tokens = ? WHERE id = ?`).run(JSON.stringify(tokens), id) - if (commandInfo.changes === 0) throw new Error('Failed to update tokens') + uploader?: { + id: string + name: string + } + isVideo: boolean } } -export const DB = new Storage(new Database('./src/lib/server/lazuli.db', { verbose: console.info })) +class Database { + public readonly knex: knex.Knex + + constructor(knex: knex.Knex<'better-sqlite3'>) { + this.knex = knex + } + + public uuid() { + return this.knex.fn.uuid() + } + + public get users() { + return this.knex('Users') + } + + public get connections() { + return this.knex('Connections') + } + + public get mixes() { + return this.knex('Mixes') + } + + public get mixItems() { + return this.knex('MixItems') + } + + public get songs() { + return this.knex('Songs') + } + + public get sqliteError() { + return SqliteError + } + + public static async createUsersTable(db: knex.Knex<'better-sqlite3'>) { + const exists = await db.schema.hasTable('Users') + if (exists) return + + await db.schema.createTable('Users', (tb) => { + tb.uuid('id').primary(), tb.string('username').unique().notNullable().checkLength('<=', 30), tb.string('passwordHash').notNullable().checkLength('=', 60) + }) + } + + public static async createConnectionsTable(db: knex.Knex<'better-sqlite3'>) { + const exists = await db.schema.hasTable('Connections') + if (exists) return + + await db.schema.createTable('Connections', (tb) => { + tb.uuid('id').primary(), + tb.uuid('userId').notNullable().references('id').inTable('Users'), + tb.enum('type', connectionTypes).notNullable(), + tb.string('serviceUserId'), + tb.string('serverUrl'), + tb.string('accessToken'), + tb.string('refreshToken'), + tb.integer('expiry') + }) + } + + public static async createMixesTable(db: knex.Knex<'better-sqlite3'>) { + const exists = await db.schema.hasTable('Mixes') + if (exists) return + + await db.schema.createTable('Mixes', (tb) => { + tb.uuid('id').primary(), + tb.uuid('userId').notNullable().references('id').inTable('Users'), + tb.string('name').notNullable(), + tb.uuid('thumbnailTag'), + tb.string('description'), + tb.integer('trackCount').notNullable(), + tb.integer('duration').notNullable() + }) + } + + public static async createMixItemsTable(db: knex.Knex<'better-sqlite3'>) { + const exists = await db.schema.hasTable('MixItems') + if (exists) return + + await db.schema.createTable('MixItems', (tb) => { + tb.uuid('mixId').notNullable().references('id').inTable('Mixes'), + tb.uuid('connectionId').notNullable().references('id').inTable('Connections'), + tb.enum('connectionType', connectionTypes).notNullable(), + tb.string('id').notNullable() + tb.integer('index').notNullable() + }) + } + + public static async createSongsTable(db: knex.Knex<'better-sqlite3'>) { + const exists = await db.schema.hasTable('Songs') + if (exists) return + + await db.schema.createTable('Songs', (tb) => { + tb.uuid('connectionId').notNullable().references('id').inTable('Connections'), + tb.enum('connectionType', connectionTypes), + tb.string('id').notNullable(), + tb.string('name').notNullable(), + tb.integer('duration').notNullable(), + tb.string('thumbnailUrl').notNullable(), + tb.datetime('releaseDate', { precision: 3 }), + tb.json('artists'), + tb.json('album'), + tb.json('uploader'), + tb.boolean('isVideo').notNullable() + }) + } +} + +const db = knex<'better-sqlite3'>({ client: 'better-sqlite3', connection: { filename: './src/lib/server/lazuli.db' }, useNullAsDefault: false }) +await Promise.all([Database.createUsersTable(db), Database.createConnectionsTable(db), Database.createMixesTable(db), Database.createMixItemsTable(db), Database.createSongsTable(db)]) + +export const DB = new Database(db) diff --git a/src/lib/server/musicBrainz.ts b/src/lib/server/musicBrainz.ts deleted file mode 100644 index 5a8a643..0000000 --- a/src/lib/server/musicBrainz.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { PUBLIC_VERSION } from '$env/static/public' -import { MusicBrainzApi } from 'musicbrainz-api' - -const mbApi = new MusicBrainzApi({ - appName: 'Lazuli', - appVersion: PUBLIC_VERSION, - appContactInfo: 'Ec1ypsed@proton.me', -}) - -async function potentialAliasesFromNames(artistNames: string[]) { - const luceneQuery = artistNames.join(' OR ') - const artistsResponse = await mbApi.search('artist', { query: luceneQuery }) - - const SCORE_THRESHOLD = 90 - const possibleArtists = artistsResponse.artists.filter((artist) => artist.score >= SCORE_THRESHOLD) - const aliases = possibleArtists.flatMap((artist) => [artist.name].concat(artist.aliases?.filter((alias) => alias.primary !== null).map((alias) => alias.name) ?? [])) - return [...new Set(aliases)] // Removes any duplicates -} - -export class MusicBrainz { - static async searchRecording(songName: string, artistNames?: string[]) { - const standardSearchResults = await mbApi.search('recording', { query: songName, limit: 5 }) - const SCORE_THRESHOLD = 90 - const bestResults = standardSearchResults.recordings.filter((recording) => recording.score >= SCORE_THRESHOLD) - - const artistAliases = artistNames ? await potentialAliasesFromNames(artistNames) : null - const luceneQuery = artistAliases ? `"${songName}"`.concat(` AND (${artistAliases.map((alias) => `artist:"${alias}"`).join(' OR ')})`) : `"${songName}"` - - console.log(luceneQuery) - - const searchResults = await mbApi.search('recording', { query: luceneQuery, limit: 1 }) - if (searchResults.recordings.length === 0) { - console.log('Nothing returned for ' + songName) - return null - } - - const topResult = searchResults.recordings[0] - // const bestMatch = searchResults.recordings.reduce((prev, current) => (prev.score > current.score ? prev : current)) - console.log(JSON.stringify(topResult)) - } - - static async searchRelease(albumName: string, artistNames?: string[]): Promise { - const searchResulst = await mbApi.search('release', { query: albumName, limit: 10 }) - if (searchResulst.releases.length === 0) { - console.log(JSON.stringify('Nothing returned for ' + albumName)) - return null - } - - const bestMatch = searchResulst.releases.reduce((prev, current) => { - if (prev.score === current.score) return new Date(prev.date).getTime() > new Date(current.date).getTime() ? prev : current - return prev.score > current.score ? prev : current - }) - - const { id, title, date } = bestMatch - const trackCount = bestMatch.media.reduce((acummulator, current) => acummulator + current['track-count'], 0) - const artists = bestMatch['artist-credit']?.map((artist) => ({ id: artist.artist.id, name: artist.artist.name })) - - return { id, name: title, releaseDate: date, artists, trackCount } satisfies MusicBrainz.ReleaseSearchResult - } - - static async searchArtist(artistName: string) { - const searchResults = await mbApi.search('artist', { query: artistName }) - } -} - -declare namespace MusicBrainz { - type ReleaseSearchResult = { - id: string - name: string - releaseDate: string - artists?: { - id: string - name: string - }[] - trackCount: number - } -} diff --git a/src/lib/server/youtube-music.ts b/src/lib/server/youtube-music.ts index 0f95bac..b1acda1 100644 --- a/src/lib/server/youtube-music.ts +++ b/src/lib/server/youtube-music.ts @@ -1,10 +1,10 @@ import { youtube, type youtube_v3 } from 'googleapis/build/src/apis/youtube' -import { DB } from './db' import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public' import { YOUTUBE_API_CLIENT_SECRET } from '$env/static/private' import type { InnerTube } from './youtube-music-types' +import { DB } from './db' -const ytDataApi = youtube('v3') +const ytDataApi = youtube('v3') // TODO: At some point I want to ditch this package and just make the API calls directly. Fewer dependecies type ytMusicv1ApiRequestParams = | { @@ -55,7 +55,10 @@ export class YouTubeMusic implements Connection { } public async getConnectionInfo() { - const access_token = await this.requestManager.accessToken.catch(() => null) + const access_token = await this.requestManager.accessToken.catch(() => { + console.log('Failed to get yt access token') + return null + }) let username: string | undefined, profilePicture: string | undefined if (access_token) { @@ -515,6 +518,43 @@ export class YouTubeMusic implements Connection { } }) as ScrapedMediaItemMap[] } + + // ! HOLY FUCK HOLY FUCK THIS IS IT!!!! THIS IS HOW YOU CAN BATCH FETCH FULL DETAILS FOR COMPLETELY UNRELATED SONGS IN ONE API CALL!!!! + // ! IT GIVES BACK FUCKING EVERYTHING (almost)! NAME, ALBUM, ARTISTS, UPLOADER, DURATION, THUMBNAIL. + // ! The only thing kinda missing is release date, but that could be fetched from the official API. In fact I'll already need to make a call to + // ! the offical API to get the thumbnails for the videos any way. And since you can batch call that one, you won't be making any extra queries just + // ! to get the release date. HOLY FUCK THIS IS PERFECT! (something is going to go wrong in the future for sure) + private async testMethod(videoIds: string[]) { + const currentDate = new Date() + const year = currentDate.getUTCFullYear().toString() + const month = (currentDate.getUTCMonth() + 1).toString().padStart(2, '0') // Months are zero-based, so add 1 + const day = currentDate.getUTCDate().toString().padStart(2, '0') + + const response = await fetch('https://music.youtube.com/youtubei/v1/music/get_queue', { + headers: { + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0', + authorization: `Bearer ${await this.requestManager.accessToken}`, + }, + method: 'POST', + body: JSON.stringify({ + context: { + client: { + clientName: 'WEB_REMIX', + clientVersion: `1.${year + month + day}.01.00`, + }, + }, + videoIds, + }), + }) + + if (!response.ok) { + console.log(response) + return + } + + const data = await response.json() + console.log(JSON.stringify(data)) + } } function parseTwoRowItemRenderer(rowContent: InnerTube.musicTwoRowItemRenderer): InnerTube.ScrapedSong | InnerTube.ScrapedAlbum | InnerTube.ScrapedArtist | InnerTube.ScrapedPlaylist { @@ -742,8 +782,8 @@ class YTRequestManager { if (this.accessTokenRefreshRequest) return this.accessTokenRefreshRequest this.accessTokenRefreshRequest = refreshAccessToken() - .then(({ accessToken, expiry }) => { - DB.updateTokens(this.connectionId, { accessToken, refreshToken: this.refreshToken, expiry }) + .then(async ({ accessToken, expiry }) => { + await DB.connections.where('id', this.connectionId).update('tokens', { accessToken, refreshToken: this.refreshToken, expiry }) this.currentAccessToken = accessToken this.expiry = expiry this.accessTokenRefreshRequest = null diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index e7088de..e497dc0 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -1,14 +1,32 @@
@@ -24,10 +42,23 @@
-