feat: updates

This commit is contained in:
qier222 2022-04-16 21:14:03 +08:00
parent fc1c25f404
commit 7b6579e068
No known key found for this signature in database
GPG Key ID: 9C85007ED905F14D
48 changed files with 1155 additions and 777 deletions

View File

@ -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",

View File

@ -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

View File

@ -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',
}

View File

@ -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',
}

View File

@ -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<T extends keyof APIsParams>(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}`)

View File

@ -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<T extends TableNames>(
table: T,
key: TablesStructures[T]['id']
) {
delete<T extends TableNames>(table: T, key: TablesStructures[T]['id']) {
return this.sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key)
}

View File

@ -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<TypedElectronStore>({
defaults: {
window: {
width: 1440,
height: 960,
class Main {
win: BrowserWindow | null = null
store = new Store<TypedElectronStore>({
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()

View File

@ -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 = <T extends keyof IpcChannelsParams>(
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,

View File

@ -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')

View File

@ -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()

View File

@ -15,5 +15,5 @@
"@/*": ["../*"]
}
},
"include": ["./**/*.ts"]
"include": ["./**/*.ts", "../shared/**/*.ts"]
}

View File

@ -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<LikeAAlbumResponse> {

View File

@ -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<FetchArtistAlbumsResponse> {

View File

@ -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

View File

@ -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<FetchRecommendedPlaylistsResponse> {
@ -59,12 +39,7 @@ export function fetchRecommendedPlaylists(
}
// 每日推荐歌单(需要登录)
export interface FetchDailyRecommendPlaylistsResponse {
code: number
featureFirst: boolean
haveRcmdSongs: boolean
recommend: Playlist[]
}
export function fetchDailyRecommendPlaylists(): Promise<FetchDailyRecommendPlaylistsResponse> {
return request({
url: '/recommend/resource',
@ -72,13 +47,6 @@ export function fetchDailyRecommendPlaylists(): Promise<FetchDailyRecommendPlayl
})
}
export interface LikeAPlaylistParams {
t: 1 | 2
id: number
}
export interface LikeAPlaylistResponse {
code: number
}
export function likeAPlaylist(
params: LikeAPlaylistParams
): Promise<LikeAPlaylistResponse> {

View File

@ -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<SearchResponse> {
return request({
url: '/search',
@ -79,19 +20,6 @@ export function search(params: SearchParams): Promise<SearchResponse> {
}
// 搜索多重匹配
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<MultiMatchSearchResponse> {

View File

@ -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<FetchTracksResponse> {
@ -30,39 +24,6 @@ export function fetchTracks(
}
// 获取音源URL
export interface FetchAudioSourceParams {
id: number
br?: number // bitrate, default 999000320000 = 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<FetchAudioSourceResponse> {
@ -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<FetchLyricResponse> {
@ -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<LikeATrackResponse> {

View File

@ -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<fetchUserAccountResponse> {
export function fetchUserAccount(): Promise<FetchUserAccountResponse> {
return request({
url: '/user/account',
method: 'get',
@ -94,17 +39,6 @@ export function fetchUserAccount(): Promise<fetchUserAccountResponse> {
}
// 获取用户歌单
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<FetchUserPlaylistsResponse> {
@ -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<FetchUserLikedTracksIDsResponse> {
@ -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<FetchUserAlbumsResponse> {
@ -178,12 +93,6 @@ export function fetchUserAlbums(
}
// 获取收藏的歌手
export interface FetchUserArtistsResponse {
code: number
hasMore: boolean
count: number
data: Artist[]
}
export function fetchUserArtists(): Promise<FetchUserArtistsResponse> {
return request({
url: '/artist/sublist',

View File

@ -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)

View File

@ -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: <T extends keyof IpcChannelsParams>(
channel: T,
params?: IpcChannelsParams[T]
) => IpcChannelsReturns[T]
send: <T extends keyof IpcChannelsParams>(
channel: T,
params?: IpcChannelsParams[T]
) => void
on: <T extends keyof IpcChannelsParams>(
channel: T,
listener: (
event: Electron.IpcRendererEvent,
value: IpcChannelsReturns[T]
) => void
) => void
}
env?: {

View File

@ -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,
},

View File

@ -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,
},

View File

@ -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,
},

View File

@ -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,
},

View File

@ -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,
},

View File

@ -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(','),
},

View File

@ -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

View File

@ -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,
}),
})
}

View File

@ -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,
}),
}

View File

@ -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,
}),
})
}

View File

@ -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,
},

View File

@ -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,
},

View File

@ -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,
}),
}
)

View File

@ -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[] }) => {

View File

@ -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', () => {

View File

@ -20,5 +20,5 @@
"@/*": ["../*"]
}
},
"include": ["./**/*.ts", "./**/*.tsx"]
"include": ["./**/*.ts", "./**/*.tsx", "../shared/**/*.ts"]
}

View File

@ -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,
})
}

View File

@ -1,4 +1,4 @@
import { FetchLyricResponse } from '@/renderer/api/track'
import { FetchLyricResponse } from '@/shared/api/Track'
export function lyricParser(lrc: FetchLyricResponse) {
return {

72
src/shared/CacheAPIs.ts Normal file
View File

@ -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
}

40
src/shared/IpcChannels.ts Normal file
View File

@ -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
}

23
src/shared/api/Album.ts Normal file
View File

@ -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
}

28
src/shared/api/Artist.ts Normal file
View File

@ -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
}

View File

@ -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
}

82
src/shared/api/Search.ts Normal file
View File

@ -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'>
}
}

104
src/shared/api/Track.ts Normal file
View File

@ -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 999000320000 = 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[]
}

108
src/shared/api/User.ts Normal file
View File

@ -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[]
}

19
src/shared/tsconfig.json Normal file
View File

@ -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"]
}