From aef705834bc6e37beabe8541a198ed8f9021bf31 Mon Sep 17 00:00:00 2001 From: valere Date: Sat, 21 Feb 2026 09:07:45 +0100 Subject: [PATCH] WIP blurash & sync card ? --- app/components/Card.vue | 1 + data/music.db | Bin 20480 -> 24576 bytes nuxt.config.ts | 2 +- package.json | 2 + pnpm-lock.yaml | 279 ++++++++++++++++++++++++++++ server/db/schema.ts | 1 + server/services/cardSync.service.ts | 46 +++-- server/utils/blurHash.ts | 114 ++++++++++++ 8 files changed, 425 insertions(+), 20 deletions(-) create mode 100644 server/utils/blurHash.ts diff --git a/app/components/Card.vue b/app/components/Card.vue index cb3cfc4..5b18389 100644 --- a/app/components/Card.vue +++ b/app/components/Card.vue @@ -115,6 +115,7 @@ const isTrackLoaded = ref(false) .face-up { border-radius: 1rem; + border: none; transform: rotateY(0deg); transition: box-shadow 0.6s; diff --git a/data/music.db b/data/music.db index 93d4991014ef4319a7cf487d20329eb956dcd659..ad0cafab7ec722130c8102b10b988380b9a06da2 100644 GIT binary patch delta 2644 zcmeI!?@t?b90%~0@pD^&uCT&b2c%q(AJ=_5C@sULKklxy^sbZ^dO)|bgS8wj>$T&$ zUfJk4h*2M?V>4>vlR1sXC&oD+824m9VvO;@7R?e9W8z#kllj0UCVSvw;(ZR{6Qh5@ z+N953{d~Tk>-+oZ`ep6<4ef_4*#O$Iw7s%J2KbLEF4!Z~`HzAQccw^kWR`MW~#o5XbW$dDoXN z{y4VLu@`Ww+zQy#=LCG-@SB64BGK|ZSBklX)Le)qxRQ{PW?4cU5|aD}i*6Nb_5iMv z>j1ZzaKJA$ee0kHZEknD&`+BS3~ins%o75GP&k_7rq1#2zZq6>*KWXVa+`_|y8vgJ ze|OM6U)XDR%_9^!g=fVf${O+`6qqWrk!Ai$%jNCLb^oqh*soOfFdQ-KDrS5LQAe;; zAZM{7t2B!rl{kL$-6w$MTrFU&tcA)369$#z#_JAxG(1YN|}OM&Lf16NQJ=E_PFs8j3F_8^ZZ>^ z`>R3YG7cI)JOMh|YEZPze}p2O%1O)l1nw-7L&{@26LVuhX_n9Uw&y)r1$sapP_>N( zEgxuCw1b#gjfq)gjrLlX#mU6P;vfo1g>p>kJk#5u*6ykVy)$rgw>t3aZOXtYVm!CD zHZyp1_)LH;o=l8Ovy?S@L@*1;2E%&|_#iayS zvUujlLDQyh`5`VKa+pE^LjhBD8jf?31#Y zDH3z8g_v=ZVO{OJMGLxFZU)`cHVmbk+aGk%1D?rD!aIb9)|`lN4Le0WX|Lu! zMCf%TLQQqA)^^rZRJ~E@-^En)YVL34Z#HfCS|OzWTx<+P{1sBKM0|!Ooq>=z7wSsm zBP6zTHnnVtV%97cE%T2)ysSpNqXqQN7Q)Qx7W$l0wvc6Fxj!e`g#c`!be#1HshFGf zvfgx@f3#=&I+u)qtK=%UAB?6RxF5ds-FDC%x!_uu&W31V5yP^5X-XKwUE+XfQ!d3{ zj^9u#lluYp$$fw^+c@<7&S-;a7vNzk|aBZ~gN>-=8Fr>Z{K1llMYLHD(dK;tcAzdGmzkL9!|q{8|6q8Ld@SxX#hMTj{xrR%o+65wF5zVE3i@4s7g^>%9=i(|Vo;&GqN#sRcjcPC<1Jm2e69_?u12nbleC)p;3Ox#UpweU z-zYby{ADr=tF9vJVTtr{EX0+Vv#ghY@~4`rjbjG@+hsdon>7xPz(;gzyXc^mN~ANc d+453hIkp&1o?4n)@MWgczNCeJT)z&@{0<6k`AY delta 1520 zcmeH{-%C?b9Kd(?>+bj6yw2-7T{l@e*BUBg3|ZwiTQ1G~v4Qgc4O74bhO(;c)mHj*CKWq0P+^<)OCj?6v71bi#&$ zlR8nT^frjX+|OehM0wEi26@(OMJ`jBCGf_P+rJTIIgw#pV?pksF646H0PsB}Ux;#d zD8}-iF(WrpBXS}Kfj5h|3!-fQVo9=%yO2j{1bMi=8z%BeuMb8(Z(|dg^}A5`b7|T@ z_#ShBu(>qP?u4csh2pbN<;7>=U;>_nO>^Pec1y^0REHHL*$;f6X`Lt^d)8RIuMByB z29Q^U+o8azLjn|dH(SSqv=fCmjiV538U$f68AZYK7zH|6ih@KX6apj)70Q}F6Xk7k zlqE=s$fGoh+#l}~9l3C-4idb)Im3kE5)|M^K^h!`3b&7>L0DJWnc#DvP+X%gglnv6 f#Wk*sdzc{FQK+Pqn4#XCge8m|-7zRD5?kVLvi!I7 diff --git a/nuxt.config.ts b/nuxt.config.ts index 89747b0..a996d0a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -13,7 +13,7 @@ export default defineNuxtConfig({ tasks: true }, scheduledTasks: { - '*/5 * * * *': ['syncTracks'] + '*/1 * * * *': ['sync-tracks'] } }, compatibilityDate: '2025-07-15', diff --git a/package.json b/package.json index 87f572c..950d881 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,11 @@ "@nuxtjs/tailwindcss": "6.14.0", "@pinia/nuxt": "0.11.2", "atropos": "^2.0.2", + "blurhash": "^2.0.5", "drizzle-orm": "^0.45.1", "nuxt": "^4.3.0", "pinia": "^3.0.3", + "sharp": "^0.34.5", "vue": "^3.5.18", "vue-router": "^4.5.1", "vuedraggable": "^4.1.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff7977a..a7bc66e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: atropos: specifier: ^2.0.2 version: 2.0.2 + blurhash: + specifier: ^2.0.5 + version: 2.0.5 drizzle-orm: specifier: ^0.45.1 version: 0.45.1(@libsql/client@0.17.0) @@ -32,6 +35,9 @@ importers: pinia: specifier: ^3.0.3 version: 3.0.4(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)) + sharp: + specifier: ^0.34.5 + version: 0.34.5 vue: specifier: ^3.5.18 version: 3.5.27(typescript@5.9.3) @@ -845,6 +851,143 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@ioredis/commands@1.5.0': resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} @@ -2339,6 +2482,9 @@ packages: birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + blurhash@2.0.5: + resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -5270,6 +5416,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -6706,6 +6856,102 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@ioredis/commands@1.5.0': {} '@isaacs/balanced-match@4.0.1': {} @@ -8431,6 +8677,8 @@ snapshots: birpc@2.9.0: {} + blurhash@2.0.5: {} + boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -11798,6 +12046,37 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/server/db/schema.ts b/server/db/schema.ts index 5dd7be6..63ff085 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -14,6 +14,7 @@ export const cards = sqliteTable('cards', { slug: text('slug').notNull(), suit: text('suit').notNull(), rank: text('rank').notNull(), + blurhash: text('blurhash').notNull(), // blurhash of the image createdAt: int('created_at', { mode: 'timestamp' }) .notNull() .$defaultFn(() => new Date()), diff --git a/server/services/cardSync.service.ts b/server/services/cardSync.service.ts index b877368..8fd390f 100644 --- a/server/services/cardSync.service.ts +++ b/server/services/cardSync.service.ts @@ -1,6 +1,7 @@ import { eq, notInArray } from 'drizzle-orm' import { useDB, schema } from '../db' import { scanMusicFolder } from '../utils/fileScanner' +import { generateBlurhash } from '../utils/blurHash' const { cards } = schema @@ -21,27 +22,34 @@ export async function syncCardsWithDatabase(folderPath: string) { const scannedEsids = new Set(scannedCards.map((t) => t.esid)) const cardsToDelete = existingCards.filter((t) => !scannedEsids.has(t.esid)) - // 4. Insérer les nouvelles cards + // 4. Insérer les nouvelles cartes if (cardsToInsert.length > 0) { - // Dans la fonction syncCardsWithDatabase - await db.insert(cards).values( - cardsToInsert.map((card) => ({ - url_audio: card.url_audio, - url_image: card.url_image, - year: card.year, - month: card.month, - day: card.day, - hour: card.hour, - artist: card.artist, - title: card.title, - esid: card.esid, - slug: card.slug, - createdAt: card.createdAt, - suit: card.suit, - rank: card.rank - })) + // Générer tous les blurhash en parallèle + const cardsWithBlurhash = await Promise.all( + cardsToInsert.map(async (card) => { + const blurhash = await generateBlurhash(card.url_image) + return { + url_audio: card.url_audio, + url_image: card.url_image, + year: card.year, + month: card.month, + day: card.day, + hour: card.hour, + artist: card.artist, + title: card.title, + esid: card.esid, + slug: card.slug, + createdAt: card.createdAt, + suit: card.suit, + rank: card.rank, + blurhash: blurhash + } + }) ) - console.log(`✅ ${cardsToInsert.length} cards ajoutées`) + + // Insérer les cartes avec les blurhash déjà résolus + await db.insert(cards).values(cardsWithBlurhash) + console.log(`✅ ${cardsToInsert.length} cartes ajoutées`) } // 5. Supprimer les cards obsolètes avec une requête distincte pour chaque esid diff --git a/server/utils/blurHash.ts b/server/utils/blurHash.ts new file mode 100644 index 0000000..1c19e85 --- /dev/null +++ b/server/utils/blurHash.ts @@ -0,0 +1,114 @@ +// server/utils/blurHash.ts +import sharp from 'sharp' +import { encode } from 'blurhash' +import { $fetch } from 'ofetch' +import { mkdir, writeFile, unlink } from 'node:fs/promises' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { randomUUID } from 'node:crypto' + +// Dossier temporaire pour les images téléchargées +const TMP_DIR = join(tmpdir(), 'evilspins-images') + +async function ensureTmpDir() { + try { + await mkdir(TMP_DIR, { recursive: true }) + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error + } +} + +async function downloadImage(url: string): Promise { + await ensureTmpDir() + const tmpPath = join(TMP_DIR, `${randomUUID()}.jpg`) + + try { + const response = await $fetch(url, { + responseType: 'arrayBuffer', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + Accept: 'image/*,*/*;q=0.8' + }, + // Timeout plus long + timeout: 60000, // 60 secondes + // Désactiver la compression pour les images + headers: { + 'Accept-Encoding': 'identity' + } + }) + + await writeFile(tmpPath, Buffer.from(response)) + return tmpPath + } catch (error) { + // Nettoyer en cas d'erreur + try { + await unlink(tmpPath).catch(() => {}) + } catch { + /* Ignorer les erreurs de suppression */ + } + console.error(`Failed to download image from ${url}:`, error.message) + throw error + } +} + +function createDefaultBlurhash() { + // Un blurhash plus discret + return 'L5H2EC=H00~q^-=wD6xuxvV@%KxZ' +} + +export async function generateBlurhash( + input: Buffer | string, + componentX: number = 4, + componentY: number = 3 +): Promise { + let tmpPath: string | null = null + + try { + if (typeof input === 'string') { + if (input.startsWith('http')) { + try { + tmpPath = await downloadImage(input) + input = tmpPath + } catch (error) { + console.warn(`Using default blurhash for ${input} due to download error`) + return createDefaultBlurhash() + } + } + + // Vérifier si le fichier existe + try { + const image = sharp(input).resize(32, 32, { fit: 'inside' }).ensureAlpha() + + const { data, info } = await image.raw().toBuffer({ resolveWithObject: true }) + return encode(new Uint8ClampedArray(data), info.width, info.height, componentX, componentY) + } catch (error) { + console.warn(`Error processing image ${input}:`, error.message) + return createDefaultBlurhash() + } + } else { + // Si c'est déjà un Buffer + try { + const image = sharp(input).resize(32, 32, { fit: 'inside' }).ensureAlpha() + + const { data, info } = await image.raw().toBuffer({ resolveWithObject: true }) + return encode(new Uint8ClampedArray(data), info.width, info.height, componentX, componentY) + } catch (error) { + console.warn('Error processing image buffer:', error.message) + return createDefaultBlurhash() + } + } + } catch (error) { + console.error('Unexpected error in generateBlurhash:', error.message) + return createDefaultBlurhash() + } finally { + // Nettoyer le fichier temporaire s'il existe + if (tmpPath) { + try { + await unlink(tmpPath).catch(() => {}) + } catch { + /* Ignorer les erreurs de suppression */ + } + } + } +}