diff --git a/package.json b/package.json index cb2ded7..c6d23c6 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ "test": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", - "test:types": "tsc --noEmit --project src/renderer/tsconfig.json", + "test:types": "npm run test:types-renderer && npm run test:types-main && npm run test:types-shared", + "test:types-renderer": "tsc --noEmit --project src/renderer/tsconfig.json", + "test:types-main": "tsc --noEmit --project src/main/tsconfig.json", + "test:types-shared": "tsc --noEmit --project src/shared/tsconfig.json", "eslint": "eslint --ext .ts,.js ./", "prettier": "prettier --write './**/*.{ts,js,tsx,jsx}'" }, @@ -35,7 +38,7 @@ "dependencies": { "@sentry/node": "^6.19.6", "@sentry/tracing": "^6.19.6", - "NeteaseCloudMusicApi": "^4.5.11", + "NeteaseCloudMusicApi": "^4.5.12", "better-sqlite3": "7.5.1", "change-case": "^4.1.2", "cookie-parser": "^1.4.6", @@ -46,6 +49,7 @@ }, "devDependencies": { "@sentry/react": "^6.19.6", + "@testing-library/react": "^13.1.0", "@types/better-sqlite3": "^7.5.0", "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.13", @@ -56,7 +60,7 @@ "@types/md5": "^2.3.2", "@types/qrcode": "^1.4.2", "@types/react": "^18.0.5", - "@types/react-dom": "^18.0.0", + "@types/react-dom": "^18.0.1", "@typescript-eslint/eslint-plugin": "^5.19.0", "@typescript-eslint/parser": "^5.19.0", "@vitejs/plugin-react": "^1.3.1", @@ -69,13 +73,13 @@ "colord": "^2.9.2", "concurrently": "^7.1.0", "cross-env": "^7.0.3", - "dayjs": "^1.11.0", + "dayjs": "^1.11.1", "dotenv": "^16.0.0", - "electron": "^18.0.3", + "electron": "^18.0.4", "electron-builder": "^23.0.3", "electron-devtools-installer": "^3.2.0", "electron-rebuild": "^3.2.7", - "electron-releases": "^3.985.0", + "electron-releases": "^3.987.0", "esbuild": "^0.14.36", "eslint": "^8.13.0", "eslint-plugin-react": "^7.29.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 580c37b..2f2a216 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,7 @@ specifiers: '@sentry/node': ^6.19.6 '@sentry/react': ^6.19.6 '@sentry/tracing': ^6.19.6 + '@testing-library/react': ^13.1.0 '@types/better-sqlite3': ^7.5.0 '@types/cookie-parser': ^1.4.2 '@types/express': ^4.17.13 @@ -14,12 +15,12 @@ specifiers: '@types/md5': ^2.3.2 '@types/qrcode': ^1.4.2 '@types/react': ^18.0.5 - '@types/react-dom': ^18.0.0 + '@types/react-dom': ^18.0.1 '@typescript-eslint/eslint-plugin': ^5.19.0 '@typescript-eslint/parser': ^5.19.0 '@vitejs/plugin-react': ^1.3.1 '@vitest/ui': ^0.9.3 - NeteaseCloudMusicApi: ^4.5.11 + NeteaseCloudMusicApi: ^4.5.12 autoprefixer: ^10.4.4 axios: ^0.26.1 better-sqlite3: 7.5.1 @@ -31,15 +32,14 @@ specifiers: concurrently: ^7.1.0 cookie-parser: ^1.4.6 cross-env: ^7.0.3 - csstype: ^3.0.11 - dayjs: ^1.11.0 + dayjs: ^1.11.1 dotenv: ^16.0.0 - electron: ^18.0.3 + electron: ^18.0.4 electron-builder: ^23.0.3 electron-devtools-installer: ^3.2.0 electron-log: ^4.4.6 electron-rebuild: ^3.2.7 - electron-releases: ^3.985.0 + electron-releases: ^3.987.0 electron-store: ^8.0.1 esbuild: ^0.14.36 eslint: ^8.13.0 @@ -64,18 +64,17 @@ specifiers: qrcode: ^1.5.0 react: ^18.0.0 react-dom: ^18.0.0 + react-ga4: ^1.4.1 react-hot-toast: ^2.2.0 react-query: ^3.34.19 react-router-dom: ^6.3.0 react-use: ^17.3.2 - rollup: ^2.70.1 rollup-plugin-visualizer: ^5.6.0 sass: ^1.50.0 tailwindcss: ^3.0.24 typescript: ^4.6.3 unplugin-auto-import: ^0.7.1 valtio: ^1.5.2 - valtio-persist: ^1.0.2 vite: ^2.9.5 vite-plugin-svg-icons: ^2.0.1 vitest: ^0.9.3 @@ -84,7 +83,7 @@ specifiers: dependencies: '@sentry/node': 6.19.6 '@sentry/tracing': 6.19.6 - NeteaseCloudMusicApi: 4.5.11 + NeteaseCloudMusicApi: 4.5.12 better-sqlite3: 7.5.1 change-case: 4.1.2 cookie-parser: 1.4.6 @@ -95,6 +94,7 @@ dependencies: devDependencies: '@sentry/react': 6.19.6_react@18.0.0 + '@testing-library/react': 13.1.0_react-dom@18.0.0+react@18.0.0 '@types/better-sqlite3': 7.5.0 '@types/cookie-parser': 1.4.2 '@types/express': 4.17.13 @@ -105,7 +105,7 @@ devDependencies: '@types/md5': 2.3.2 '@types/qrcode': 1.4.2 '@types/react': 18.0.5 - '@types/react-dom': 18.0.0 + '@types/react-dom': 18.0.1 '@typescript-eslint/eslint-plugin': 5.19.0_f34adc8488d2e4f014fe61432d70cbf2 '@typescript-eslint/parser': 5.19.0_eslint@8.13.0+typescript@4.6.3 '@vitejs/plugin-react': 1.3.1 @@ -118,14 +118,13 @@ devDependencies: colord: 2.9.2 concurrently: 7.1.0 cross-env: 7.0.3 - csstype: 3.0.11 - dayjs: 1.11.0 + dayjs: 1.11.1 dotenv: 16.0.0 - electron: 18.0.3 + electron: 18.0.4 electron-builder: 23.0.3 electron-devtools-installer: 3.2.0 electron-rebuild: 3.2.7 - electron-releases: 3.985.0 + electron-releases: 3.987.0 esbuild: 0.14.36 eslint: 8.13.0 eslint-plugin-react: 7.29.4_eslint@8.13.0 @@ -147,18 +146,17 @@ devDependencies: qrcode: 1.5.0 react: 18.0.0 react-dom: 18.0.0_react@18.0.0 - react-hot-toast: 2.2.0_aee3b59847029cfc9aee5217330a3daf + react-ga4: 1.4.1 + react-hot-toast: 2.2.0_react-dom@18.0.0+react@18.0.0 react-query: 3.34.19_react-dom@18.0.0+react@18.0.0 react-router-dom: 6.3.0_react-dom@18.0.0+react@18.0.0 react-use: 17.3.2_react-dom@18.0.0+react@18.0.0 - rollup: 2.70.1 - rollup-plugin-visualizer: 5.6.0_rollup@2.70.1 + rollup-plugin-visualizer: 5.6.0 sass: 1.50.0 tailwindcss: 3.0.24 typescript: 4.6.3 - unplugin-auto-import: 0.7.1_05062056c3506028f60dc849695a4e6b + unplugin-auto-import: 0.7.1_esbuild@0.14.36+vite@2.9.5 valtio: 1.5.2_react@18.0.0+vite@2.9.5 - valtio-persist: 1.0.2_valtio@1.5.2 vite: 2.9.5_sass@1.50.0 vite-plugin-svg-icons: 2.0.1_vite@2.9.5 vitest: 0.9.3_ac1eaec0e6cd6e44c577f894fa1b602e @@ -767,6 +765,34 @@ packages: defer-to-connect: 2.0.1 dev: true + /@testing-library/dom/8.13.0: + resolution: {integrity: sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==} + engines: {node: '>=12'} + dependencies: + '@babel/code-frame': 7.16.7 + '@babel/runtime': 7.17.8 + '@types/aria-query': 4.2.2 + aria-query: 5.0.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.13 + lz-string: 1.4.4 + pretty-format: 27.5.1 + dev: true + + /@testing-library/react/13.1.0_react-dom@18.0.0+react@18.0.0: + resolution: {integrity: sha512-neStnDZdhkvZNNmPhhhi8+BXg3YCvjNmd8yGdr44VLVcFUDPForwokJWpDRCh3DvuX/M37Pt94fLwkM7aNut/A==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/runtime': 7.17.8 + '@testing-library/dom': 8.13.0 + '@types/react-dom': 18.0.1 + react: 18.0.0 + react-dom: 18.0.0_react@18.0.0 + dev: true + /@tokenizer/token/0.3.0: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -784,6 +810,10 @@ packages: engines: {node: '>=10.13.0'} dev: true + /@types/aria-query/4.2.2: + resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} + dev: true + /@types/better-sqlite3/7.5.0: resolution: {integrity: sha512-G9ZbMjydW2yj1AgiPlUtdgF3a1qNpLJLudc9ynJCeJByS3XFWpmT9LT+VSHrKHFbxb31CvtYwetLTOvG9zdxdg==} dependencies: @@ -936,8 +966,8 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true - /@types/node/16.11.26: - resolution: {integrity: sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==} + /@types/node/16.11.27: + resolution: {integrity: sha512-C1pD3kgLoZ56Uuy5lhfOxie4aZlA3UMGLX9rXteq4WitEZH6Rl80mwactt9QG0w0gLFlN/kLBTFnGXtDVWvWQw==} dev: true /@types/node/17.0.23: @@ -970,8 +1000,8 @@ packages: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} dev: true - /@types/react-dom/18.0.0: - resolution: {integrity: sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg==} + /@types/react-dom/18.0.1: + resolution: {integrity: sha512-jCwTXvHtRLiyVvKm9aEdHXs8rflVOGd5Sl913JZrPshfXjn8NYsTNZOz70bCsA31IR0TOqwi3ad+X4tSCBoMTw==} dependencies: '@types/react': 18.0.5 dev: true @@ -1174,8 +1204,8 @@ packages: resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} dev: true - /NeteaseCloudMusicApi/4.5.11: - resolution: {integrity: sha512-v/L3I5NA+tCTfD9Nkr0i3igTEHJcvHQeZXM0Sqw1iBCqN6z62qtKoTON9yNSEgIGoZrzFfMyqHloLkv1iBo5sQ==} + /NeteaseCloudMusicApi/4.5.12: + resolution: {integrity: sha512-tlATnWTyOVH6hAxiH2f+nhgFtWulC1xMTz/7VyRjP/axcoMWff2mEa2Bw9kNl+Wb2bUrP/1oC2GXF41oyWwAJQ==} engines: {node: '>=12'} hasBin: true dependencies: @@ -1353,6 +1383,11 @@ packages: dependencies: color-convert: 2.0.1 + /ansi-styles/5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + /anymatch/3.1.2: resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} engines: {node: '>= 8'} @@ -1424,6 +1459,11 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /aria-query/5.0.0: + resolution: {integrity: sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==} + engines: {node: '>=6.0'} + dev: true + /arr-diff/4.0.0: resolution: {integrity: sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=} engines: {node: '>=0.10.0'} @@ -2523,8 +2563,8 @@ packages: engines: {node: '>=0.11'} dev: true - /dayjs/1.11.0: - resolution: {integrity: sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug==} + /dayjs/1.11.1: + resolution: {integrity: sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA==} dev: true /debounce-fn/4.0.0: @@ -2617,6 +2657,15 @@ packages: object-keys: 1.1.1 dev: true + /define-properties/1.1.4: + resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + optional: true + /define-property/0.2.5: resolution: {integrity: sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=} engines: {node: '>=0.10.0'} @@ -2781,6 +2830,10 @@ packages: esutils: 2.0.3 dev: true + /dom-accessibility-api/0.5.13: + resolution: {integrity: sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw==} + dev: true + /dom-serializer/0.2.2: resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} dependencies: @@ -2980,8 +3033,8 @@ packages: - supports-color dev: true - /electron-releases/3.985.0: - resolution: {integrity: sha512-1HJzI4m6kS0aV45NWqs99eR7ekSAN7JIKsklsV+1eGfcRwwyP6O8c3458rL4vdvlhZ9+QnUM3B0S03+XV5M0Tg==} + /electron-releases/3.987.0: + resolution: {integrity: sha512-Rol/iOHhTdEiVqD5O9p4rLkNSlK82FFX3M99BrNvE7Gs52/mRWJYNdhPuY9d/Cs3jx2H8KJsB49Tfpm4TOqJHw==} dev: true /electron-store/8.0.1: @@ -2995,14 +3048,14 @@ packages: resolution: {integrity: sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg==} dev: true - /electron/18.0.3: - resolution: {integrity: sha512-QRUZkGL8O/8CyDmTLSjBeRsZmGTPlPVeWnnpkdNqgHYYaOc/A881FKMiNzvQ9Cj0a+rUavDdwBUfUL82U3Ay7w==} + /electron/18.0.4: + resolution: {integrity: sha512-xfsozNpFr3WzeM1EFlw2qqiqXbCrgQNBJJMlcC4/DUYVpkF8364SZenX7FFFA42NmwXiOEahkvvho/u7UrAcGg==} engines: {node: '>= 8.6'} hasBin: true requiresBuild: true dependencies: '@electron/get': 1.14.1 - '@types/node': 16.11.26 + '@types/node': 16.11.27 extract-zip: 1.7.0 transitivePeerDependencies: - supports-color @@ -4040,7 +4093,7 @@ packages: es6-error: 4.1.1 matcher: 3.0.0 roarr: 2.15.4 - semver: 7.3.6 + semver: 7.3.7 serialize-error: 7.0.1 dev: true optional: true @@ -4080,7 +4133,7 @@ packages: resolution: {integrity: sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==} engines: {node: '>= 0.4'} dependencies: - define-properties: 1.1.3 + define-properties: 1.1.4 dev: true optional: true @@ -4096,12 +4149,10 @@ packages: slash: 3.0.0 dev: true - /goober/2.1.8_csstype@3.0.11: + /goober/2.1.8: resolution: {integrity: sha512-S0C85gCzcfFCMSdjD/CxyQMt1rbf2qEg6hmDzxk2FfD7+7Ogk55m8ZFUMtqNaZM4VVX/qaU9AzSORG+Gf4ZpAQ==} peerDependencies: csstype: ^3.0.10 - dependencies: - csstype: 3.0.11 dev: true /got/11.8.3: @@ -4174,6 +4225,13 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors/1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.1.1 + dev: true + optional: true + /has-symbols/1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} @@ -4357,6 +4415,16 @@ packages: transitivePeerDependencies: - supports-color + /https-proxy-agent/5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /humanize-ms/1.2.1: resolution: {integrity: sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=} dependencies: @@ -4967,7 +5035,7 @@ packages: dependencies: universalify: 2.0.0 optionalDependencies: - graceful-fs: 4.2.9 + graceful-fs: 4.2.10 dev: true /jsx-ast-utils/3.2.2: @@ -5171,6 +5239,11 @@ packages: resolution: {integrity: sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=} dev: false + /lz-string/1.4.4: + resolution: {integrity: sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=} + hasBin: true + dev: true + /lzma-native/8.0.6: resolution: {integrity: sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==} engines: {node: '>=10.0.0'} @@ -5902,7 +5975,7 @@ packages: debug: 4.3.4 get-uri: 3.0.2 http-proxy-agent: 4.0.1 - https-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 pac-resolver: 5.0.0 raw-body: 2.5.1 socks-proxy-agent: 5.0.1 @@ -6230,6 +6303,15 @@ packages: hasBin: true dev: true + /pretty-format/27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + /process-nextick-args/2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -6372,14 +6454,18 @@ packages: scheduler: 0.21.0 dev: true - /react-hot-toast/2.2.0_aee3b59847029cfc9aee5217330a3daf: + /react-ga4/1.4.1: + resolution: {integrity: sha512-ioBMEIxd4ePw4YtaloTUgqhQGqz5ebDdC4slEpLgy2sLx1LuZBC9iYCwDymTXzcntw6K1dHX183ulP32nNdG7w==} + dev: true + + /react-hot-toast/2.2.0_react-dom@18.0.0+react@18.0.0: resolution: {integrity: sha512-248rXw13uhf/6TNDVzagX+y7R8J183rp7MwUMNkcrBRyHj/jWOggfXTGlM8zAOuh701WyVW+eUaWG2LeSufX9g==} engines: {node: '>=10'} peerDependencies: react: '>=16' react-dom: '>=16' dependencies: - goober: 2.1.8_csstype@3.0.11 + goober: 2.1.8 react: 18.0.0 react-dom: 18.0.0_react@18.0.0 transitivePeerDependencies: @@ -6390,6 +6476,10 @@ packages: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true + /react-is/17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + /react-query/3.34.19_react-dom@18.0.0+react@18.0.0: resolution: {integrity: sha512-JO0Ymi58WKmvnhgg6bGIrYIeKb64KsKaPWo8JcGnmK2jJxAs2XmMBzlP75ZepSU7CHzcsWtIIyhMrLbX3pb/3w==} peerDependencies: @@ -6697,7 +6787,7 @@ packages: dev: true optional: true - /rollup-plugin-visualizer/5.6.0_rollup@2.70.1: + /rollup-plugin-visualizer/5.6.0: resolution: {integrity: sha512-CKcc8GTUZjC+LsMytU8ocRr/cGZIfMR7+mdy4YnlyetlmIl/dM8BMnOEpD4JPIGt+ZVW7Db9ZtSsbgyeBH3uTA==} engines: {node: '>=12'} hasBin: true @@ -6706,13 +6796,12 @@ packages: dependencies: nanoid: 3.3.2 open: 8.4.0 - rollup: 2.70.1 source-map: 0.7.3 yargs: 17.4.0 dev: true - /rollup/2.70.1: - resolution: {integrity: sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA==} + /rollup/2.70.2: + resolution: {integrity: sha512-EitogNZnfku65I1DD5Mxe8JYRUCy0hkK5X84IlDtUs+O6JRMpRciXTzyCUuX11b5L5pvjH+OmFXiQ3XjabcXgg==} engines: {node: '>=10.0.0'} hasBin: true optionalDependencies: @@ -6833,6 +6922,15 @@ packages: lru-cache: 7.8.1 dev: true + /semver/7.3.7: + resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + optional: true + /send/0.17.2: resolution: {integrity: sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==} engines: {node: '>= 0.8.0'} @@ -7715,7 +7813,7 @@ packages: engines: {node: '>= 0.8'} dev: false - /unplugin-auto-import/0.7.1_05062056c3506028f60dc849695a4e6b: + /unplugin-auto-import/0.7.1_esbuild@0.14.36+vite@2.9.5: resolution: {integrity: sha512-9865OV9eP99PNxHR2mtTDExeN01m4M9boT5U2BtIwsU1wDRsaFIYWLwcCBEjvXzXfTTC2NNMskhHGVAMfL2WgA==} engines: {node: '>=14'} peerDependencies: @@ -7729,7 +7827,7 @@ packages: local-pkg: 0.4.1 magic-string: 0.26.1 resolve: 1.22.0 - unplugin: 0.6.2_05062056c3506028f60dc849695a4e6b + unplugin: 0.6.2_esbuild@0.14.36+vite@2.9.5 transitivePeerDependencies: - esbuild - rollup @@ -7737,7 +7835,7 @@ packages: - webpack dev: true - /unplugin/0.6.2_05062056c3506028f60dc849695a4e6b: + /unplugin/0.6.2_esbuild@0.14.36+vite@2.9.5: resolution: {integrity: sha512-+QONc2uBFQbeo4x5mlJHjTKjR6pmuchMpGVrWhwdGFFMb4ttFZ4E9KqhOOrNcm3Q8NNyB1vJ4s5e36IZC7UWYw==} peerDependencies: esbuild: '>=0.13' @@ -7756,7 +7854,6 @@ packages: dependencies: chokidar: 3.5.3 esbuild: 0.14.36 - rollup: 2.70.1 vite: 2.9.5_sass@1.50.0 webpack-sources: 3.2.3 webpack-virtual-modules: 0.4.3 @@ -7880,15 +7977,6 @@ packages: source-map: 0.7.3 dev: true - /valtio-persist/1.0.2_valtio@1.5.2: - resolution: {integrity: sha512-OBVEUZTS1heiA5R3j8CPDuXIMmmjIvq/4CiO+pElXd7f7b7nR3vIH5qql35hXw/AkLdftqTUcVCNVf6yAJ1ypA==} - peerDependencies: - valtio: ^1.2.5 - dependencies: - lodash: 4.17.21 - valtio: 1.5.2_react@18.0.0+vite@2.9.5 - dev: true - /valtio/1.5.2_react@18.0.0+vite@2.9.5: resolution: {integrity: sha512-4oqGO+7xSKZJJgLsfwRdzQxxy4hiOjiE0IZv0xoNNLtJQ+Y6mtWoEl0Y0JyUCrU/HBmY+0W/yL3lwjrpTBCJ/w==} engines: {node: '>=12.7.0'} @@ -7970,7 +8058,7 @@ packages: esbuild: 0.14.36 postcss: 8.4.12 resolve: 1.22.0 - rollup: 2.70.1 + rollup: 2.70.2 sass: 1.50.0 optionalDependencies: fsevents: 2.3.2 diff --git a/src/main/CacheAPIsName.ts b/src/main/CacheAPIsName.ts deleted file mode 100644 index ed39ae2..0000000 --- a/src/main/CacheAPIsName.ts +++ /dev/null @@ -1,15 +0,0 @@ -export enum APIs { - UserPlaylist = 'user/playlist', - UserAccount = 'user/account', - Personalized = 'personalized', - RecommendResource = 'recommend/resource', - Likelist = 'likelist', - SongDetail = 'song/detail', - SongUrl = 'song/url', - Album = 'album', - PlaylistDetail = 'playlist/detail', - Artists = 'artists', - ArtistAlbum = 'artist/album', - Lyric = 'lyric', - CoverColor = 'cover_color', -} diff --git a/src/main/IpcChannelsName.ts b/src/main/IpcChannelsName.ts deleted file mode 100644 index 4dacad1..0000000 --- a/src/main/IpcChannelsName.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum IpcChannels { - ClearAPICache = 'clear-api-cache', - Minimize = 'minimize', - MaximizeOrUnmaximize = 'maximize-or-unmaximize', - Close = 'close', - IsMaximized = 'is-maximized', - GetApiCacheSync = 'get-api-cache-sync', - DevDbExportJson = 'dev-db-export-json', - CacheCoverColor = 'cache-cover-color', -} diff --git a/src/main/cache.ts b/src/main/cache.ts index 75d11cc..8114f24 100644 --- a/src/main/cache.ts +++ b/src/main/cache.ts @@ -1,11 +1,11 @@ import { db, Tables } from './db' -import type { FetchTracksResponse } from '../renderer/api/track' +import type { FetchTracksResponse } from '@/shared/api/Track' import { app } from 'electron' import { Request, Response } from 'express' import log from './log' import fs from 'fs' import * as musicMetadata from 'music-metadata' -import { APIs } from './CacheAPIsName' +import { APIs, APIsParams, APIsResponse } from '../shared/CacheAPIs' class Cache { constructor() { @@ -18,6 +18,8 @@ class Cache { case APIs.UserAccount: case APIs.Personalized: case APIs.RecommendResource: + case APIs.UserAlbums: + case APIs.UserArtists: case APIs.Likelist: { if (!data) return db.upsert(Tables.AccountData, { @@ -27,7 +29,7 @@ class Cache { }) break } - case APIs.SongDetail: { + case APIs.Track: { if (!data.songs) return const tracks = (data as FetchTracksResponse).songs.map(t => ({ id: t.id, @@ -47,7 +49,7 @@ class Cache { }) break } - case APIs.PlaylistDetail: { + case APIs.Playlist: { if (!data.playlist) return db.upsert(Tables.Playlist, { id: data.playlist.id, @@ -56,7 +58,7 @@ class Cache { }) break } - case APIs.Artists: { + case APIs.Artist: { if (!data.artist) return db.upsert(Tables.Artist, { id: data.artist.id, @@ -108,7 +110,7 @@ class Cache { } } - get(api: string, query: any): any { + get(api: T, params: any): any { switch (api) { case APIs.UserPlaylist: case APIs.UserAccount: @@ -119,15 +121,13 @@ class Cache { if (data?.json) return JSON.parse(data.json) break } - case APIs.SongDetail: { - const ids: string[] = query?.ids.split(',') + case APIs.Track: { + const ids: number[] = params?.ids + .split(',') + .map((id: string) => Number(id)) if (ids.length === 0) return - let isIDsValid = true - ids.forEach(id => { - if (id === '' || isNaN(Number(id))) isIDsValid = false - }) - if (!isIDsValid) return + if (ids.includes(NaN)) return const tracksRaw = db.findMany(Tables.Track, ids) @@ -138,7 +138,6 @@ class Cache { const track = tracksRaw.find(t => t.id === Number(id)) as any return JSON.parse(track.json) }) - return { code: 200, songs: tracks, @@ -146,8 +145,8 @@ class Cache { } } case APIs.Album: { - if (isNaN(Number(query?.id))) return - const data = db.find(Tables.Album, query.id) + if (isNaN(Number(params?.id))) return + const data = db.find(Tables.Album, params.id) if (data?.json) return { resourceState: true, @@ -157,22 +156,22 @@ class Cache { } break } - case APIs.PlaylistDetail: { - if (isNaN(Number(query?.id))) return - const data = db.find(Tables.Playlist, query.id) + case APIs.Playlist: { + if (isNaN(Number(params?.id))) return + const data = db.find(Tables.Playlist, params.id) if (data?.json) return JSON.parse(data.json) break } - case APIs.Artists: { - if (isNaN(Number(query?.id))) return - const data = db.find(Tables.Artist, query.id) + case APIs.Artist: { + if (isNaN(Number(params?.id))) return + const data = db.find(Tables.Artist, params.id) if (data?.json) return JSON.parse(data.json) break } case APIs.ArtistAlbum: { - if (isNaN(Number(query?.id))) return + if (isNaN(Number(params?.id))) return - const artistAlbumsRaw = db.find(Tables.ArtistAlbum, query.id) + const artistAlbumsRaw = db.find(Tables.ArtistAlbum, params.id) if (!artistAlbumsRaw?.json) return const artistAlbums = JSON.parse(artistAlbumsRaw.json) @@ -186,21 +185,21 @@ class Cache { return artistAlbums } case APIs.Lyric: { - if (isNaN(Number(query?.id))) return - const data = db.find(Tables.Lyric, query.id) + if (isNaN(Number(params?.id))) return + const data = db.find(Tables.Lyric, params.id) if (data?.json) return JSON.parse(data.json) break } case APIs.CoverColor: { - if (isNaN(Number(query?.id))) return - return db.find(Tables.CoverColor, query.id)?.color + if (isNaN(Number(params?.id))) return + return db.find(Tables.CoverColor, params.id)?.color } } } getForExpress(api: string, req: Request) { // Get track detail cache - if (api === APIs.SongDetail) { + if (api === APIs.Track) { const cache = this.get(api, req.query) if (cache) { log.debug(`[cache] Cache hit for ${req.path}`) diff --git a/src/main/db.ts b/src/main/db.ts index 49afd80..b8e6e8e 100644 --- a/src/main/db.ts +++ b/src/main/db.ts @@ -38,6 +38,7 @@ export interface TablesStructures { [Tables.Audio]: { id: number br: number + type: 'mp3' | 'flac' | 'ogg' | 'wav' | 'm4a' | 'aac' | 'unknown' source: 'netease' | 'migu' | 'kuwo' | 'kugou' | 'youtube' url: string updatedAt: number @@ -155,10 +156,7 @@ class DB { upsertMany(data) } - delete( - table: T, - key: TablesStructures[T]['id'] - ) { + delete(table: T, key: TablesStructures[T]['id']) { return this.sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key) } diff --git a/src/main/index.ts b/src/main/index.ts index 4f1633f..657518b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -18,19 +18,6 @@ const isMac = process.platform === 'darwin' const isLinux = process.platform === 'linux' const isDev = process.env.NODE_ENV === 'development' -log.info('[index] Main process start') - -// Disable GPU Acceleration for Windows 7 -if (release().startsWith('6.1')) app.disableHardwareAcceleration() - -// Set application name for Windows 10+ notifications -if (process.platform === 'win32') app.setAppUserModelId(app.getName()) - -if (!app.requestSingleInstanceLock()) { - app.quit() - process.exit(0) -} - interface TypedElectronStore { window: { width: number @@ -40,115 +27,138 @@ interface TypedElectronStore { } } -const store = new Store({ - defaults: { - window: { - width: 1440, - height: 960, +class Main { + win: BrowserWindow | null = null + store = new Store({ + defaults: { + window: { + width: 1440, + height: 960, + }, }, - }, -}) - -let win: BrowserWindow | null = null - -async function createWindow() { - // Create window - - const options: BrowserWindowConstructorOptions = { - title: 'Main window', - webPreferences: { - preload: join(__dirname, 'rendererPreload.js'), - }, - width: store.get('window.width'), - height: store.get('window.height'), - minWidth: 1080, - minHeight: 720, - vibrancy: 'fullscreen-ui', - titleBarStyle: 'hiddenInset', - frame: !(isWindows || isLinux), // TODO: 适用于linux下独立的启用开关 - } - if (store.get('window')) { - options.x = store.get('window.x') - options.y = store.get('window.y') - } - win = new BrowserWindow(options) - - // Web server - const url = `http://localhost:${process.env.ELECTRON_WEB_SERVER_PORT}` - win.loadURL(url) - - if (isDev) { - win.webContents.openDevTools() - } - - // Make all links open with the browser, not with the application - win.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith('https:')) shell.openExternal(url) - return { action: 'deny' } }) - // Save window position - const saveBounds = () => { - const bounds = win?.getBounds() - if (bounds) { - store.set('window', bounds) + constructor() { + log.info('[index] Main process start') + + // Disable GPU Acceleration for Windows 7 + if (release().startsWith('6.1')) app.disableHardwareAcceleration() + + // Set application name for Windows 10+ notifications + if (process.platform === 'win32') app.setAppUserModelId(app.getName()) + + // Make sure the app only run on one instance + if (!app.requestSingleInstanceLock()) { + app.quit() + process.exit(0) } + + app.whenReady().then(() => { + log.info('[index] App ready') + this.createWindow() + this.handleAppEvents() + this.handleWindowEvents() + initIpcMain(this.win) + this.initDevTools() + }) } - win.on('resized', saveBounds) - win.on('moved', saveBounds) -} -app.whenReady().then(async () => { - log.info('[index] App ready') - createWindow() - handleWindowEvents() - initIpcMain(win) + initDevTools() { + if (!isDev || !this.win) return - // Install devtool extension - if (isDev) { + // Install devtool extension const { default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS, // eslint-disable-next-line @typescript-eslint/no-var-requires } = require('electron-devtools-installer') - installExtension(REACT_DEVELOPER_TOOLS.id).catch(err => + installExtension(REACT_DEVELOPER_TOOLS.id).catch((err: any) => log.info('An error occurred: ', err) ) - installExtension(REDUX_DEVTOOLS.id).catch(err => + installExtension(REDUX_DEVTOOLS.id).catch((err: any) => log.info('An error occurred: ', err) ) + + this.win.webContents.openDevTools() } -}) -app.on('window-all-closed', () => { - win = null - if (process.platform !== 'darwin') app.quit() -}) + createWindow() { + const options: BrowserWindowConstructorOptions = { + title: 'YesPlayMusic', + webPreferences: { + preload: join(__dirname, 'rendererPreload.js'), + }, + width: this.store.get('window.width'), + height: this.store.get('window.height'), + minWidth: 1080, + minHeight: 720, + vibrancy: 'fullscreen-ui', + titleBarStyle: 'hiddenInset', + frame: !(isWindows || isLinux), // TODO: 适用于linux下独立的启用开关 + } + if (this.store.get('window')) { + options.x = this.store.get('window.x') + options.y = this.store.get('window.y') + } + this.win = new BrowserWindow(options) -app.on('second-instance', () => { - if (win) { - // Focus on the main window if the user tried to open another - if (win.isMinimized()) win.restore() - win.focus() + // Web server + const url = `http://localhost:${process.env.ELECTRON_WEB_SERVER_PORT}` + this.win.loadURL(url) + + // Make all links open with the browser, not with the application + this.win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith('https:')) shell.openExternal(url) + return { action: 'deny' } + }) } -}) -app.on('activate', () => { - const allWindows = BrowserWindow.getAllWindows() - if (allWindows.length) { - allWindows[0].focus() - } else { - createWindow() + handleWindowEvents() { + if (!this.win) return + + // Window maximize and minimize + this.win.on('maximize', () => { + this.win && this.win.webContents.send('is-maximized', true) + }) + + this.win.on('unmaximize', () => { + this.win && this.win.webContents.send('is-maximized', false) + }) + + // Save window position + const saveBounds = () => { + const bounds = this.win?.getBounds() + if (bounds) { + this.store.set('window', bounds) + } + } + this.win.on('resized', saveBounds) + this.win.on('moved', saveBounds) } -}) -const handleWindowEvents = () => { - win?.on('maximize', () => { - win?.webContents.send('is-maximized', true) - }) + handleAppEvents() { + app.on('window-all-closed', () => { + this.win = null + if (process.platform !== 'darwin') app.quit() + }) - win?.on('unmaximize', () => { - win?.webContents.send('is-maximized', false) - }) + app.on('second-instance', () => { + if (!this.win) return + // Focus on the main window if the user tried to open another + if (this.win.isMinimized()) this.win.restore() + this.win.focus() + }) + + app.on('activate', () => { + const allWindows = BrowserWindow.getAllWindows() + if (allWindows.length) { + allWindows[0].focus() + } else { + this.createWindow() + } + }) + } } + +new Main() diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 0c9f37e..832ca7c 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -1,26 +1,33 @@ import { BrowserWindow, ipcMain, app } from 'electron' import { db, Tables } from './db' -import { IpcChannels } from './IpcChannelsName' +import { IpcChannels, IpcChannelsParams } from '../shared/IpcChannels' import cache from './cache' import log from './log' import fs from 'fs' -import { APIs } from './CacheAPIsName' +import { APIs } from '../shared/CacheAPIs' + +const on = ( + channel: T, + listener: (event: Electron.IpcMainEvent, params: IpcChannelsParams[T]) => void +) => { + ipcMain.on(channel, listener) +} /** * 处理需要win对象的事件 * @param {BrowserWindow} win */ export function initIpcMain(win: BrowserWindow | null) { - ipcMain.on(IpcChannels.Minimize, () => { + on(IpcChannels.Minimize, () => { win?.minimize() }) - ipcMain.on(IpcChannels.MaximizeOrUnmaximize, () => { + on(IpcChannels.MaximizeOrUnmaximize, () => { if (!win) return win.isMaximized() ? win.unmaximize() : win.maximize() }) - ipcMain.on(IpcChannels.Close, () => { + on(IpcChannels.Close, () => { app.exit() }) } @@ -28,7 +35,7 @@ export function initIpcMain(win: BrowserWindow | null) { /** * 清除API缓存 */ -ipcMain.on(IpcChannels.ClearAPICache, () => { +on(IpcChannels.ClearAPICache, () => { db.truncate(Tables.Track) db.truncate(Tables.Album) db.truncate(Tables.Artist) @@ -42,7 +49,7 @@ ipcMain.on(IpcChannels.ClearAPICache, () => { /** * Get API cache */ -ipcMain.on(IpcChannels.GetApiCacheSync, (event, args) => { +on(IpcChannels.GetApiCacheSync, (event, args) => { const { api, query } = args const data = cache.get(api, query) event.returnValue = data @@ -51,8 +58,8 @@ ipcMain.on(IpcChannels.GetApiCacheSync, (event, args) => { /** * 缓存封面颜色 */ -ipcMain.on(IpcChannels.CacheCoverColor, (event, args) => { - const { id, color } = args.query +on(IpcChannels.CacheCoverColor, (event, args) => { + const { id, color } = args cache.set(APIs.CoverColor, { id, color }) }) @@ -60,7 +67,7 @@ ipcMain.on(IpcChannels.CacheCoverColor, (event, args) => { * 导出tables到json文件,方便查看table大小(dev环境) */ if (process.env.NODE_ENV === 'development') { - ipcMain.on(IpcChannels.DevDbExportJson, () => { + on(IpcChannels.DevDbExportJson, () => { const tables = [ Tables.ArtistAlbum, Tables.Playlist, diff --git a/src/main/rendererPreload.ts b/src/main/rendererPreload.ts index b47ec30..1fca785 100644 --- a/src/main/rendererPreload.ts +++ b/src/main/rendererPreload.ts @@ -1,4 +1,5 @@ -import { IpcChannels } from '@/main/IpcChannelsName' +/* eslint-disable @typescript-eslint/no-var-requires */ +import { IpcChannels } from '@/shared/IpcChannels' const { contextBridge, ipcRenderer } = require('electron') const log = require('electron-log') diff --git a/src/main/server.ts b/src/main/server.ts index ce888d8..07a1c69 100644 --- a/src/main/server.ts +++ b/src/main/server.ts @@ -6,92 +6,119 @@ import cache from './cache' import fileUpload from 'express-fileupload' import path from 'path' -log.info('[server] starting http server') - const isDev = process.env.NODE_ENV === 'development' const isProd = process.env.NODE_ENV === 'production' -// eslint-disable-next-line @typescript-eslint/no-var-requires -const neteaseApi = require('NeteaseCloudMusicApi') as (params: any) => any[] +class Server { + port = Number( + isProd + ? process.env.ELECTRON_WEB_SERVER_PORT ?? 42710 + : process.env.ELECTRON_DEV_NETEASE_API_PORT ?? 3000 + ) + app = express() -const app = express() -app.use(cookieParser()) -app.use(fileUpload()) + constructor() { + log.info('[server] starting http server') + this.app.use(cookieParser()) + this.app.use(fileUpload()) + this.neteaseHandler() + this.cacheAudioHandler() + this.serveStaticForProd() + this.listen() + } -Object.entries(neteaseApi).forEach(([name, handler]) => { - if (['serveNcmApi', 'getModulesDefinitions'].includes(name)) return + neteaseHandler() { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const neteaseApi = require('NeteaseCloudMusicApi') as (params: any) => any[] - name = pathCase(name) + Object.entries(neteaseApi).forEach(([name, handler]) => { + if (['serveNcmApi', 'getModulesDefinitions'].includes(name)) return - const wrappedHandler = async (req: Request, res: Response) => { - log.debug(`[server] Handling request: ${req.path}`) + name = pathCase(name) - // Get from cache - const cacheData = await cache.getForExpress(name, req) - if (cacheData) return res.json(cacheData) + const wrappedHandler = async (req: Request, res: Response) => { + log.debug(`[server] Handling request: ${req.path}`) - // Request netease api - try { - const result = await handler({ - ...req.query, - cookie: req.cookies, - }) + // Get from cache + const cacheData = await cache.getForExpress(name, req) + if (cacheData) return res.json(cacheData) - cache.set(name, result.body, req.query) - return res.send(result.body) - } catch (error) { - return res.status(500).send(error) + // Request netease api + try { + const result = await handler({ + ...req.query, + cookie: req.cookies, + }) + + cache.set(name, result.body, req.query) + return res.send(result.body) + } catch (error: any) { + if ([400, 301].includes(error.status)) { + return res.status(error.status).send(error.body) + } + return res.status(500).send(error) + } + } + + this.app.get(`/netease/${name}`, wrappedHandler) + this.app.post(`/netease/${name}`, wrappedHandler) + }) + } + + serveStaticForProd() { + if (isProd) { + this.app.use('/', express.static(path.join(__dirname, '../renderer/'))) } } - app.get(`/netease/${name}`, wrappedHandler) - app.post(`/netease/${name}`, wrappedHandler) -}) + cacheAudioHandler() { + this.app.get( + '/yesplaymusic/audio/:filename', + async (req: Request, res: Response) => { + cache.getAudio(req.params.filename, res) + } + ) + this.app.post( + '/yesplaymusic/audio/:id', + async (req: Request, res: Response) => { + const id = Number(req.params.id) + const { url } = req.query + if (isNaN(id)) { + return res.status(400).send({ error: 'Invalid param id' }) + } + if (!url) { + return res.status(400).send({ error: 'Invalid query url' }) + } -// Cache audio -app.get( - '/yesplaymusic/audio/:filename', - async (req: Request, res: Response) => { - cache.getAudio(req.params.filename, res) - } -) -app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => { - const id = Number(req.params.id) - const { url } = req.query - if (isNaN(id)) { - return res.status(400).send({ error: 'Invalid param id' }) - } - if (!url) { - return res.status(400).send({ error: 'Invalid query url' }) + if ( + !req.files || + Object.keys(req.files).length === 0 || + !req.files.file + ) { + return res.status(400).send('No audio were uploaded.') + } + if ('length' in req.files.file) { + return res.status(400).send('Only can upload one audio at a time.') + } + + try { + await cache.setAudio(req.files.file.data, { + id: id, + source: 'netease', + }) + res.status(200).send('Audio cached!') + } catch (error) { + res.status(500).send({ error }) + } + } + ) } - if (!req.files || Object.keys(req.files).length === 0 || !req.files.file) { - return res.status(400).send('No audio were uploaded.') - } - if ('length' in req.files.file) { - return res.status(400).send('Only can upload one audio at a time.') - } - - try { - await cache.setAudio(req.files.file.data, { - id: id, - source: 'netease', + listen() { + this.app.listen(this.port, () => { + log.info(`[server] API server listening on port ${this.port}`) }) - res.status(200).send('Audio cached!') - } catch (error) { - res.status(500).send({ error }) } -}) - -if (isProd) { - app.use('/', express.static(path.join(__dirname, '../renderer/'))) } -const port = Number( - isProd - ? process.env.ELECTRON_WEB_SERVER_PORT ?? 42710 - : process.env.ELECTRON_DEV_NETEASE_API_PORT ?? 3000 -) -app.listen(port, () => { - log.info(`[server] API server listening on port ${port}`) -}) +export default new Server() diff --git a/src/main/tsconfig.json b/src/main/tsconfig.json index 40c14c6..d3b90a0 100644 --- a/src/main/tsconfig.json +++ b/src/main/tsconfig.json @@ -15,5 +15,5 @@ "@/*": ["../*"] } }, - "include": ["./**/*.ts"] + "include": ["./**/*.ts", "../shared/**/*.ts"] } diff --git a/src/renderer/api/album.ts b/src/renderer/api/album.ts index ba6efbe..e2ce0d4 100644 --- a/src/renderer/api/album.ts +++ b/src/renderer/api/album.ts @@ -1,20 +1,12 @@ import request from '@/renderer/utils/request' - -export enum AlbumApiNames { - FETCH_ALBUM = 'fetchAlbum', -} +import { + FetchAlbumParams, + FetchAlbumResponse, + LikeAAlbumParams, + LikeAAlbumResponse, +} from '@/shared/api/Album' // 专辑详情 -export interface FetchAlbumParams { - id: number -} -export interface FetchAlbumResponse { - code: number - resourceState: boolean - album: Album - songs: Track[] - description: string -} export function fetchAlbum( params: FetchAlbumParams, noCache: boolean @@ -28,13 +20,6 @@ export function fetchAlbum( }) } -export interface LikeAAlbumParams { - t: 1 | 2 - id: number -} -export interface LikeAAlbumResponse { - code: number -} export function likeAAlbum( params: LikeAAlbumParams ): Promise { diff --git a/src/renderer/api/artist.ts b/src/renderer/api/artist.ts index cb78a44..d6e9279 100644 --- a/src/renderer/api/artist.ts +++ b/src/renderer/api/artist.ts @@ -1,20 +1,12 @@ import request from '@/renderer/utils/request' - -export enum ArtistApiNames { - FETCH_ARTIST = 'fetchArtist', - FETCH_ARTIST_ALBUMS = 'fetchArtistAlbums', -} +import { + FetchArtistParams, + FetchArtistResponse, + FetchArtistAlbumsParams, + FetchArtistAlbumsResponse, +} from '@/shared/api/Artist' // 歌手详情 -export interface FetchArtistParams { - id: number -} -export interface FetchArtistResponse { - code: number - more: boolean - artist: Artist - hotSongs: Track[] -} export function fetchArtist( params: FetchArtistParams, noCache: boolean @@ -29,17 +21,6 @@ export function fetchArtist( } // 获取歌手的专辑列表 -export interface FetchArtistAlbumsParams { - id: number - limit?: number // default: 50 - offset?: number // default: 0 -} -export interface FetchArtistAlbumsResponse { - code: number - hotAlbums: Album[] - more: boolean - artist: Artist -} export function fetchArtistAlbums( params: FetchArtistAlbumsParams ): Promise { diff --git a/src/renderer/api/auth.ts b/src/renderer/api/auth.ts index f7a1e4f..6cab6d4 100644 --- a/src/renderer/api/auth.ts +++ b/src/renderer/api/auth.ts @@ -1,5 +1,5 @@ -import type { fetchUserAccountResponse } from '@/renderer/api/user' import request from '@/renderer/utils/request' +import { FetchUserAccountResponse } from '@/shared/api/User' // 手机号登录 interface LoginWithPhoneParams { @@ -30,7 +30,7 @@ export interface LoginWithEmailParams { password?: string md5_password?: string } -export interface loginWithEmailResponse extends fetchUserAccountResponse { +export interface loginWithEmailResponse extends FetchUserAccountResponse { code: number cookie: string loginType: number diff --git a/src/renderer/api/playlist.ts b/src/renderer/api/playlist.ts index 1804daa..ad634d2 100644 --- a/src/renderer/api/playlist.ts +++ b/src/renderer/api/playlist.ts @@ -1,26 +1,15 @@ import request from '@/renderer/utils/request' - -export enum PlaylistApiNames { - FETCH_PLAYLIST = 'fetchPlaylist', - FETCH_RECOMMENDED_PLAYLISTS = 'fetchRecommendedPlaylists', - FETCH_DAILY_RECOMMEND_PLAYLISTS = 'fetchDailyRecommendPlaylists', - LIKE_A_PLAYLIST = 'likeAPlaylist', -} +import { + FetchPlaylistParams, + FetchPlaylistResponse, + FetchRecommendedPlaylistsParams, + FetchRecommendedPlaylistsResponse, + FetchDailyRecommendPlaylistsResponse, + LikeAPlaylistParams, + LikeAPlaylistResponse, +} from '@/shared/api/Playlists' // 歌单详情 -export interface FetchPlaylistParams { - id: number - s?: number // 歌单最近的 s 个收藏者 -} -export interface FetchPlaylistResponse { - code: number - playlist: Playlist - privileges: unknown // TODO: unknown type - relatedVideos: null - resEntrance: null - sharedPrivilege: null - urls: null -} export function fetchPlaylist( params: FetchPlaylistParams, noCache: boolean @@ -39,15 +28,6 @@ export function fetchPlaylist( } // 推荐歌单 -interface FetchRecommendedPlaylistsParams { - limit?: number -} -export interface FetchRecommendedPlaylistsResponse { - code: number - category: number - hasTaste: boolean - result: Playlist[] -} export function fetchRecommendedPlaylists( params: FetchRecommendedPlaylistsParams ): Promise { @@ -59,12 +39,7 @@ export function fetchRecommendedPlaylists( } // 每日推荐歌单(需要登录) -export interface FetchDailyRecommendPlaylistsResponse { - code: number - featureFirst: boolean - haveRcmdSongs: boolean - recommend: Playlist[] -} + export function fetchDailyRecommendPlaylists(): Promise { return request({ url: '/recommend/resource', @@ -72,13 +47,6 @@ export function fetchDailyRecommendPlaylists(): Promise { diff --git a/src/renderer/api/search.ts b/src/renderer/api/search.ts index 7b9fdc6..10aeb98 100644 --- a/src/renderer/api/search.ts +++ b/src/renderer/api/search.ts @@ -1,72 +1,13 @@ import request from '@/renderer/utils/request' - -export enum SearchApiNames { - SEARCH = 'search', - MULTI_MATCH_SEARCH = 'multiMatchSearch', -} +import { + SearchParams, + SearchResponse, + SearchTypes, + MultiMatchSearchParams, + MultiMatchSearchResponse, +} from '@/shared/api/Search' // 搜索 -export enum SearchTypes { - SINGLE = '1', - ALBUM = '10', - ARTIST = '100', - PLAYLIST = '1000', - USER = '1002', - MV = '1004', - LYRICS = '1006', - RADIO = '1009', - VIDEO = '1014', - ALL = '1018', -} -export interface SearchParams { - keywords: string - limit?: number // 返回数量 , 默认为 30 - offset?: number // 偏移数量,用于分页 , 如 : 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0 - type: keyof typeof SearchTypes // type: 搜索类型 -} -interface SearchResponse { - code: number - result: { - album: { - albums: Album[] - more: boolean - moreText: string - resourceIds: number[] - } - artist: { - artists: Artist[] - more: boolean - moreText: string - resourceIds: number[] - } - playList: { - playLists: Playlist[] - more: boolean - moreText: string - resourceIds: number[] - } - song: { - songs: Track[] - more: boolean - moreText: string - resourceIds: number[] - } - user: { - users: User[] - more: boolean - moreText: string - resourceIds: number[] - } - circle: unknown - new_mlog: unknown - order: string[] - rec_type: null - rec_query: null[] - sim_query: unknown - voice: unknown - voiceList: unknown - } -} export function search(params: SearchParams): Promise { return request({ url: '/search', @@ -79,19 +20,6 @@ export function search(params: SearchParams): Promise { } // 搜索多重匹配 -export interface MultiMatchSearchParams { - keywords: string -} -interface MultiMatchSearchResponse { - code: number - result: { - album: Album[] - artist: Artist[] - playlist: Playlist[] - orpheus: unknown - orders: Array<'artist' | 'album'> - } -} export function multiMatchSearch( params: MultiMatchSearchParams ): Promise { diff --git a/src/renderer/api/track.ts b/src/renderer/api/track.ts index d464fe5..1495d7a 100644 --- a/src/renderer/api/track.ts +++ b/src/renderer/api/track.ts @@ -1,22 +1,16 @@ import request from '@/renderer/utils/request' - -export enum TrackApiNames { - FETCH_TRACKS = 'fetchTracks', - FETCH_AUDIO_SOURCE = 'fetchAudioSource', - FETCH_LYRIC = 'fetchLyric', -} +import { + FetchAudioSourceParams, + FetchAudioSourceResponse, + FetchLyricParams, + FetchLyricResponse, + FetchTracksParams, + FetchTracksResponse, + LikeATrackParams, + LikeATrackResponse, +} from '@/shared/api/Track' // 获取歌曲详情 -export interface FetchTracksParams { - ids: number[] -} -export interface FetchTracksResponse { - code: number - songs: Track[] - privileges: { - [key: string]: unknown - } -} export function fetchTracks( params: FetchTracksParams ): Promise { @@ -30,39 +24,6 @@ export function fetchTracks( } // 获取音源URL -export interface FetchAudioSourceParams { - id: number - br?: number // bitrate, default 999000,320000 = 320kbps -} -export interface FetchAudioSourceResponse { - code: number - data: { - br: number - canExtend: boolean - code: number - encodeType: 'mp3' | null - expi: number - fee: number - flag: number - freeTimeTrialPrivilege: { - [key: string]: unknown - } - freeTrialPrivilege: { - [key: string]: unknown - } - freeTrialInfo: null - gain: number - id: number - level: 'standard' | 'null' - md5: string | null - payed: number - size: number - type: 'mp3' | null - uf: null - url: string | null - urlSource: number - }[] -} export function fetchAudioSource( params: FetchAudioSourceParams ): Promise { @@ -74,43 +35,6 @@ export function fetchAudioSource( } // 获取歌词 -export interface FetchLyricParams { - id: number -} -export interface FetchLyricResponse { - code: number - sgc: boolean - sfy: boolean - qfy: boolean - lyricUser?: { - id: number - status: number - demand: number - userid: number - nickname: string - uptime: number - } - transUser?: { - id: number - status: number - demand: number - userid: number - nickname: string - uptime: number - } - lrc: { - version: number - lyric: string - } - klyric?: { - version: number - lyric: string - } - tlyric?: { - version: number - lyric: string - } -} export function fetchLyric( params: FetchLyricParams ): Promise { @@ -121,15 +45,7 @@ export function fetchLyric( }) } -export interface LikeATrackParams { - id: number - like: boolean -} -export interface LikeATrackResponse { - code: number - playlistId: number - songs: Track[] -} +// 收藏歌曲 export function likeATrack( params: LikeATrackParams ): Promise { diff --git a/src/renderer/api/user.ts b/src/renderer/api/user.ts index 8f909f0..54a002e 100644 --- a/src/renderer/api/user.ts +++ b/src/renderer/api/user.ts @@ -1,12 +1,14 @@ import request from '@/renderer/utils/request' - -export enum UserApiNames { - FETCH_USER_ACCOUNT = 'fetchUserAccount', - FETCH_USER_LIKED_TRACKS_IDS = 'fetchUserLikedTracksIDs', - FETCH_USER_PLAYLISTS = 'fetchUserPlaylists', - FETCH_USER_ALBUMS = 'fetchUserAlbums', - FETCH_USER_ARTIST = 'fetchUserArtists', -} +import { + FetchUserAccountResponse, + FetchUserPlaylistsParams, + FetchUserPlaylistsResponse, + FetchUserLikedTracksIDsParams, + FetchUserLikedTracksIDsResponse, + FetchUserAlbumsParams, + FetchUserAlbumsResponse, + FetchUserArtistsResponse, +} from '@/shared/api/User' /** * 获取用户详情 @@ -26,64 +28,7 @@ export function userDetail(uid: number) { } // 获取账号详情 -export interface fetchUserAccountResponse { - code: number - account: { - anonimousUser: boolean - ban: number - baoyueVersion: number - createTime: number - donateVersion: number - id: number - paidFee: boolean - status: number - tokenVersion: number - type: number - userName: string - vipType: number - whitelistAuthority: number - } | null - profile: { - userId: number - userType: number - nickname: string - avatarImgId: number - avatarUrl: string - backgroundImgId: number - backgroundUrl: string - signature: string - createTime: number - userName: string - accountType: number - shortUserName: string - birthday: number - authority: number - gender: number - accountStatus: number - province: number - city: number - authStatus: number - description: string | null - detailDescription: string | null - defaultAvatar: boolean - expertTags: [] | null - experts: [] | null - djStatus: number - locationStatus: number - vipType: number - followed: boolean - mutual: boolean - authenticated: boolean - lastLoginTime: number - lastLoginIP: string - remarkName: string | null - viptypeVersion: number - authenticationTypes: number - avatarDetail: string | null - anchor: boolean - } | null -} -export function fetchUserAccount(): Promise { +export function fetchUserAccount(): Promise { return request({ url: '/user/account', method: 'get', @@ -94,17 +39,6 @@ export function fetchUserAccount(): Promise { } // 获取用户歌单 -export interface FetchUserPlaylistsParams { - uid: number - offset: number - limit?: number // default 30 -} -export interface FetchUserPlaylistsResponse { - code: number - more: boolean - version: string - playlist: Playlist[] -} export function fetchUserPlaylists( params: FetchUserPlaylistsParams ): Promise { @@ -115,14 +49,6 @@ export function fetchUserPlaylists( }) } -export interface FetchUserLikedTracksIDsParams { - uid: number -} -export interface FetchUserLikedTracksIDsResponse { - code: number - checkPoint: number - ids: number[] -} export function fetchUserLikedTracksIDs( params: FetchUserLikedTracksIDsParams ): Promise { @@ -153,17 +79,6 @@ export function dailySignin(type = 0) { }) } -export interface FetchUserAlbumsParams { - offset?: number // default 0 - limit?: number // default 25 -} -export interface FetchUserAlbumsResponse { - code: number - hasMore: boolean - paidCount: number - count: number - data: Album[] -} export function fetchUserAlbums( params: FetchUserAlbumsParams ): Promise { @@ -178,12 +93,6 @@ export function fetchUserAlbums( } // 获取收藏的歌手 -export interface FetchUserArtistsResponse { - code: number - hasMore: boolean - count: number - data: Artist[] -} export function fetchUserArtists(): Promise { return request({ url: '/artist/sublist', diff --git a/src/renderer/components/TitleBar.tsx b/src/renderer/components/TitleBar.tsx index 4f4df86..b98f4d1 100644 --- a/src/renderer/components/TitleBar.tsx +++ b/src/renderer/components/TitleBar.tsx @@ -1,6 +1,6 @@ import { player } from '@/renderer/store' import SvgIcon from './SvgIcon' -import { IpcChannels } from '@/main/IpcChannelsName' +import { IpcChannels } from '@/shared/IpcChannels' const Controls = () => { const [isMaximized, setIsMaximized] = useState(false) diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts index b26b642..bd31a40 100644 --- a/src/renderer/global.d.ts +++ b/src/renderer/global.d.ts @@ -1,17 +1,25 @@ -import { IpcChannels } from '@/main/IpcChannelsName' +import { IpcChannelsParams, IpcChannelsReturns } from '@/shared/IpcChannels' import { ElectronLog } from 'electron-log' export {} declare global { interface Window { - // Expose some Api through preload script ipcRenderer?: { - sendSync: (channel: IpcChannels, ...args: any[]) => any - send: (channel: IpcChannels, ...args: any[]) => void - on: ( - channel: IpcChannels, - listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void + sendSync: ( + channel: T, + params?: IpcChannelsParams[T] + ) => IpcChannelsReturns[T] + send: ( + channel: T, + params?: IpcChannelsParams[T] + ) => void + on: ( + channel: T, + listener: ( + event: Electron.IpcRendererEvent, + value: IpcChannelsReturns[T] + ) => void ) => void } env?: { diff --git a/src/renderer/hooks/useAlbum.ts b/src/renderer/hooks/useAlbum.ts index 46c3498..9cce5c0 100644 --- a/src/renderer/hooks/useAlbum.ts +++ b/src/renderer/hooks/useAlbum.ts @@ -1,8 +1,12 @@ import { fetchAlbum } from '@/renderer/api/album' -import { AlbumApiNames } from '@/renderer/api/album' -import type { FetchAlbumParams, FetchAlbumResponse } from '@/renderer/api/album' import reactQueryClient from '@/renderer/utils/reactQueryClient' -import { IpcChannels } from '@/main/IpcChannelsName' +import { IpcChannels } from '@/shared/IpcChannels' +import { APIs } from '@/shared/CacheAPIs' +import { + FetchAlbumParams, + AlbumApiNames, + FetchAlbumResponse, +} from '@/shared/api/Album' const fetch = async (params: FetchAlbumParams, noCache?: boolean) => { const album = await fetchAlbum(params, !!noCache) @@ -21,7 +25,7 @@ export default function useAlbum(params: FetchAlbumParams, noCache?: boolean) { staleTime: 24 * 60 * 60 * 1000, // 24 hours placeholderData: (): FetchAlbumResponse => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'album', + api: APIs.Album, query: { id: params.id, }, diff --git a/src/renderer/hooks/useArtist.ts b/src/renderer/hooks/useArtist.ts index dd84213..3a839f2 100644 --- a/src/renderer/hooks/useArtist.ts +++ b/src/renderer/hooks/useArtist.ts @@ -1,10 +1,11 @@ import { fetchArtist } from '@/renderer/api/artist' -import { ArtistApiNames } from '@/renderer/api/artist' -import type { +import { IpcChannels } from '@/shared/IpcChannels' +import { APIs } from '@/shared/CacheAPIs' +import { FetchArtistParams, + ArtistApiNames, FetchArtistResponse, -} from '@/renderer/api/artist' -import { IpcChannels } from '@/main/IpcChannelsName' +} from '@/shared/api/Artist' export default function useArtist( params: FetchArtistParams, @@ -18,7 +19,7 @@ export default function useArtist( staleTime: 5 * 60 * 1000, // 5 mins placeholderData: (): FetchArtistResponse => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'artists', + api: APIs.Artist, query: { id: params.id, }, diff --git a/src/renderer/hooks/useArtistAlbums.ts b/src/renderer/hooks/useArtistAlbums.ts index 4a9fa6d..9a8bb53 100644 --- a/src/renderer/hooks/useArtistAlbums.ts +++ b/src/renderer/hooks/useArtistAlbums.ts @@ -1,10 +1,11 @@ import { fetchArtistAlbums } from '@/renderer/api/artist' -import { ArtistApiNames } from '@/renderer/api/artist' -import type { +import { IpcChannels } from '@/shared/IpcChannels' +import { APIs } from '@/shared/CacheAPIs' +import { FetchArtistAlbumsParams, + ArtistApiNames, FetchArtistAlbumsResponse, -} from '@/renderer/api/artist' -import { IpcChannels } from '@/main/IpcChannelsName' +} from '@/shared/api/Artist' export default function useUserAlbums(params: FetchArtistAlbumsParams) { return useQuery( @@ -18,7 +19,7 @@ export default function useUserAlbums(params: FetchArtistAlbumsParams) { staleTime: 3600000, placeholderData: (): FetchArtistAlbumsResponse => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'artist/album', + api: APIs.ArtistAlbum, query: { id: params.id, }, diff --git a/src/renderer/hooks/useLyric.ts b/src/renderer/hooks/useLyric.ts index a9879c7..2071770 100644 --- a/src/renderer/hooks/useLyric.ts +++ b/src/renderer/hooks/useLyric.ts @@ -1,7 +1,12 @@ -import { TrackApiNames, fetchLyric } from '@/renderer/api/track' -import type { FetchLyricParams, FetchLyricResponse } from '@/renderer/api/track' +import { fetchLyric } from '@/renderer/api/track' import reactQueryClient from '@/renderer/utils/reactQueryClient' -import { IpcChannels } from '@/main/IpcChannelsName' +import { + FetchLyricParams, + FetchLyricResponse, + TrackApiNames, +} from '@/shared/api/Track' +import { APIs } from '@/shared/CacheAPIs' +import { IpcChannels } from '@/shared/IpcChannels' export default function useLyric(params: FetchLyricParams) { return useQuery( @@ -15,7 +20,7 @@ export default function useLyric(params: FetchLyricParams) { staleTime: Infinity, initialData: (): FetchLyricResponse | undefined => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'lyric', + api: APIs.Lyric, query: { id: params.id, }, diff --git a/src/renderer/hooks/usePlaylist.ts b/src/renderer/hooks/usePlaylist.ts index 25c034c..15fe7b7 100644 --- a/src/renderer/hooks/usePlaylist.ts +++ b/src/renderer/hooks/usePlaylist.ts @@ -1,11 +1,12 @@ import { fetchPlaylist } from '@/renderer/api/playlist' -import { PlaylistApiNames } from '@/renderer/api/playlist' -import type { - FetchPlaylistParams, - FetchPlaylistResponse, -} from '@/renderer/api/playlist' import reactQueryClient from '@/renderer/utils/reactQueryClient' -import { IpcChannels } from '@/main/IpcChannelsName' +import { IpcChannels } from '@/shared/IpcChannels' +import { APIs } from '@/shared/CacheAPIs' +import { + FetchPlaylistParams, + PlaylistApiNames, + FetchPlaylistResponse, +} from '@/shared/api/Playlists' const fetch = (params: FetchPlaylistParams, noCache?: boolean) => { return fetchPlaylist(params, !!noCache) @@ -23,7 +24,7 @@ export default function usePlaylist( refetchOnWindowFocus: true, placeholderData: (): FetchPlaylistResponse | undefined => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'playlist/detail', + api: APIs.Playlist, query: { id: params.id, }, diff --git a/src/renderer/hooks/useTracks.ts b/src/renderer/hooks/useTracks.ts index d454a18..5404eca 100644 --- a/src/renderer/hooks/useTracks.ts +++ b/src/renderer/hooks/useTracks.ts @@ -1,15 +1,14 @@ +import { fetchAudioSource, fetchTracks } from '@/renderer/api/track' +import type {} from '@/renderer/api/track' +import reactQueryClient from '@/renderer/utils/reactQueryClient' +import { IpcChannels } from '@/shared/IpcChannels' import { - TrackApiNames, - fetchAudioSource, - fetchTracks, -} from '@/renderer/api/track' -import type { FetchAudioSourceParams, FetchTracksParams, FetchTracksResponse, -} from '@/renderer/api/track' -import reactQueryClient from '@/renderer/utils/reactQueryClient' -import { IpcChannels } from '@/main/IpcChannelsName' + TrackApiNames, +} from '@/shared/api/Track' +import { APIs } from '@/shared/CacheAPIs' export default function useTracks(params: FetchTracksParams) { return useQuery( @@ -23,7 +22,7 @@ export default function useTracks(params: FetchTracksParams) { staleTime: Infinity, initialData: (): FetchTracksResponse | undefined => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'song/detail', + api: APIs.Track, query: { ids: params.ids.join(','), }, diff --git a/src/renderer/hooks/useTracksInfinite.ts b/src/renderer/hooks/useTracksInfinite.ts index 74414ad..df49c5d 100644 --- a/src/renderer/hooks/useTracksInfinite.ts +++ b/src/renderer/hooks/useTracksInfinite.ts @@ -1,5 +1,5 @@ -import { TrackApiNames, fetchTracks } from '@/renderer/api/track' -import type { FetchTracksParams } from '@/renderer/api/track' +import { FetchTracksParams, TrackApiNames } from '@/shared/api/Track' +import { fetchTracks } from 'api/track' // 100 tracks each page const offset = 100 diff --git a/src/renderer/hooks/useUser.ts b/src/renderer/hooks/useUser.ts index c02e462..2bf31ef 100644 --- a/src/renderer/hooks/useUser.ts +++ b/src/renderer/hooks/useUser.ts @@ -1,13 +1,14 @@ -import { fetchUserAccount, fetchUserAccountResponse } from '@/renderer/api/user' -import { UserApiNames } from '@/renderer/api/user' -import { IpcChannels } from '@/main/IpcChannelsName' +import { fetchUserAccount } from '@/renderer/api/user' +import { UserApiNames, FetchUserAccountResponse } from '@/shared/api/User' +import { APIs } from '@/shared/CacheAPIs' +import { IpcChannels } from '@/shared/IpcChannels' export default function useUser() { return useQuery(UserApiNames.FETCH_USER_ACCOUNT, fetchUserAccount, { refetchOnWindowFocus: true, - placeholderData: (): fetchUserAccountResponse | undefined => + placeholderData: (): FetchUserAccountResponse | undefined => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'user/account', + api: APIs.UserAccount, }), }) } diff --git a/src/renderer/hooks/useUserAlbums.ts b/src/renderer/hooks/useUserAlbums.ts index c83e5ab..5d0af28 100644 --- a/src/renderer/hooks/useUserAlbums.ts +++ b/src/renderer/hooks/useUserAlbums.ts @@ -1,12 +1,14 @@ import { likeAAlbum } from '@/renderer/api/album' -import type { - FetchUserAlbumsParams, - FetchUserAlbumsResponse, -} from '@/renderer/api/user' -import { UserApiNames, fetchUserAlbums } from '@/renderer/api/user' import { useQueryClient } from 'react-query' import useUser from './useUser' -import { IpcChannels } from '@/main/IpcChannelsName' +import { IpcChannels } from '@/shared/IpcChannels' +import { APIs } from '@/shared/CacheAPIs' +import { + FetchUserAlbumsParams, + UserApiNames, + FetchUserAlbumsResponse, +} from '@/shared/api/User' +import { fetchUserAlbums } from 'api/user' export default function useUserAlbums(params: FetchUserAlbumsParams = {}) { const { data: user } = useUser() @@ -17,7 +19,7 @@ export default function useUserAlbums(params: FetchUserAlbumsParams = {}) { refetchOnWindowFocus: true, placeholderData: (): FetchUserAlbumsResponse | undefined => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'album/sublist', + api: APIs.UserAlbums, query: params, }), } diff --git a/src/renderer/hooks/useUserArtists.ts b/src/renderer/hooks/useUserArtists.ts index 17c56ff..a4060b8 100644 --- a/src/renderer/hooks/useUserArtists.ts +++ b/src/renderer/hooks/useUserArtists.ts @@ -1,13 +1,14 @@ -import type { FetchUserArtistsResponse } from '@/renderer/api/user' -import { UserApiNames, fetchUserArtists } from '@/renderer/api/user' -import { IpcChannels } from '@/main/IpcChannelsName' +import { fetchUserArtists } from '@/renderer/api/user' +import { UserApiNames, FetchUserArtistsResponse } from '@/shared/api/User' +import { APIs } from '@/shared/CacheAPIs' +import { IpcChannels } from '@/shared/IpcChannels' export default function useUserArtists() { return useQuery([UserApiNames.FETCH_USER_ARTIST], fetchUserArtists, { refetchOnWindowFocus: true, placeholderData: (): FetchUserArtistsResponse => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'album/sublist', + api: APIs.UserArtists, }), }) } diff --git a/src/renderer/hooks/useUserLikedTracksIDs.ts b/src/renderer/hooks/useUserLikedTracksIDs.ts index ef4bb90..c3c757d 100644 --- a/src/renderer/hooks/useUserLikedTracksIDs.ts +++ b/src/renderer/hooks/useUserLikedTracksIDs.ts @@ -1,9 +1,13 @@ -import type { FetchUserLikedTracksIDsResponse } from '@/renderer/api/user' -import { UserApiNames, fetchUserLikedTracksIDs } from '@/renderer/api/user' import { likeATrack } from '@/renderer/api/track' import useUser from './useUser' import { useQueryClient } from 'react-query' -import { IpcChannels } from '@/main/IpcChannelsName' +import { IpcChannels } from '@/shared/IpcChannels' +import { APIs } from '@/shared/CacheAPIs' +import { fetchUserLikedTracksIDs } from 'api/user' +import { + FetchUserLikedTracksIDsResponse, + UserApiNames, +} from '@/shared/api/User' export default function useUserLikedTracksIDs() { const { data: user } = useUser() @@ -17,7 +21,7 @@ export default function useUserLikedTracksIDs() { refetchOnWindowFocus: true, placeholderData: (): FetchUserLikedTracksIDsResponse | undefined => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'likelist', + api: APIs.Likelist, query: { uid, }, diff --git a/src/renderer/hooks/useUserPlaylists.ts b/src/renderer/hooks/useUserPlaylists.ts index 20089a9..ff2d8db 100644 --- a/src/renderer/hooks/useUserPlaylists.ts +++ b/src/renderer/hooks/useUserPlaylists.ts @@ -1,9 +1,10 @@ import { likeAPlaylist } from '@/renderer/api/playlist' -import type { FetchUserPlaylistsResponse } from '@/renderer/api/user' -import { UserApiNames, fetchUserPlaylists } from '@/renderer/api/user' import { useQueryClient } from 'react-query' import useUser from './useUser' -import { IpcChannels } from '@/main/IpcChannelsName' +import { IpcChannels } from '@/shared/IpcChannels' +import { APIs } from '@/shared/CacheAPIs' +import { fetchUserPlaylists } from 'api/user' +import { FetchUserPlaylistsResponse, UserApiNames } from '@/shared/api/User' export default function useUserPlaylists() { const { data: user } = useUser() @@ -33,7 +34,7 @@ export default function useUserPlaylists() { refetchOnWindowFocus: true, placeholderData: (): FetchUserPlaylistsResponse => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'user/playlist', + api: APIs.UserPlaylist, query: { uid: params.uid, }, diff --git a/src/renderer/pages/Home.tsx b/src/renderer/pages/Home.tsx index bc0c519..937c012 100644 --- a/src/renderer/pages/Home.tsx +++ b/src/renderer/pages/Home.tsx @@ -1,12 +1,13 @@ import { - PlaylistApiNames, fetchRecommendedPlaylists, fetchDailyRecommendPlaylists, } from '@/renderer/api/playlist' import CoverRow from '@/renderer/components/CoverRow' import DailyTracksCard from '@/renderer/components/DailyTracksCard' import FMCard from '@/renderer/components/FMCard' -import { IpcChannels } from '@/main/IpcChannelsName' +import { PlaylistApiNames } from '@/shared/api/Playlists' +import { APIs } from '@/shared/CacheAPIs' +import { IpcChannels } from '@/shared/IpcChannels' export default function Home() { const { @@ -19,7 +20,7 @@ export default function Home() { retry: false, placeholderData: () => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'recommend/resource', + api: APIs.RecommendResource, }), } ) @@ -35,7 +36,7 @@ export default function Home() { { placeholderData: () => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { - api: 'personalized', + api: APIs.Personalized, }), } ) diff --git a/src/renderer/pages/Search/Search.tsx b/src/renderer/pages/Search/Search.tsx index 75a8af8..4258b5d 100644 --- a/src/renderer/pages/Search/Search.tsx +++ b/src/renderer/pages/Search/Search.tsx @@ -1,13 +1,9 @@ -import { - multiMatchSearch, - search, - SearchApiNames, - SearchTypes, -} from '@/renderer/api/search' +import { multiMatchSearch, search } from '@/renderer/api/search' import Cover from '@/renderer/components/Cover' import TrackGrid from '@/renderer/components/TracksGrid' import { player } from '@/renderer/store' import { resizeImage } from '@/renderer/utils/common' +import { SearchTypes, SearchApiNames } from '@/shared/api/Search' import dayjs from 'dayjs' const Artists = ({ artists }: { artists: Artist[] }) => { diff --git a/src/renderer/test/utils/common.test.ts b/src/renderer/test/utils/common.test.ts index e57ba24..9fec78d 100644 --- a/src/renderer/test/utils/common.test.ts +++ b/src/renderer/test/utils/common.test.ts @@ -8,8 +8,8 @@ import { getCoverColor, storage, } from '@/renderer/utils/common' -import { IpcChannels } from '@/main/IpcChannelsName' -import { APIs } from '@/main/CacheAPIsName' +import { IpcChannels } from '@/shared/IpcChannels' +import { APIs } from '@/shared/CacheAPIs' test('resizeImage', () => { expect(resizeImage('https://test.com/test.jpg', 'xs')).toBe( @@ -62,28 +62,48 @@ test('formatDuration', () => { expect(formatDuration(0, 'zh-CN', 'hh[hr] mm[min]')).toBe('0 分钟') }) -test('cacheCoverColor', () => { - vi.stubGlobal('ipcRenderer', { - send: (channel: IpcChannels, ...args: any[]) => { - expect(channel).toBe(IpcChannels.CacheCoverColor) - expect(args[0].api).toBe(APIs.CoverColor) - expect(args[0].query).toEqual({ - id: '109951165911363', - color: '#fff', - }) - }, - }) +describe('cacheCoverColor', () => { + test('cache with valid url', () => { + vi.stubGlobal('ipcRenderer', { + send: (channel: IpcChannels, ...args: any[]) => { + expect(channel).toBe(IpcChannels.CacheCoverColor) + expect(args[0].api).toBe(APIs.CoverColor) + expect(args[0].query).toEqual({ + id: '109951165911363', + color: '#fff', + }) + }, + }) - const sendSpy = vi.spyOn(window.ipcRenderer as any, 'send') - expect( + const sendSpy = vi.spyOn(window.ipcRenderer as any, 'send') cacheCoverColor( 'https://p2.music.126.net/2qW-OYZod7SgrzxTwtyBqA==/109951165911363.jpg?param=256y256', '#fff' ) - ) - expect(sendSpy).toHaveBeenCalledTimes(1) - vi.stubGlobal('ipcRenderer', undefined) + expect(sendSpy).toHaveBeenCalledTimes(1) + + vi.stubGlobal('ipcRenderer', undefined) + }) + + test('cache with invalid url', () => { + vi.stubGlobal('ipcRenderer', { + send: (channel: IpcChannels, ...args: any[]) => { + expect(channel).toBe(IpcChannels.CacheCoverColor) + expect(args[0].api).toBe(APIs.CoverColor) + expect(args[0].query).toEqual({ + id: '', + color: '#fff', + }) + }, + }) + + const sendSpy = vi.spyOn(window.ipcRenderer as any, 'send') + cacheCoverColor('not a valid url', '#fff') + expect(sendSpy).toHaveBeenCalledTimes(0) + + vi.stubGlobal('ipcRenderer', undefined) + }) }) test('calcCoverColor', async () => { @@ -117,7 +137,6 @@ test('calcCoverColor', async () => { ) ).toBe('#808080') - expect(sendSpy).toHaveBeenCalledTimes(1) vi.stubGlobal('ipcRenderer', undefined) }) @@ -174,6 +193,10 @@ describe('getCoverColor', () => { expect(sendSpy).toHaveBeenCalledTimes(1) vi.stubGlobal('ipcRenderer', undefined) }) + + test('invalid url', async () => { + expect(await getCoverColor('not a valid url')).toBe(undefined) + }) }) test('storage', () => { diff --git a/src/renderer/tsconfig.json b/src/renderer/tsconfig.json index f820578..6f1abe7 100644 --- a/src/renderer/tsconfig.json +++ b/src/renderer/tsconfig.json @@ -20,5 +20,5 @@ "@/*": ["../*"] } }, - "include": ["./**/*.ts", "./**/*.tsx"] + "include": ["./**/*.ts", "./**/*.tsx", "../shared/**/*.ts"] } diff --git a/src/renderer/utils/common.ts b/src/renderer/utils/common.ts index 2a08bfd..2079641 100644 --- a/src/renderer/utils/common.ts +++ b/src/renderer/utils/common.ts @@ -1,7 +1,7 @@ -import { IpcChannels } from '@/main/IpcChannelsName' +import { IpcChannels } from '@/shared/IpcChannels' import dayjs from 'dayjs' import duration from 'dayjs/plugin/duration' -import { APIs } from '@/main/CacheAPIsName' +import { APIs } from '@/shared/CacheAPIs' import { average } from 'color.js' import { colord } from 'colord' @@ -110,7 +110,13 @@ export function scrollToTop(smooth = false) { } export async function getCoverColor(coverUrl: string) { - const id = new URL(coverUrl).pathname.split('/').pop()?.split('.')[0] + let id: string | undefined + try { + id = new URL(coverUrl).pathname.split('/').pop()?.split('.')[0] + } catch (e) { + return + } + const colorFromCache = window.ipcRenderer?.sendSync( IpcChannels.GetApiCacheSync, { @@ -124,14 +130,18 @@ export async function getCoverColor(coverUrl: string) { } export async function cacheCoverColor(coverUrl: string, color: string) { - const id = new URL(coverUrl).pathname.split('/').pop()?.split('.')[0] + let id: string | undefined + try { + id = new URL(coverUrl).pathname.split('/').pop()?.split('.')[0] + } catch (e) { + return + } + + if (!id || isNaN(Number(id))) return window.ipcRenderer?.send(IpcChannels.CacheCoverColor, { - api: APIs.CoverColor, - query: { - id, - color, - }, + id: Number(id), + color, }) } diff --git a/src/renderer/utils/lyric.ts b/src/renderer/utils/lyric.ts index b29dc33..ac75680 100644 --- a/src/renderer/utils/lyric.ts +++ b/src/renderer/utils/lyric.ts @@ -1,4 +1,4 @@ -import { FetchLyricResponse } from '@/renderer/api/track' +import { FetchLyricResponse } from '@/shared/api/Track' export function lyricParser(lrc: FetchLyricResponse) { return { diff --git a/src/shared/CacheAPIs.ts b/src/shared/CacheAPIs.ts new file mode 100644 index 0000000..9b14e4c --- /dev/null +++ b/src/shared/CacheAPIs.ts @@ -0,0 +1,72 @@ +import { FetchArtistAlbumsResponse, FetchArtistResponse } from './api/Artist' +import { FetchAlbumResponse } from './api/Album' +import { + FetchUserAccountResponse, + FetchUserAlbumsResponse, + FetchUserArtistsResponse, + FetchUserLikedTracksIDsResponse, + FetchUserPlaylistsResponse, +} from './api/User' +import { + FetchAudioSourceResponse, + FetchLyricResponse, + FetchTracksResponse, +} from './api/Track' +import { + FetchPlaylistResponse, + FetchRecommendedPlaylistsResponse, +} from './api/Playlists' + +export const enum APIs { + Album = 'album', + Artist = 'artists', + ArtistAlbum = 'artist/album', + CoverColor = 'cover_color', + Likelist = 'likelist', + Lyric = 'lyric', + Personalized = 'personalized', + Playlist = 'playlist/detail', + RecommendResource = 'recommend/resource', + SongUrl = 'song/url', + Track = 'song/detail', + UserAccount = 'user/account', + UserAlbums = 'album/sublist', + UserArtists = 'artist/sublist', + UserPlaylist = 'user/playlist', +} + +export interface APIsParams { + [APIs.Album]: { id: number } + [APIs.Artist]: { id: number } + [APIs.ArtistAlbum]: { id: number } + [APIs.CoverColor]: { id: number } + [APIs.Likelist]: void + [APIs.Lyric]: { id: number } + [APIs.Personalized]: void + [APIs.Playlist]: { id: number } + [APIs.RecommendResource]: void + [APIs.SongUrl]: { id: string } + [APIs.Track]: { ids: string } + [APIs.UserAccount]: void + [APIs.UserAlbums]: void + [APIs.UserArtists]: void + [APIs.UserPlaylist]: void +} + +export interface APIsResponse { + [APIs.Album]: FetchAlbumResponse + [APIs.Artist]: FetchArtistResponse + [APIs.ArtistAlbum]: FetchArtistAlbumsResponse + [APIs.CoverColor]: string | undefined + [APIs.Likelist]: FetchUserLikedTracksIDsResponse + [APIs.Lyric]: FetchLyricResponse + [APIs.Personalized]: FetchRecommendedPlaylistsResponse + [APIs.Playlist]: FetchPlaylistResponse + [APIs.RecommendResource]: FetchRecommendedPlaylistsResponse + [APIs.SongUrl]: FetchAudioSourceResponse + [APIs.Track]: FetchTracksResponse + [APIs.UserAccount]: FetchUserAccountResponse + [APIs.UserAlbums]: FetchUserAlbumsResponse + [APIs.UserArtists]: FetchUserArtistsResponse + [APIs.UserPlaylist]: FetchUserPlaylistsResponse +} diff --git a/src/shared/IpcChannels.ts b/src/shared/IpcChannels.ts new file mode 100644 index 0000000..3708bca --- /dev/null +++ b/src/shared/IpcChannels.ts @@ -0,0 +1,40 @@ +import { APIs } from './CacheAPIs' + +export const enum IpcChannels { + ClearAPICache = 'clear-api-cache', + Minimize = 'minimize', + MaximizeOrUnmaximize = 'maximize-or-unmaximize', + Close = 'close', + IsMaximized = 'is-maximized', + GetApiCacheSync = 'get-api-cache-sync', + DevDbExportJson = 'dev-db-export-json', + CacheCoverColor = 'cache-cover-color', +} + +export interface IpcChannelsParams { + [IpcChannels.ClearAPICache]: void + [IpcChannels.Minimize]: void + [IpcChannels.MaximizeOrUnmaximize]: void + [IpcChannels.Close]: void + [IpcChannels.IsMaximized]: void + [IpcChannels.GetApiCacheSync]: { + api: APIs + query?: any + } + [IpcChannels.DevDbExportJson]: void + [IpcChannels.CacheCoverColor]: { + id: number + color: string + } +} + +export interface IpcChannelsReturns { + [IpcChannels.ClearAPICache]: void + [IpcChannels.Minimize]: void + [IpcChannels.MaximizeOrUnmaximize]: void + [IpcChannels.Close]: void + [IpcChannels.IsMaximized]: boolean + [IpcChannels.GetApiCacheSync]: any + [IpcChannels.DevDbExportJson]: void + [IpcChannels.CacheCoverColor]: void +} diff --git a/src/shared/api/Album.ts b/src/shared/api/Album.ts new file mode 100644 index 0000000..74156f4 --- /dev/null +++ b/src/shared/api/Album.ts @@ -0,0 +1,23 @@ +export enum AlbumApiNames { + FETCH_ALBUM = 'fetchAlbum', +} + +// 专辑详情 +export interface FetchAlbumParams { + id: number +} +export interface FetchAlbumResponse { + code: number + resourceState: boolean + album: Album + songs: Track[] + description: string +} + +export interface LikeAAlbumParams { + t: 1 | 2 + id: number +} +export interface LikeAAlbumResponse { + code: number +} diff --git a/src/shared/api/Artist.ts b/src/shared/api/Artist.ts new file mode 100644 index 0000000..28039e5 --- /dev/null +++ b/src/shared/api/Artist.ts @@ -0,0 +1,28 @@ +export enum ArtistApiNames { + FETCH_ARTIST = 'fetchArtist', + FETCH_ARTIST_ALBUMS = 'fetchArtistAlbums', +} + +// 歌手详情 +export interface FetchArtistParams { + id: number +} +export interface FetchArtistResponse { + code: number + more: boolean + artist: Artist + hotSongs: Track[] +} + +// 获取歌手的专辑列表 +export interface FetchArtistAlbumsParams { + id: number + limit?: number // default: 50 + offset?: number // default: 0 +} +export interface FetchArtistAlbumsResponse { + code: number + hotAlbums: Album[] + more: boolean + artist: Artist +} diff --git a/src/shared/api/Playlists.ts b/src/shared/api/Playlists.ts new file mode 100644 index 0000000..5740d5d --- /dev/null +++ b/src/shared/api/Playlists.ts @@ -0,0 +1,48 @@ +export enum PlaylistApiNames { + FETCH_PLAYLIST = 'fetchPlaylist', + FETCH_RECOMMENDED_PLAYLISTS = 'fetchRecommendedPlaylists', + FETCH_DAILY_RECOMMEND_PLAYLISTS = 'fetchDailyRecommendPlaylists', + LIKE_A_PLAYLIST = 'likeAPlaylist', +} + +// 歌单详情 +export interface FetchPlaylistParams { + id: number + s?: number // 歌单最近的 s 个收藏者 +} +export interface FetchPlaylistResponse { + code: number + playlist: Playlist + privileges: unknown // TODO: unknown type + relatedVideos: null + resEntrance: null + sharedPrivilege: null + urls: null +} + +// 推荐歌单 +export interface FetchRecommendedPlaylistsParams { + limit?: number +} +export interface FetchRecommendedPlaylistsResponse { + code: number + category: number + hasTaste: boolean + result: Playlist[] +} + +// 每日推荐歌单(需要登录) +export interface FetchDailyRecommendPlaylistsResponse { + code: number + featureFirst: boolean + haveRcmdSongs: boolean + recommend: Playlist[] +} + +export interface LikeAPlaylistParams { + t: 1 | 2 + id: number +} +export interface LikeAPlaylistResponse { + code: number +} diff --git a/src/shared/api/Search.ts b/src/shared/api/Search.ts new file mode 100644 index 0000000..0705cc3 --- /dev/null +++ b/src/shared/api/Search.ts @@ -0,0 +1,82 @@ +export enum SearchApiNames { + SEARCH = 'search', + MULTI_MATCH_SEARCH = 'multiMatchSearch', +} + +// 搜索 +export enum SearchTypes { + SINGLE = '1', + ALBUM = '10', + ARTIST = '100', + PLAYLIST = '1000', + USER = '1002', + MV = '1004', + LYRICS = '1006', + RADIO = '1009', + VIDEO = '1014', + ALL = '1018', +} +export interface SearchParams { + keywords: string + limit?: number // 返回数量 , 默认为 30 + offset?: number // 偏移数量,用于分页 , 如 : 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0 + type: keyof typeof SearchTypes // type: 搜索类型 +} +export interface SearchResponse { + code: number + result: { + album: { + albums: Album[] + more: boolean + moreText: string + resourceIds: number[] + } + artist: { + artists: Artist[] + more: boolean + moreText: string + resourceIds: number[] + } + playList: { + playLists: Playlist[] + more: boolean + moreText: string + resourceIds: number[] + } + song: { + songs: Track[] + more: boolean + moreText: string + resourceIds: number[] + } + user: { + users: User[] + more: boolean + moreText: string + resourceIds: number[] + } + circle: unknown + new_mlog: unknown + order: string[] + rec_type: null + rec_query: null[] + sim_query: unknown + voice: unknown + voiceList: unknown + } +} + +// 搜索多重匹配 +export interface MultiMatchSearchParams { + keywords: string +} +export interface MultiMatchSearchResponse { + code: number + result: { + album: Album[] + artist: Artist[] + playlist: Playlist[] + orpheus: unknown + orders: Array<'artist' | 'album'> + } +} diff --git a/src/shared/api/Track.ts b/src/shared/api/Track.ts new file mode 100644 index 0000000..6a082ea --- /dev/null +++ b/src/shared/api/Track.ts @@ -0,0 +1,104 @@ +export enum TrackApiNames { + FETCH_TRACKS = 'fetchTracks', + FETCH_AUDIO_SOURCE = 'fetchAudioSource', + FETCH_LYRIC = 'fetchLyric', +} + +// 获取歌曲详情 +export interface FetchTracksParams { + ids: number[] +} +export interface FetchTracksResponse { + code: number + songs: Track[] + privileges: { + [key: string]: unknown + } +} + +// 获取音源URL + +export interface FetchAudioSourceParams { + id: number + br?: number // bitrate, default 999000,320000 = 320kbps +} +export interface FetchAudioSourceResponse { + code: number + data: { + br: number + canExtend: boolean + code: number + encodeType: 'mp3' | null + expi: number + fee: number + flag: number + freeTimeTrialPrivilege: { + [key: string]: unknown + } + freeTrialPrivilege: { + [key: string]: unknown + } + freeTrialInfo: null + gain: number + id: number + level: 'standard' | 'null' + md5: string | null + payed: number + size: number + type: 'mp3' | null + uf: null + url: string | null + urlSource: number + }[] +} + +// 获取歌词 + +export interface FetchLyricParams { + id: number +} +export interface FetchLyricResponse { + code: number + sgc: boolean + sfy: boolean + qfy: boolean + lyricUser?: { + id: number + status: number + demand: number + userid: number + nickname: string + uptime: number + } + transUser?: { + id: number + status: number + demand: number + userid: number + nickname: string + uptime: number + } + lrc: { + version: number + lyric: string + } + klyric?: { + version: number + lyric: string + } + tlyric?: { + version: number + lyric: string + } +} + +// 收藏歌曲 +export interface LikeATrackParams { + id: number + like: boolean +} +export interface LikeATrackResponse { + code: number + playlistId: number + songs: Track[] +} diff --git a/src/shared/api/User.ts b/src/shared/api/User.ts new file mode 100644 index 0000000..67d8bb0 --- /dev/null +++ b/src/shared/api/User.ts @@ -0,0 +1,108 @@ +export enum UserApiNames { + FETCH_USER_ACCOUNT = 'fetchUserAccount', + FETCH_USER_LIKED_TRACKS_IDS = 'fetchUserLikedTracksIDs', + FETCH_USER_PLAYLISTS = 'fetchUserPlaylists', + FETCH_USER_ALBUMS = 'fetchUserAlbums', + FETCH_USER_ARTIST = 'fetchUserArtists', +} + +// 获取账号详情 +export interface FetchUserAccountResponse { + code: number + account: { + anonimousUser: boolean + ban: number + baoyueVersion: number + createTime: number + donateVersion: number + id: number + paidFee: boolean + status: number + tokenVersion: number + type: number + userName: string + vipType: number + whitelistAuthority: number + } | null + profile: { + userId: number + userType: number + nickname: string + avatarImgId: number + avatarUrl: string + backgroundImgId: number + backgroundUrl: string + signature: string + createTime: number + userName: string + accountType: number + shortUserName: string + birthday: number + authority: number + gender: number + accountStatus: number + province: number + city: number + authStatus: number + description: string | null + detailDescription: string | null + defaultAvatar: boolean + expertTags: [] | null + experts: [] | null + djStatus: number + locationStatus: number + vipType: number + followed: boolean + mutual: boolean + authenticated: boolean + lastLoginTime: number + lastLoginIP: string + remarkName: string | null + viptypeVersion: number + authenticationTypes: number + avatarDetail: string | null + anchor: boolean + } | null +} + +// 获取用户歌单 +export interface FetchUserPlaylistsParams { + uid: number + offset: number + limit?: number // default 30 +} +export interface FetchUserPlaylistsResponse { + code: number + more: boolean + version: string + playlist: Playlist[] +} + +export interface FetchUserLikedTracksIDsParams { + uid: number +} +export interface FetchUserLikedTracksIDsResponse { + code: number + checkPoint: number + ids: number[] +} + +export interface FetchUserAlbumsParams { + offset?: number // default 0 + limit?: number // default 25 +} +export interface FetchUserAlbumsResponse { + code: number + hasMore: boolean + paidCount: number + count: number + data: Album[] +} + +// 获取收藏的歌手 +export interface FetchUserArtistsResponse { + code: number + hasMore: boolean + count: number + data: Artist[] +} diff --git a/src/renderer/interface.d.ts b/src/shared/interface.d.ts similarity index 100% rename from src/renderer/interface.d.ts rename to src/shared/interface.d.ts diff --git a/src/shared/tsconfig.json b/src/shared/tsconfig.json new file mode 100644 index 0000000..40c14c6 --- /dev/null +++ b/src/shared/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "allowJs": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "esModuleInterop": true, + "moduleResolution": "Node", + "resolveJsonModule": true, + "strict": true, + "jsx": "react-jsx", + "baseUrl": "./", + "paths": { + "@/*": ["../*"] + } + }, + "include": ["./**/*.ts"] +}