From 778c0ce992c376c86d21fcadee49af50e08d4f05 Mon Sep 17 00:00:00 2001 From: Ellen Date: Wed, 7 Oct 2026 19:16:18 +0200 Subject: [PATCH 1/3] wrote importscript, tested it but it has slight flaws. will fix later --- .gitignore | 2 + package-lock.json | 672 +++++++++++++++++++++- package.json | 5 +- server/.env.example | 1 + server/src/database/migrations/schema.sql | 25 +- server/src/scripts/import.ts | 242 ++++++++ server/tsconfig.json | 15 + src/App.tsx | 2 +- 8 files changed, 949 insertions(+), 15 deletions(-) create mode 100644 server/.env.example create mode 100644 server/src/scripts/import.ts diff --git a/.gitignore b/.gitignore index 35cf3a3..d8222ca 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ server/data/covers/* # Environment variables .env + +public/covers/ diff --git a/package-lock.json b/package-lock.json index 8bf91f5..970cd77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,14 @@ "name": "t05-project-2", "version": "0.0.0", "dependencies": { + "pg": "^8.23.1", "react": "^19.2.8", "react-dom": "^19.2.8" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/node": "^24.13.3", + "@types/node": "^24.19.1", + "@types/pg": "^8.23.1", "@types/react": "^19.3.0", "@types/react-dom": "^19.2.7", "@vitejs/plugin-react": "^6.1.1", @@ -21,6 +23,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.6", "globals": "^17.12.0", + "tsx": "^4.23.15", "typescript": "~6.0.2", "typescript-eslint": "^8.69.0", "vite": "^8.3.0" @@ -290,6 +293,448 @@ "keyv": "^5.6.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -852,15 +1297,27 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.19.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz", - "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", "dev": true, "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", @@ -1370,6 +1827,48 @@ "dev": true, "license": "ISC" }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2288,6 +2787,95 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.1", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.1", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz", + "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz", + "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz", + "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2337,6 +2925,45 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2481,6 +3108,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2511,6 +3147,25 @@ "typescript": ">=4.8.4" } }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2714,6 +3369,15 @@ "node": ">=0.10.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 635251b..cbd713b 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,14 @@ "preview": "vite preview" }, "dependencies": { + "pg": "^8.23.1", "react": "^19.2.8", "react-dom": "^19.2.8" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/node": "^24.13.3", + "@types/node": "^24.19.1", + "@types/pg": "^8.23.1", "@types/react": "^19.3.0", "@types/react-dom": "^19.2.7", "@vitejs/plugin-react": "^6.1.1", @@ -23,6 +25,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.6", "globals": "^17.12.0", + "tsx": "^4.23.15", "typescript": "~6.0.2", "typescript-eslint": "^8.69.0", "vite": "^8.3.0" diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..262ff73 --- /dev/null +++ b/server/.env.example @@ -0,0 +1 @@ +DATABASE_URL=postgres://USER:PASSWORD@localhost:5432/DBNAME \ No newline at end of file diff --git a/server/src/database/migrations/schema.sql b/server/src/database/migrations/schema.sql index c266251..ed47267 100644 --- a/server/src/database/migrations/schema.sql +++ b/server/src/database/migrations/schema.sql @@ -1,35 +1,42 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE TABLE IF NOT EXISTS labels ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL +); + -- album-tabell CREATE TABLE IF NOT EXISTS albums ( id VARCHAR(64) PRIMARY KEY, -- MusicBrainz release-group MBID title VARCHAR(255) NOT NULL, artist VARCHAR(255) NOT NULL, release_year INT NOT NULL, - cover_url TEXT, + cover_path TEXT, + label_id INT REFERENCES labels(id) ); -- sjangre (mange-til-mange relasjon) CREATE TABLE IF NOT EXISTS genres ( id SERIAL PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL -); + ); CREATE TABLE IF NOT EXISTS album_genres ( album_id VARCHAR(64) REFERENCES albums(id) ON DELETE CASCADE, genre_id INT REFERENCES genres(id) ON DELETE CASCADE, PRIMARY KEY (album_id, genre_id) -); + ); --- tracks-tabell (en-til-mange: et album har mange spor) CREATE TABLE IF NOT EXISTS tracks ( id SERIAL PRIMARY KEY, album_id VARCHAR(64) NOT NULL REFERENCES albums(id) ON DELETE CASCADE, - position INT NOT NULL, + disc_number INT NOT NULL DEFAULT 1, + track_number INT NOT NULL, title VARCHAR(255) NOT NULL, - duration VARCHAR(50) NOT NULL, -- f.eks. "3:45" eller millisekunder -); + duration_ms INT, + UNIQUE (album_id, disc_number, track_number) + ); -- reviews and ratings (for UI innhold) CREATE TABLE IF NOT EXISTS reviews ( @@ -39,7 +46,7 @@ CREATE TABLE IF NOT EXISTS reviews ( rating INT NOT NULL CHECK (rating >= 1 AND rating <= 5), comment TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); + ); -- for collections/favs for brukere DO $$ BEGIN @@ -54,7 +61,7 @@ CREATE TABLE IF NOT EXISTS user_collections ( collection_type collection_type_enum NOT NULL, added_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, album_id, collection_type) -); + ); -- INDEKSER FOR HØY YTELSE (Søk, filtrering, sortering og paginering) diff --git a/server/src/scripts/import.ts b/server/src/scripts/import.ts new file mode 100644 index 0000000..971d9a4 --- /dev/null +++ b/server/src/scripts/import.ts @@ -0,0 +1,242 @@ +import { Client } from 'pg'; +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'node:url'; +import 'dotenv/config'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const COVER_DIR = path.join(__dirname, '..', '..', '..', 'public', 'covers'); + +interface TrackResponse { + media?: Array<{ + position?: number; + tracks?: Array<{ + position?: number; + length?: number; + title?: string; + }>; + }>; +} + +interface SearchResponse { + 'release-groups'?: Array<{ + id: string; + title?: string; + 'artist-credit'?: Array<{ name?: string }>; + 'primary-type'?: string; + 'secondary-types'?: string[]; + tags?: Array<{ name: string }>; + }>; +} + +interface ReleaseResponse { + releases?: Array<{ id: string }>; +} + +const client = new Client({ connectionString: process.env.DATABASE_URL }); + +if (!fs.existsSync(COVER_DIR)) { + fs.mkdirSync(COVER_DIR, { recursive: true }); +} + +// Helper for rate limiting +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const ALLOWED_GENRES = [ + 'Pop', 'Rock', 'HipHop', 'Jazz', 'Electronic', + 'Tekno', 'Indie', 'Soul', 'Kpop', 'Funk', 'Country', + 'Classic', 'Metal', 'RnB', 'Blues' +]; + +function mapToAllowedGenre(mbTags: Array<{ name: string }> | undefined): string { + if (!mbTags || mbTags.length === 0) return 'Rock'; + + const cleanedTags = mbTags.map((t) => t.name.toLowerCase().trim()); + + // 1. Exact match check + for (const tag of cleanedTags) { + for (const allowed of ALLOWED_GENRES) { + if (tag === allowed.toLowerCase()) { + return allowed; + } + } + } + + // 2. Partial match fallback + for (const tag of cleanedTags) { + for (const allowed of ALLOWED_GENRES) { + if (tag.includes(allowed.toLowerCase())) { + return allowed; + } + } + } + + return 'Rock'; +} + +async function downloadCover(mbid: string, title: string): Promise { + const imageUrl = `https://coverartarchive.org/release-group/${mbid}/front-250`; + const filePath = path.join(COVER_DIR, `${mbid}.jpg`); + + try { + const response = await fetch(imageUrl); + if (!response.ok) { + console.warn(`⚠️ Mangler cover for: "${title}" (MBID: ${mbid})`); + return null; + } + + const arrayBuffer = await response.arrayBuffer(); + fs.writeFileSync(filePath, Buffer.from(arrayBuffer)); + return `/covers/${mbid}.jpg`; + } catch (err) { + console.warn(`⚠️ Kunne ikke laste ned cover for: "${title}"`); + return null; + } +} + +async function importDecade(startYear: number) { + console.log(`\n==================================================`); + console.log(`Startar import for tiåret ${startYear}-${startYear + 9}`); + console.log(`==================================================\n`); + + for (let year = startYear; year < startYear + 10; year++) { + console.log(`\n--- Henter album for årstallet ${year} ---`); + + // Added &inc=tags so MusicBrainz includes genre tags + const query = `tag:album AND date:${year}`; + const searchUrl = `https://musicbrainz.org/ws/2/release-group?query=${encodeURIComponent(query)}&inc=tags&limit=15&fmt=json`; + + try { + const searchRes = await fetch(searchUrl, { + headers: { 'User-Agent': 'AlbumImportApp/1.0.0 ( student@skole.no )' }, + }); + + if (!searchRes.ok) { + console.error(`Kunne ikke hente søkeresultater for ${year}`); + continue; + } + + const searchData = (await searchRes.json()) as SearchResponse; + const releaseGroups = searchData['release-groups'] || []; + let importedThisYear = 0; + + for (const rg of releaseGroups) { + if (importedThisYear >= 10) break; + + const mbid = rg.id; + const title = rg.title ? rg.title.trim().slice(0, 250) : null; + const artist = rg['artist-credit']?.[0]?.name ? rg['artist-credit'][0].name.trim().slice(0, 250) : null; + const primaryType = rg['primary-type']; + const secondaryTypes = rg['secondary-types'] || []; + + if (primaryType !== 'Album' || secondaryTypes.length > 0) { + continue; + } + + if (!title || !artist) { + console.warn(`⚠️ Mangler tittel eller artist for MBID: ${mbid}. Hopper over.`); + continue; + } + + await delay(1100); + + const releaseUrl = `https://musicbrainz.org/ws/2/release?release-group=${mbid}&limit=1&fmt=json`; + const relRes = await fetch(releaseUrl, { + headers: { 'User-Agent': 'AlbumImportApp/1.0.0 ( student@skole.no )' }, + }); + + const relData = (await relRes.json()) as ReleaseResponse; + const firstRelease = relData.releases?.[0]; + + if (!firstRelease) { + console.warn(`⚠️ Fant ingen tilhørende release for "${title}". Hopper over.`); + continue; + } + + await delay(1100); + + const trackUrl = `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings&fmt=json`; + const trackRes = await fetch(trackUrl, { + headers: { 'User-Agent': 'AlbumImportApp/1.0.0 ( student@skole.no )' }, + }); + + const trackData = (await trackRes.json()) as TrackResponse; + const media = trackData.media?.[0]; + const rawTracks = media?.tracks || []; + + if (rawTracks.length === 0) { + console.warn(`⚠️ Ingen spor funnet for "${title}". Hopper over.`); + continue; + } + + const localCoverPath = await downloadCover(mbid, title); + + // Wrap database writes in a transaction + await client.query('BEGIN'); + + try { + // Insert Album + await client.query( + `INSERT INTO albums (id, title, artist, release_year, cover_path) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (id) DO NOTHING;`, + [mbid, title, artist, year, localCoverPath] + ); + + // Insert Genre + const genreName = mapToAllowedGenre(rg.tags); + const genreRes = await client.query( + `INSERT INTO genres (name) VALUES ($1) + ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name + RETURNING id;`, + [genreName] + ); + const genreId = genreRes.rows[0].id; + + await client.query( + `INSERT INTO album_genres (album_id, genre_id) VALUES ($1, $2) ON CONFLICT DO NOTHING;`, + [mbid, genreId] + ); + + // Insert Tracks + const discNum = media?.position || 1; + for (const tr of rawTracks) { + const trackTitle = tr.title ? tr.title.trim().slice(0, 250) : 'Untitled'; + const trackNum = tr.position || 1; + const durationMs = tr.length && tr.length > 0 ? tr.length : null; + + await client.query( + `INSERT INTO tracks (album_id, disc_number, track_number, title, duration_ms) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (album_id, disc_number, track_number) DO NOTHING;`, + [mbid, discNum, trackNum, trackTitle, durationMs] + ); + } + + await client.query('COMMIT'); + importedThisYear++; + console.log(`✔ [${year}] Importert: "${title}" av ${artist} [Sjanger: ${genreName}]`); + } catch (dbErr) { + await client.query('ROLLBACK'); + console.error(`Feil ved lagring i databasen for MBID ${mbid}:`, dbErr); + } + } + } catch (err) { + console.error(`Feil under prosessering av årstall ${year}:`, err); + } + } +} + +async function main() { + await client.connect(); + + const targetYear = parseInt(process.argv[2], 10) || 1980; + + await importDecade(targetYear); + + await client.end(); + console.log(`\nImport for ${targetYear}-tallet er fullført!`); +} + +main(); + diff --git a/server/tsconfig.json b/server/tsconfig.json index e69de29..885143f 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 5dcdbf4..c57c43e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import './App.css' import albumData from './mock/albums.json' -import type { Album } from './types/albums' +import type { Album } from './types/album' function App() { const albums: Album[] = albumData as Album[] From 8bd534d5f8050be8f7dc4c393b3014e6475a5258 Mon Sep 17 00:00:00 2001 From: Ellen Date: Thu, 8 Oct 2026 14:58:53 +0200 Subject: [PATCH 2/3] Finish importscript, schema.sql, typeDefs. (fixes #23) --- server/src/scripts/import.ts | 338 ++++++++++++++++++----------------- 1 file changed, 172 insertions(+), 166 deletions(-) diff --git a/server/src/scripts/import.ts b/server/src/scripts/import.ts index 971d9a4..45410ee 100644 --- a/server/src/scripts/import.ts +++ b/server/src/scripts/import.ts @@ -6,6 +6,9 @@ import 'dotenv/config'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const COVER_DIR = path.join(__dirname, '..', '..', '..', 'public', 'covers'); +const USER_AGENT = 'Cdllection/1.0.0 ( elleniaf@stud.ntnu.no )'; +//MusicBrainz trenger en epost på header, så epost må byttes hvis noe andre skal kjøre den +//tar skoleeposten for oppgaven interface TrackResponse { media?: Array<{ @@ -19,14 +22,16 @@ interface TrackResponse { } interface SearchResponse { - 'release-groups'?: Array<{ - id: string; - title?: string; - 'artist-credit'?: Array<{ name?: string }>; - 'primary-type'?: string; - 'secondary-types'?: string[]; - tags?: Array<{ name: string }>; - }>; + count?: number; + 'release-groups'?: Array<{ + id: string; + title?: string; + 'artist-credit'?: Array<{ name?: string }>; + 'first-release-date'?: string; + 'primary-type'?: string; + 'secondary-types'?: string[]; + tags?: Array<{ name: string; count?: number }>; + }>; } interface ReleaseResponse { @@ -42,36 +47,169 @@ if (!fs.existsSync(COVER_DIR)) { // Helper for rate limiting const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -const ALLOWED_GENRES = [ - 'Pop', 'Rock', 'HipHop', 'Jazz', 'Electronic', - 'Tekno', 'Indie', 'Soul', 'Kpop', 'Funk', 'Country', - 'Classic', 'Metal', 'RnB', 'Blues' -]; -function mapToAllowedGenre(mbTags: Array<{ name: string }> | undefined): string { - if (!mbTags || mbTags.length === 0) return 'Rock'; - - const cleanedTags = mbTags.map((t) => t.name.toLowerCase().trim()); +// Genre name in our app -> tag to search for in MusicBrainz +const GENRE_SEARCH_TAGS: Record = { + Pop: 'pop', + Rock: 'rock', + HipHop: 'hip hop', + Jazz: 'jazz', + Electronic: 'electronic', + Indie: 'indie', + Soul: 'soul', + Funk: 'funk', + Country: 'country', + Metal: 'metal', + Blues: 'blues', + Tekno: 'techno', + Kpop: 'k-pop', + Classic: 'classical', + RnB: 'rnb', +}; + +const QUOTA_PER_GENRE = parseInt(process.argv[3], 10) || 10; + +async function mbFetch(url: string): Promise { + for (let attempt = 1; attempt <= 4; attempt++) { + await delay(1100); // MusicBrainz: maks 1 kall per sekund + try { + const res = await fetch(url, { headers: { 'User-Agent': USER_AGENT } }); + if (res.ok) return (await res.json()) as T; - // 1. Exact match check - for (const tag of cleanedTags) { - for (const allowed of ALLOWED_GENRES) { - if (tag === allowed.toLowerCase()) { - return allowed; + if (res.status === 503 || res.status === 429) { + console.warn(`${res.status}, venter og prøver igjen (${attempt}/4)`); + await delay(5000 * attempt); // lengre pause for hvert forsøk + continue; } + + console.warn(`MusicBrainz svarte ${res.status} for ${url}`); + return null; + } catch (err) { + console.warn(`Nettverksfeil (${attempt}/4)`); + await delay(5000 * attempt); } } + return null; +} - // 2. Partial match fallback - for (const tag of cleanedTags) { - for (const allowed of ALLOWED_GENRES) { - if (tag.includes(allowed.toLowerCase())) { - return allowed; - } +const PAGE_SIZE = 100; // MusicBrainz maximum per request + +/** Fetches and saves one album with its tracks. Returns true if it was imported. */ +async function importAlbum( + rg: NonNullable[number], + genreName: string +): Promise { + const mbid = rg.id; + const title = rg.title?.trim().slice(0, 250); + const artist = rg['artist-credit']?.[0]?.name?.trim().slice(0, 250); + const year = parseInt(rg['first-release-date']?.slice(0, 4) ?? '', 10); + if (!title || !artist || Number.isNaN(year)) return false; + + // Already imported (e.g. found under another genre): skip without API calls + const exists = await client.query('SELECT 1 FROM albums WHERE id = $1', [mbid]); + if (exists.rowCount && exists.rowCount > 0) return false; + + const relData = await mbFetch( + `https://musicbrainz.org/ws/2/release?release-group=${mbid}&limit=1&fmt=json` + ); + const firstRelease = relData?.releases?.[0]; + if (!firstRelease) return false; + + const trackData = await mbFetch( + `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings&fmt=json` + ); + const media = trackData?.media?.[0]; + const rawTracks = media?.tracks || []; + if (rawTracks.length === 0) return false; + + const coverPath = await downloadCover(mbid, title); // null if missing + + // Album, genre link and tracks are saved together or not at all + await client.query('BEGIN'); + try { + await client.query( + `INSERT INTO albums (id, title, artist, release_year, cover_path) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO NOTHING;`, + [mbid, title, artist, year, coverPath] + ); + const genreRes = await client.query( + `INSERT INTO genres (name) VALUES ($1) + ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id;`, + [genreName] + ); + await client.query( + `INSERT INTO album_genres (album_id, genre_id) VALUES ($1, $2) ON CONFLICT DO NOTHING;`, + [mbid, genreRes.rows[0].id] + ); + const discNum = media?.position || 1; + for (const tr of rawTracks) { + await client.query( + `INSERT INTO tracks (album_id, disc_number, track_number, title, duration_ms) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (album_id, disc_number, track_number) DO NOTHING;`, + [ + mbid, + discNum, + tr.position || 1, + tr.title?.trim().slice(0, 250) || 'Untitled', + tr.length && tr.length > 0 ? tr.length : null, + ] + ); } + await client.query('COMMIT'); + console.log(`✔ [${year}] ${title} av ${artist} [${genreName}]`); + return true; + } catch (dbErr) { + await client.query('ROLLBACK'); + console.error(`Feil ved lagring for MBID ${mbid}:`, dbErr); + return false; } +} + +async function importDecade(startYear: number) { + const endYear = startYear + 9; + console.log(`\n=== Import for ${startYear}-${endYear} (kvote ${QUOTA_PER_GENRE}/sjanger) ===`); + + for (const [genreName, tag] of Object.entries(GENRE_SEARCH_TAGS)) { + console.log(`\n--- ${genreName} (${startYear}-${endYear}) ---`); - return 'Rock'; + const query = + `primarytype:album AND NOT secondarytype:* AND tag:"${tag}" ` + + `AND firstreleasedate:[${startYear} TO ${endYear}]`; + + let imported = 0; + let offset = 0; + let total = Infinity; // updated after the first response + + while (imported < QUOTA_PER_GENRE && offset < total) { + const url = + `https://musicbrainz.org/ws/2/release-group?query=${encodeURIComponent(query)}` + + `&limit=${PAGE_SIZE}&offset=${offset}&fmt=json`; + + const page = await mbFetch(url); + if (!page) { + console.warn(`⚠️ Søket feilet for ${genreName} ved offset ${offset}. Går videre til neste sjanger.`); + break; + } + + total = page.count ?? 0; + const groups = page['release-groups'] || []; + if (groups.length === 0) break; + + for (const rg of groups) { + if (imported >= QUOTA_PER_GENRE) break; + try { + if (await importAlbum(rg, genreName)) imported++; + } catch (err) { + console.error(`Feil for album ${rg.id}, hopper over:`, err); + } + } + + offset += PAGE_SIZE; + } + + console.log(`${genreName}: ${imported}/${QUOTA_PER_GENRE} importert (${total === Infinity ? 0 : total} treff totalt)`); + } } async function downloadCover(mbid: string, title: string): Promise { @@ -94,149 +232,17 @@ async function downloadCover(mbid: string, title: string): Promise= 10) break; - - const mbid = rg.id; - const title = rg.title ? rg.title.trim().slice(0, 250) : null; - const artist = rg['artist-credit']?.[0]?.name ? rg['artist-credit'][0].name.trim().slice(0, 250) : null; - const primaryType = rg['primary-type']; - const secondaryTypes = rg['secondary-types'] || []; - - if (primaryType !== 'Album' || secondaryTypes.length > 0) { - continue; - } - - if (!title || !artist) { - console.warn(`⚠️ Mangler tittel eller artist for MBID: ${mbid}. Hopper over.`); - continue; - } - - await delay(1100); - - const releaseUrl = `https://musicbrainz.org/ws/2/release?release-group=${mbid}&limit=1&fmt=json`; - const relRes = await fetch(releaseUrl, { - headers: { 'User-Agent': 'AlbumImportApp/1.0.0 ( student@skole.no )' }, - }); - - const relData = (await relRes.json()) as ReleaseResponse; - const firstRelease = relData.releases?.[0]; - - if (!firstRelease) { - console.warn(`⚠️ Fant ingen tilhørende release for "${title}". Hopper over.`); - continue; - } - - await delay(1100); - - const trackUrl = `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings&fmt=json`; - const trackRes = await fetch(trackUrl, { - headers: { 'User-Agent': 'AlbumImportApp/1.0.0 ( student@skole.no )' }, - }); - - const trackData = (await trackRes.json()) as TrackResponse; - const media = trackData.media?.[0]; - const rawTracks = media?.tracks || []; - - if (rawTracks.length === 0) { - console.warn(`⚠️ Ingen spor funnet for "${title}". Hopper over.`); - continue; - } - - const localCoverPath = await downloadCover(mbid, title); - - // Wrap database writes in a transaction - await client.query('BEGIN'); - - try { - // Insert Album - await client.query( - `INSERT INTO albums (id, title, artist, release_year, cover_path) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (id) DO NOTHING;`, - [mbid, title, artist, year, localCoverPath] - ); - - // Insert Genre - const genreName = mapToAllowedGenre(rg.tags); - const genreRes = await client.query( - `INSERT INTO genres (name) VALUES ($1) - ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name - RETURNING id;`, - [genreName] - ); - const genreId = genreRes.rows[0].id; - - await client.query( - `INSERT INTO album_genres (album_id, genre_id) VALUES ($1, $2) ON CONFLICT DO NOTHING;`, - [mbid, genreId] - ); - - // Insert Tracks - const discNum = media?.position || 1; - for (const tr of rawTracks) { - const trackTitle = tr.title ? tr.title.trim().slice(0, 250) : 'Untitled'; - const trackNum = tr.position || 1; - const durationMs = tr.length && tr.length > 0 ? tr.length : null; - - await client.query( - `INSERT INTO tracks (album_id, disc_number, track_number, title, duration_ms) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (album_id, disc_number, track_number) DO NOTHING;`, - [mbid, discNum, trackNum, trackTitle, durationMs] - ); - } - - await client.query('COMMIT'); - importedThisYear++; - console.log(`✔ [${year}] Importert: "${title}" av ${artist} [Sjanger: ${genreName}]`); - } catch (dbErr) { - await client.query('ROLLBACK'); - console.error(`Feil ved lagring i databasen for MBID ${mbid}:`, dbErr); - } - } - } catch (err) { - console.error(`Feil under prosessering av årstall ${year}:`, err); - } +async function main() { + const targetYear = parseInt(process.argv[2], 10); + if (Number.isNaN(targetYear)) { + console.error('Bruk: npx tsx src/scripts/import.ts , f.eks. 1990 40'); + process.exit(1); } -} -async function main() { await client.connect(); - - const targetYear = parseInt(process.argv[2], 10) || 1980; - await importDecade(targetYear); - await client.end(); console.log(`\nImport for ${targetYear}-tallet er fullført!`); } main(); - From f10671530f1310644a9402fe5e697d7bfa49b324 Mon Sep 17 00:00:00 2001 From: Ellen Date: Thu, 8 Oct 2026 15:15:48 +0200 Subject: [PATCH 3/3] chore: update root dependencies --- package-lock.json | 16 ++++++++++++++++ package.json | 1 + 2 files changed, 17 insertions(+) diff --git a/package-lock.json b/package-lock.json index 970cd77..cf4b97d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "t05-project-2", "version": "0.0.0", "dependencies": { + "dotenv": "^18.0.6", "pg": "^8.23.1", "react": "^19.2.8", "react-dom": "^19.2.8" @@ -1820,6 +1821,21 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.6.tgz", + "integrity": "sha512-ZMVmohV6CvAJtj++pZNyggluUt6hbRdQAxc2+Z635RL6/AG/ruXv5lHnybUFoxMPL/yrTL/tsF4paZu2l7RA6Q==", + "license": "BSD-2-Clause", + "bin": { + "dotenv": "dist/index.cjs" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.443", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.443.tgz", diff --git a/package.json b/package.json index cbd713b..7f52c08 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "dotenv": "^18.0.6", "pg": "^8.23.1", "react": "^19.2.8", "react-dom": "^19.2.8"