From 0de20987558db456e5092c981c7b9ab79051bb24 Mon Sep 17 00:00:00 2001 From: Nexmoe <16796652+nexmoe@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:22:02 +0800 Subject: [PATCH] feat(db): move download history schema into migrate + drizzle config --- drizzle.config.ts | 7 + package.json | 4 +- pnpm-lock.yaml | 291 ++++++++++++ resources/drizzle/0000_swift_aaron_stack.sql | 66 +++ resources/drizzle/meta/0000_snapshot.json | 456 +++++++++++++++++++ resources/drizzle/meta/_journal.json | 13 + src/main/lib/database-path.ts | 6 + src/main/lib/database/migrate.ts | 97 ++++ src/main/lib/database/schema.ts | 83 ++++ src/main/lib/history-manager.ts | 43 +- src/main/lib/subscription-manager.ts | 114 +---- 11 files changed, 1042 insertions(+), 138 deletions(-) create mode 100644 drizzle.config.ts create mode 100644 resources/drizzle/0000_swift_aaron_stack.sql create mode 100644 resources/drizzle/meta/0000_snapshot.json create mode 100644 resources/drizzle/meta/_journal.json create mode 100644 src/main/lib/database-path.ts create mode 100644 src/main/lib/database/migrate.ts create mode 100644 src/main/lib/database/schema.ts diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..1c47d74 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'drizzle-kit' + +export default defineConfig({ + schema: './src/main/lib/database/schema.ts', + out: './resources/drizzle', + dialect: 'sqlite' +}) diff --git a/package.json b/package.json index 0229a60..c07ea11 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "build:win": "node scripts/check-ytdlp.js win && pnpm run build && electron-builder --win", "build:mac": "node scripts/check-ytdlp.js mac && pnpm run build && electron-builder --mac --x64 --arm64", "build:linux": "node scripts/check-ytdlp.js linux && pnpm run build && electron-builder --linux", - "release": "git checkout main && git pull && pnpm run check && bumpp" + "release": "git checkout main && git pull && pnpm run check && bumpp", + "db:generate": "drizzle-kit generate" }, "dependencies": { "@electron-toolkit/preload": "^3.0.2", @@ -77,6 +78,7 @@ "@types/react-dom": "^19.1.9", "@vitejs/plugin-react": "^5.0.3", "bumpp": "^10.3.1", + "drizzle-kit": "^0.31.7", "electron": "^38.1.2", "electron-builder": "^25.1.8", "electron-vite": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7f9c85..1682c00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -168,6 +168,9 @@ importers: bumpp: specifier: ^10.3.1 version: 10.3.1 + drizzle-kit: + specifier: ^0.31.7 + version: 0.31.7 electron: specifier: ^38.1.2 version: 38.3.0 @@ -354,6 +357,9 @@ packages: resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} engines: {node: '>= 8.9.0'} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@electron-toolkit/preload@3.0.2': resolution: {integrity: sha512-TWWPToXd8qPRfSXwzf5KVhpXMfONaUuRAZJHsKthKgZR/+LqX1dZVSSClQ8OTAEduvLGdecljCsoT2jSshfoUg==} peerDependencies: @@ -396,102 +402,206 @@ packages: resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==} engines: {node: '>=16.4'} + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + '@esbuild/aix-ppc64@0.25.11': resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.11': resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.11': resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.11': resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.11': resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.11': resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.11': resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.11': resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.11': resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.11': resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.11': resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.11': resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.11': resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.11': resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.11': resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.11': resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.11': resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} engines: {node: '>=18'} @@ -504,6 +614,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.11': resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} engines: {node: '>=18'} @@ -516,6 +632,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.11': resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} engines: {node: '>=18'} @@ -528,24 +650,48 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.11': resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.11': resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.11': resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.11': resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} engines: {node: '>=18'} @@ -1934,6 +2080,10 @@ packages: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} + drizzle-kit@0.31.7: + resolution: {integrity: sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A==} + hasBin: true + drizzle-orm@0.44.7: resolution: {integrity: sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==} peerDependencies: @@ -2141,6 +2291,16 @@ packages: es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.11: resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} engines: {node: '>=18'} @@ -2274,6 +2434,9 @@ packages: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + giget@2.0.0: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true @@ -3104,6 +3267,9 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} @@ -3740,6 +3906,8 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) + '@drizzle-team/brocli@0.10.2': {} + '@electron-toolkit/preload@3.0.2(electron@38.3.0)': dependencies: electron: 38.3.0 @@ -3823,81 +3991,157 @@ snapshots: transitivePeerDependencies: - supports-color + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.0 + '@esbuild/aix-ppc64@0.25.11': optional: true + '@esbuild/android-arm64@0.18.20': + optional: true + '@esbuild/android-arm64@0.25.11': optional: true + '@esbuild/android-arm@0.18.20': + optional: true + '@esbuild/android-arm@0.25.11': optional: true + '@esbuild/android-x64@0.18.20': + optional: true + '@esbuild/android-x64@0.25.11': optional: true + '@esbuild/darwin-arm64@0.18.20': + optional: true + '@esbuild/darwin-arm64@0.25.11': optional: true + '@esbuild/darwin-x64@0.18.20': + optional: true + '@esbuild/darwin-x64@0.25.11': optional: true + '@esbuild/freebsd-arm64@0.18.20': + optional: true + '@esbuild/freebsd-arm64@0.25.11': optional: true + '@esbuild/freebsd-x64@0.18.20': + optional: true + '@esbuild/freebsd-x64@0.25.11': optional: true + '@esbuild/linux-arm64@0.18.20': + optional: true + '@esbuild/linux-arm64@0.25.11': optional: true + '@esbuild/linux-arm@0.18.20': + optional: true + '@esbuild/linux-arm@0.25.11': optional: true + '@esbuild/linux-ia32@0.18.20': + optional: true + '@esbuild/linux-ia32@0.25.11': optional: true + '@esbuild/linux-loong64@0.18.20': + optional: true + '@esbuild/linux-loong64@0.25.11': optional: true + '@esbuild/linux-mips64el@0.18.20': + optional: true + '@esbuild/linux-mips64el@0.25.11': optional: true + '@esbuild/linux-ppc64@0.18.20': + optional: true + '@esbuild/linux-ppc64@0.25.11': optional: true + '@esbuild/linux-riscv64@0.18.20': + optional: true + '@esbuild/linux-riscv64@0.25.11': optional: true + '@esbuild/linux-s390x@0.18.20': + optional: true + '@esbuild/linux-s390x@0.25.11': optional: true + '@esbuild/linux-x64@0.18.20': + optional: true + '@esbuild/linux-x64@0.25.11': optional: true '@esbuild/netbsd-arm64@0.25.11': optional: true + '@esbuild/netbsd-x64@0.18.20': + optional: true + '@esbuild/netbsd-x64@0.25.11': optional: true '@esbuild/openbsd-arm64@0.25.11': optional: true + '@esbuild/openbsd-x64@0.18.20': + optional: true + '@esbuild/openbsd-x64@0.25.11': optional: true '@esbuild/openharmony-arm64@0.25.11': optional: true + '@esbuild/sunos-x64@0.18.20': + optional: true + '@esbuild/sunos-x64@0.25.11': optional: true + '@esbuild/win32-arm64@0.18.20': + optional: true + '@esbuild/win32-arm64@0.25.11': optional: true + '@esbuild/win32-ia32@0.18.20': + optional: true + '@esbuild/win32-ia32@0.25.11': optional: true + '@esbuild/win32-x64@0.18.20': + optional: true + '@esbuild/win32-x64@0.25.11': optional: true @@ -5373,6 +5617,15 @@ snapshots: dotenv@17.2.3: {} + drizzle-kit@0.31.7: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.11 + esbuild-register: 3.6.0(esbuild@0.25.11) + transitivePeerDependencies: + - supports-color + drizzle-orm@0.44.7(better-sqlite3@12.4.1): optionalDependencies: better-sqlite3: 12.4.1 @@ -5525,6 +5778,38 @@ snapshots: es6-error@4.1.1: optional: true + esbuild-register@3.6.0(esbuild@0.25.11): + dependencies: + debug: 4.4.3 + esbuild: 0.25.11 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + esbuild@0.25.11: optionalDependencies: '@esbuild/aix-ppc64': 0.25.11 @@ -5690,6 +5975,10 @@ snapshots: dependencies: pump: 3.0.3 + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + giget@2.0.0: dependencies: citty: 0.1.6 @@ -6492,6 +6781,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + responselike@2.0.1: dependencies: lowercase-keys: 2.0.0 diff --git a/resources/drizzle/0000_swift_aaron_stack.sql b/resources/drizzle/0000_swift_aaron_stack.sql new file mode 100644 index 0000000..a635795 --- /dev/null +++ b/resources/drizzle/0000_swift_aaron_stack.sql @@ -0,0 +1,66 @@ +CREATE TABLE `download_history` ( + `id` text PRIMARY KEY NOT NULL, + `url` text NOT NULL, + `title` text NOT NULL, + `thumbnail` text, + `type` text NOT NULL, + `status` text NOT NULL, + `download_path` text, + `saved_file_name` text, + `file_size` integer, + `duration` integer, + `downloaded_at` integer NOT NULL, + `completed_at` integer, + `sort_key` integer NOT NULL, + `error` text, + `description` text, + `channel` text, + `uploader` text, + `view_count` integer, + `tags` text, + `origin` text, + `subscription_id` text, + `selected_format` text, + `playlist_id` text, + `playlist_title` text, + `playlist_index` integer, + `playlist_size` integer +); +--> statement-breakpoint +CREATE TABLE `subscription_items` ( + `subscription_id` text NOT NULL, + `item_id` text NOT NULL, + `title` text NOT NULL, + `url` text NOT NULL, + `published_at` integer NOT NULL, + `thumbnail` text, + `added` integer NOT NULL, + `download_id` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + PRIMARY KEY(`subscription_id`, `item_id`) +); +--> statement-breakpoint +CREATE INDEX `subscription_items_subscription_idx` ON `subscription_items` (`subscription_id`);--> statement-breakpoint +CREATE TABLE `subscriptions` ( + `id` text PRIMARY KEY NOT NULL, + `title` text NOT NULL, + `source_url` text NOT NULL, + `feed_url` text NOT NULL, + `platform` text NOT NULL, + `keywords` text NOT NULL, + `tags` text NOT NULL, + `only_latest` integer NOT NULL, + `enabled` integer NOT NULL, + `cover_url` text, + `latest_video_title` text, + `latest_video_published_at` integer, + `last_checked_at` integer, + `last_success_at` integer, + `status` text NOT NULL, + `last_error` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + `download_directory` text, + `naming_template` text +); diff --git a/resources/drizzle/meta/0000_snapshot.json b/resources/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..be2caeb --- /dev/null +++ b/resources/drizzle/meta/0000_snapshot.json @@ -0,0 +1,456 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "91501763-7554-435d-91d8-382c52ee65e4", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "download_history": { + "name": "download_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "download_path": { + "name": "download_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "saved_file_name": { + "name": "saved_file_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "downloaded_at": { + "name": "downloaded_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_key": { + "name": "sort_key", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uploader": { + "name": "uploader", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view_count": { + "name": "view_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selected_format": { + "name": "selected_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "playlist_id": { + "name": "playlist_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "playlist_title": { + "name": "playlist_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "playlist_index": { + "name": "playlist_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "playlist_size": { + "name": "playlist_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "subscription_items": { + "name": "subscription_items", + "columns": { + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "added": { + "name": "added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "download_id": { + "name": "download_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "subscription_items_subscription_idx": { + "name": "subscription_items_subscription_idx", + "columns": [ + "subscription_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "subscription_items_pk": { + "columns": [ + "subscription_id", + "item_id" + ], + "name": "subscription_items_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "subscriptions": { + "name": "subscriptions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feed_url": { + "name": "feed_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keywords": { + "name": "keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "only_latest": { + "name": "only_latest", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_video_title": { + "name": "latest_video_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_video_published_at": { + "name": "latest_video_published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "download_directory": { + "name": "download_directory", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "naming_template": { + "name": "naming_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/resources/drizzle/meta/_journal.json b/resources/drizzle/meta/_journal.json new file mode 100644 index 0000000..7073213 --- /dev/null +++ b/resources/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1763176841336, + "tag": "0000_swift_aaron_stack", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/main/lib/database-path.ts b/src/main/lib/database-path.ts new file mode 100644 index 0000000..b5a782a --- /dev/null +++ b/src/main/lib/database-path.ts @@ -0,0 +1,6 @@ +import { join } from 'node:path' +import { app } from 'electron' + +export const getDatabaseFilePath = (): string => { + return join(app.getPath('userData'), 'vidbee.db') +} diff --git a/src/main/lib/database/migrate.ts b/src/main/lib/database/migrate.ts new file mode 100644 index 0000000..aaa44c0 --- /dev/null +++ b/src/main/lib/database/migrate.ts @@ -0,0 +1,97 @@ +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { sql } from 'drizzle-orm' +import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3' +import { migrate } from 'drizzle-orm/better-sqlite3/migrator' +import { type MigrationMeta, readMigrationFiles } from 'drizzle-orm/migrator' +import { app } from 'electron' +import log from 'electron-log/main' + +const MIGRATIONS_RELATIVE_PATH = 'resources/drizzle' +const MIGRATIONS_TABLE = '__drizzle_migrations' +const KNOWN_TABLES = ['download_history', 'subscriptions', 'subscription_items'] + +const migrationsTableDefinition = sql` + CREATE TABLE IF NOT EXISTS ${sql.identifier(MIGRATIONS_TABLE)} ( + id SERIAL PRIMARY KEY, + hash text NOT NULL, + created_at numeric + ) +` + +export const runMigrations = (database: BetterSQLite3Database): void => { + const migrationsFolder = resolveMigrationsFolder() + if (!migrationsFolder) { + log.warn('database: drizzle migrations folder not found, skipping migrations') + return + } + + try { + ensureBaseline(database, migrationsFolder) + migrate(database, { migrationsFolder, migrationsTable: MIGRATIONS_TABLE }) + } catch (error) { + log.error('database: failed to run drizzle migrations', error) + } +} + +const resolveMigrationsFolder = (): string | null => { + const candidates = new Set() + candidates.add(resolve(process.cwd(), MIGRATIONS_RELATIVE_PATH)) + candidates.add(resolve(__dirname, '../../../../', MIGRATIONS_RELATIVE_PATH)) + + if (process.resourcesPath) { + candidates.add(join(process.resourcesPath, MIGRATIONS_RELATIVE_PATH)) + candidates.add(join(process.resourcesPath, 'app.asar.unpacked', MIGRATIONS_RELATIVE_PATH)) + } + + try { + candidates.add(join(app.getAppPath(), MIGRATIONS_RELATIVE_PATH)) + } catch { + // app might not be ready yet, ignore + } + + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate + } + } + + return null +} + +const ensureBaseline = (database: BetterSQLite3Database, migrationsFolder: string): void => { + if (hasTable(database, MIGRATIONS_TABLE)) { + return + } + + const hasExistingSchema = KNOWN_TABLES.some((table) => hasTable(database, table)) + if (!hasExistingSchema) { + return + } + + log.info('database: detected existing schema, seeding drizzle migrations baseline') + database.run(migrationsTableDefinition) + + let migrations: MigrationMeta[] + try { + migrations = readMigrationFiles({ migrationsFolder, migrationsTable: MIGRATIONS_TABLE }) + } catch (error) { + log.error('database: failed to read drizzle migrations while seeding baseline', error) + return + } + + database.transaction((tx) => { + for (const migration of migrations) { + tx.run( + sql`INSERT INTO ${sql.identifier(MIGRATIONS_TABLE)} ("hash", "created_at") VALUES(${migration.hash}, ${migration.folderMillis})` + ) + } + }) +} + +const hasTable = (database: BetterSQLite3Database, tableName: string): boolean => { + const rows = database.all<{ name: string }>( + sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${tableName}` + ) + return rows.length > 0 +} diff --git a/src/main/lib/database/schema.ts b/src/main/lib/database/schema.ts new file mode 100644 index 0000000..0e2de38 --- /dev/null +++ b/src/main/lib/database/schema.ts @@ -0,0 +1,83 @@ +import { index, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const downloadHistoryTable = sqliteTable('download_history', { + id: text('id').primaryKey(), + url: text('url').notNull(), + title: text('title').notNull(), + thumbnail: text('thumbnail'), + type: text('type').notNull(), + status: text('status').notNull(), + downloadPath: text('download_path'), + savedFileName: text('saved_file_name'), + fileSize: integer('file_size', { mode: 'number' }), + duration: integer('duration', { mode: 'number' }), + downloadedAt: integer('downloaded_at', { mode: 'number' }).notNull(), + completedAt: integer('completed_at', { mode: 'number' }), + sortKey: integer('sort_key', { mode: 'number' }).notNull(), + error: text('error'), + description: text('description'), + channel: text('channel'), + uploader: text('uploader'), + viewCount: integer('view_count', { mode: 'number' }), + tags: text('tags'), + origin: text('origin'), + subscriptionId: text('subscription_id'), + selectedFormat: text('selected_format'), + playlistId: text('playlist_id'), + playlistTitle: text('playlist_title'), + playlistIndex: integer('playlist_index', { mode: 'number' }), + playlistSize: integer('playlist_size', { mode: 'number' }) +}) + +export const subscriptionsTable = sqliteTable('subscriptions', { + id: text('id').primaryKey(), + title: text('title').notNull(), + sourceUrl: text('source_url').notNull(), + feedUrl: text('feed_url').notNull(), + platform: text('platform').notNull(), + keywords: text('keywords').notNull(), + tags: text('tags').notNull(), + onlyDownloadLatest: integer('only_latest', { mode: 'number' }).notNull(), + enabled: integer('enabled', { mode: 'number' }).notNull(), + coverUrl: text('cover_url'), + latestVideoTitle: text('latest_video_title'), + latestVideoPublishedAt: integer('latest_video_published_at', { mode: 'number' }), + lastCheckedAt: integer('last_checked_at', { mode: 'number' }), + lastSuccessAt: integer('last_success_at', { mode: 'number' }), + status: text('status').notNull(), + lastError: text('last_error'), + createdAt: integer('created_at', { mode: 'number' }).notNull(), + updatedAt: integer('updated_at', { mode: 'number' }).notNull(), + downloadDirectory: text('download_directory'), + namingTemplate: text('naming_template') +}) + +export const subscriptionItemsTable = sqliteTable( + 'subscription_items', + { + subscriptionId: text('subscription_id').notNull(), + itemId: text('item_id').notNull(), + title: text('title').notNull(), + url: text('url').notNull(), + publishedAt: integer('published_at', { mode: 'number' }).notNull(), + thumbnail: text('thumbnail'), + added: integer('added', { mode: 'number' }).notNull(), + downloadId: text('download_id'), + createdAt: integer('created_at', { mode: 'number' }).notNull(), + updatedAt: integer('updated_at', { mode: 'number' }).notNull() + }, + (table) => ({ + pk: primaryKey({ + columns: [table.subscriptionId, table.itemId], + name: 'subscription_items_pk' + }), + subscriptionIdx: index('subscription_items_subscription_idx').on(table.subscriptionId) + }) +) + +export type DownloadHistoryRow = typeof downloadHistoryTable.$inferSelect +export type DownloadHistoryInsert = typeof downloadHistoryTable.$inferInsert +export type SubscriptionRow = typeof subscriptionsTable.$inferSelect +export type SubscriptionInsert = typeof subscriptionsTable.$inferInsert +export type SubscriptionItemRow = typeof subscriptionItemsTable.$inferSelect +export type SubscriptionItemInsert = typeof subscriptionItemsTable.$inferInsert diff --git a/src/main/lib/history-manager.ts b/src/main/lib/history-manager.ts index b94222d..8eb5416 100644 --- a/src/main/lib/history-manager.ts +++ b/src/main/lib/history-manager.ts @@ -8,6 +8,13 @@ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' import { app } from 'electron' import log from 'electron-log/main' import type { DownloadHistoryItem } from '../../shared/types' +import { runMigrations } from './database/migrate' +import { + type DownloadHistoryInsert, + type DownloadHistoryRow, + downloadHistoryTable +} from './database/schema' +import { getDatabaseFilePath } from './database-path' const logger = log.scope('history-manager') @@ -136,35 +143,6 @@ const parseTags = (value: string | null): string[] | undefined => { return parsed.length > 0 ? parsed : undefined } -const downloadHistoryTable = sqliteTable('download_history', { - id: text('id').primaryKey(), - url: text('url').notNull(), - title: text('title').notNull(), - thumbnail: text('thumbnail'), - type: text('type').notNull(), - status: text('status').notNull(), - downloadPath: text('download_path'), - savedFileName: text('saved_file_name'), - fileSize: integer('file_size', { mode: 'number' }), - duration: integer('duration', { mode: 'number' }), - downloadedAt: integer('downloaded_at', { mode: 'number' }).notNull(), - completedAt: integer('completed_at', { mode: 'number' }), - sortKey: integer('sort_key', { mode: 'number' }).notNull(), - error: text('error'), - description: text('description'), - channel: text('channel'), - uploader: text('uploader'), - viewCount: integer('view_count', { mode: 'number' }), - tags: text('tags'), - origin: text('origin'), - subscriptionId: text('subscription_id'), - selectedFormat: text('selected_format'), - playlistId: text('playlist_id'), - playlistTitle: text('playlist_title'), - playlistIndex: integer('playlist_index', { mode: 'number' }), - playlistSize: integer('playlist_size', { mode: 'number' }) -}) - const legacyDownloadHistoryTable = sqliteTable('download_history_legacy', { id: text('id').primaryKey(), status: text('status').notNull(), @@ -173,9 +151,6 @@ const legacyDownloadHistoryTable = sqliteTable('download_history_legacy', { sortKey: integer('sort_key', { mode: 'number' }).notNull(), payload: text('payload').notNull() }) - -type DownloadHistoryRow = typeof downloadHistoryTable.$inferSelect -type DownloadHistoryInsert = typeof downloadHistoryTable.$inferInsert type LegacyDownloadHistoryRow = typeof legacyDownloadHistoryTable.$inferSelect class HistoryManager { @@ -211,7 +186,7 @@ class HistoryManager { sqlite.pragma('foreign_keys = ON') const database = drizzle(sqlite) - database.run(createDownloadHistoryTableSql) + runMigrations(database) this.db = database logger.info(`history-db initialized at ${databasePath}`) @@ -219,7 +194,7 @@ class HistoryManager { } private getDatabasePath(): string { - return join(app.getPath('userData'), 'download-history.sqlite') + return getDatabaseFilePath() } private getLegacyStorePath(): string { diff --git a/src/main/lib/subscription-manager.ts b/src/main/lib/subscription-manager.ts index bc5c0cd..95e1089 100644 --- a/src/main/lib/subscription-manager.ts +++ b/src/main/lib/subscription-manager.ts @@ -1,14 +1,11 @@ import { randomUUID } from 'node:crypto' import { EventEmitter } from 'node:events' import fs from 'node:fs' -import { join } from 'node:path' import type { Database as BetterSqlite3Instance } from 'better-sqlite3' import DatabaseConstructor from 'better-sqlite3' import { and, desc, eq, inArray } from 'drizzle-orm' import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3' import { drizzle } from 'drizzle-orm/better-sqlite3' -import { index, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core' -import { app } from 'electron' import log from 'electron-log/main' import type { SubscriptionCreatePayload, @@ -18,6 +15,15 @@ import type { SubscriptionUpdatePayload } from '../../shared/types' import { sanitizeFilenameTemplate } from '../download-engine/args-builder' +import { runMigrations } from './database/migrate' +import { + type SubscriptionInsert, + type SubscriptionItemRow, + type SubscriptionRow, + subscriptionItemsTable, + subscriptionsTable +} from './database/schema' +import { getDatabaseFilePath } from './database-path' const sanitizeList = (values?: string[]): string[] => { if (!values || values.length === 0) { @@ -56,56 +62,6 @@ const parseStringArray = (value: string | null | undefined): string[] => { const stringifyArray = (values: string[]): string => JSON.stringify(sanitizeList(values)) -const subscriptionsTable = sqliteTable('subscriptions', { - id: text('id').primaryKey(), - title: text('title').notNull(), - sourceUrl: text('source_url').notNull(), - feedUrl: text('feed_url').notNull(), - platform: text('platform').notNull(), - keywords: text('keywords').notNull(), - tags: text('tags').notNull(), - onlyDownloadLatest: integer('only_latest', { mode: 'number' }).notNull(), - enabled: integer('enabled', { mode: 'number' }).notNull(), - coverUrl: text('cover_url'), - latestVideoTitle: text('latest_video_title'), - latestVideoPublishedAt: integer('latest_video_published_at', { mode: 'number' }), - lastCheckedAt: integer('last_checked_at', { mode: 'number' }), - lastSuccessAt: integer('last_success_at', { mode: 'number' }), - status: text('status').notNull(), - lastError: text('last_error'), - createdAt: integer('created_at', { mode: 'number' }).notNull(), - updatedAt: integer('updated_at', { mode: 'number' }).notNull(), - downloadDirectory: text('download_directory'), - namingTemplate: text('naming_template') -}) - -const subscriptionItemsTable = sqliteTable( - 'subscription_items', - { - subscriptionId: text('subscription_id').notNull(), - itemId: text('item_id').notNull(), - title: text('title').notNull(), - url: text('url').notNull(), - publishedAt: integer('published_at', { mode: 'number' }).notNull(), - thumbnail: text('thumbnail'), - added: integer('added', { mode: 'number' }).notNull(), - downloadId: text('download_id'), - createdAt: integer('created_at', { mode: 'number' }).notNull(), - updatedAt: integer('updated_at', { mode: 'number' }).notNull() - }, - (table) => ({ - pk: primaryKey({ - columns: [table.subscriptionId, table.itemId], - name: 'subscription_items_pk' - }), - subscriptionIdx: index('subscription_items_subscription_idx').on(table.subscriptionId) - }) -) - -type SubscriptionRow = typeof subscriptionsTable.$inferSelect -type SubscriptionInsert = typeof subscriptionsTable.$inferInsert -type SubscriptionItemRow = typeof subscriptionItemsTable.$inferSelect - export class SubscriptionManager extends EventEmitter { private sqlite: BetterSqlite3Instance | null = null private db: BetterSQLite3Database | null = null @@ -338,55 +294,7 @@ export class SubscriptionManager extends EventEmitter { this.sqlite.pragma('journal_mode = WAL') this.sqlite.pragma('foreign_keys = ON') this.db = drizzle(this.sqlite) - this.sqlite - .prepare( - `CREATE TABLE IF NOT EXISTS subscriptions ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - source_url TEXT NOT NULL, - feed_url TEXT NOT NULL, - platform TEXT NOT NULL, - keywords TEXT NOT NULL, - tags TEXT NOT NULL, - only_latest INTEGER NOT NULL, - enabled INTEGER NOT NULL, - cover_url TEXT, - latest_video_title TEXT, - latest_video_published_at INTEGER, - last_checked_at INTEGER, - last_success_at INTEGER, - status TEXT NOT NULL, - last_error TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - download_directory TEXT, - naming_template TEXT - )` - ) - .run() - - this.sqlite - .prepare( - `CREATE TABLE IF NOT EXISTS subscription_items ( - subscription_id TEXT NOT NULL, - item_id TEXT NOT NULL, - title TEXT NOT NULL, - url TEXT NOT NULL, - published_at INTEGER NOT NULL, - thumbnail TEXT, - added INTEGER NOT NULL, - download_id TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - PRIMARY KEY (subscription_id, item_id) - )` - ) - .run() - this.sqlite - .prepare( - 'CREATE INDEX IF NOT EXISTS subscription_items_subscription_idx ON subscription_items (subscription_id)' - ) - .run() + runMigrations(this.db) this.ensureSubscriptionsSchema() this.ensureItemsSchema() log.info('subscriptions: database initialized at', databasePath) @@ -580,7 +488,7 @@ export class SubscriptionManager extends EventEmitter { } private getDatabasePath(): string { - return join(app.getPath('userData'), 'subscriptions.sqlite') + return getDatabaseFilePath() } private migrateLegacyStore(): void {