mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add experimental ytmusic playback for external songs
This commit is contained in:
@@ -135,6 +135,7 @@
|
|||||||
"string-to-color": "^2.2.2",
|
"string-to-color": "^2.2.2",
|
||||||
"wavesurfer.js": "^7.11.1",
|
"wavesurfer.js": "^7.11.1",
|
||||||
"ws": "^8.18.2",
|
"ws": "^8.18.2",
|
||||||
|
"ytmusic-api": "^5.3.0",
|
||||||
"zod": "^3.22.3",
|
"zod": "^3.22.3",
|
||||||
"zustand": "^5.0.5"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+146
-1
@@ -218,6 +218,9 @@ importers:
|
|||||||
ws:
|
ws:
|
||||||
specifier: ^8.18.2
|
specifier: ^8.18.2
|
||||||
version: 8.18.2
|
version: 8.18.2
|
||||||
|
ytmusic-api:
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.3.0
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.22.3
|
specifier: ^3.22.3
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
@@ -892,53 +895,106 @@ packages:
|
|||||||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@biomejs/biome@1.8.3':
|
||||||
|
resolution: {integrity: sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@biomejs/biome@1.9.4':
|
'@biomejs/biome@1.9.4':
|
||||||
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
|
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@biomejs/cli-darwin-arm64@1.8.3':
|
||||||
|
resolution: {integrity: sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
'@biomejs/cli-darwin-arm64@1.9.4':
|
'@biomejs/cli-darwin-arm64@1.9.4':
|
||||||
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
|
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
'@biomejs/cli-darwin-x64@1.8.3':
|
||||||
|
resolution: {integrity: sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
'@biomejs/cli-darwin-x64@1.9.4':
|
'@biomejs/cli-darwin-x64@1.9.4':
|
||||||
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
|
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-arm64-musl@1.8.3':
|
||||||
|
resolution: {integrity: sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64-musl@1.9.4':
|
'@biomejs/cli-linux-arm64-musl@1.9.4':
|
||||||
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
|
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-arm64@1.8.3':
|
||||||
|
resolution: {integrity: sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64@1.9.4':
|
'@biomejs/cli-linux-arm64@1.9.4':
|
||||||
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
|
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-x64-musl@1.8.3':
|
||||||
|
resolution: {integrity: sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64-musl@1.9.4':
|
'@biomejs/cli-linux-x64-musl@1.9.4':
|
||||||
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
|
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-x64@1.8.3':
|
||||||
|
resolution: {integrity: sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64@1.9.4':
|
'@biomejs/cli-linux-x64@1.9.4':
|
||||||
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
|
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@biomejs/cli-win32-arm64@1.8.3':
|
||||||
|
resolution: {integrity: sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@biomejs/cli-win32-arm64@1.9.4':
|
'@biomejs/cli-win32-arm64@1.9.4':
|
||||||
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
|
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@biomejs/cli-win32-x64@1.8.3':
|
||||||
|
resolution: {integrity: sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@biomejs/cli-win32-x64@1.9.4':
|
'@biomejs/cli-win32-x64@1.9.4':
|
||||||
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
|
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
|
||||||
engines: {node: '>=14.21.3'}
|
engines: {node: '>=14.21.3'}
|
||||||
@@ -3365,7 +3421,7 @@ packages:
|
|||||||
glob@8.1.0:
|
glob@8.1.0:
|
||||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
deprecated: Glob versions prior to v9 are no longer supported
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
global-agent@3.0.0:
|
global-agent@3.0.0:
|
||||||
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
|
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
|
||||||
@@ -4553,6 +4609,9 @@ packages:
|
|||||||
proxy-from-env@1.1.0:
|
proxy-from-env@1.1.0:
|
||||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
|
psl@1.15.0:
|
||||||
|
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
|
||||||
|
|
||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||||
|
|
||||||
@@ -4568,6 +4627,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
|
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
|
querystringify@2.2.0:
|
||||||
|
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||||
|
|
||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
@@ -4813,6 +4875,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
requires-port@1.0.0:
|
||||||
|
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||||
|
|
||||||
resedit@1.7.2:
|
resedit@1.7.2:
|
||||||
resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==}
|
resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==}
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
@@ -5446,6 +5511,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==}
|
resolution: {integrity: sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
tough-cookie@4.1.4:
|
||||||
|
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
tough-cookie@5.1.2:
|
tough-cookie@5.1.2:
|
||||||
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -5570,6 +5639,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
|
|
||||||
|
universalify@0.2.0:
|
||||||
|
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||||
|
engines: {node: '>= 4.0.0'}
|
||||||
|
|
||||||
universalify@2.0.1:
|
universalify@2.0.1:
|
||||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
@@ -5596,6 +5669,9 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
url-parse@1.5.10:
|
||||||
|
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||||
|
|
||||||
use-callback-ref@1.3.3:
|
use-callback-ref@1.3.3:
|
||||||
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -5943,6 +6019,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
ytmusic-api@5.3.0:
|
||||||
|
resolution: {integrity: sha512-RETSp5V5Cj1OT7N4HuEpehpq/YdKlKdXbYSSYH1ks8VBN1WRRpy2CFFwZVO+kIx7nuufBqsXN4B6/nxk4GNKDw==}
|
||||||
|
|
||||||
zod-validation-error@4.0.2:
|
zod-validation-error@4.0.2:
|
||||||
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -6704,6 +6783,17 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|
||||||
|
'@biomejs/biome@1.8.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@biomejs/cli-darwin-arm64': 1.8.3
|
||||||
|
'@biomejs/cli-darwin-x64': 1.8.3
|
||||||
|
'@biomejs/cli-linux-arm64': 1.8.3
|
||||||
|
'@biomejs/cli-linux-arm64-musl': 1.8.3
|
||||||
|
'@biomejs/cli-linux-x64': 1.8.3
|
||||||
|
'@biomejs/cli-linux-x64-musl': 1.8.3
|
||||||
|
'@biomejs/cli-win32-arm64': 1.8.3
|
||||||
|
'@biomejs/cli-win32-x64': 1.8.3
|
||||||
|
|
||||||
'@biomejs/biome@1.9.4':
|
'@biomejs/biome@1.9.4':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@biomejs/cli-darwin-arm64': 1.9.4
|
'@biomejs/cli-darwin-arm64': 1.9.4
|
||||||
@@ -6715,27 +6805,51 @@ snapshots:
|
|||||||
'@biomejs/cli-win32-arm64': 1.9.4
|
'@biomejs/cli-win32-arm64': 1.9.4
|
||||||
'@biomejs/cli-win32-x64': 1.9.4
|
'@biomejs/cli-win32-x64': 1.9.4
|
||||||
|
|
||||||
|
'@biomejs/cli-darwin-arm64@1.8.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-darwin-arm64@1.9.4':
|
'@biomejs/cli-darwin-arm64@1.9.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-darwin-x64@1.8.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-darwin-x64@1.9.4':
|
'@biomejs/cli-darwin-x64@1.9.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-arm64-musl@1.8.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64-musl@1.9.4':
|
'@biomejs/cli-linux-arm64-musl@1.9.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-arm64@1.8.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64@1.9.4':
|
'@biomejs/cli-linux-arm64@1.9.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-x64-musl@1.8.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64-musl@1.9.4':
|
'@biomejs/cli-linux-x64-musl@1.9.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-x64@1.8.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64@1.9.4':
|
'@biomejs/cli-linux-x64@1.9.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-win32-arm64@1.8.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-win32-arm64@1.9.4':
|
'@biomejs/cli-win32-arm64@1.9.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-win32-x64@1.8.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@biomejs/cli-win32-x64@1.9.4':
|
'@biomejs/cli-win32-x64@1.9.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -10631,6 +10745,10 @@ snapshots:
|
|||||||
|
|
||||||
proxy-from-env@1.1.0: {}
|
proxy-from-env@1.1.0: {}
|
||||||
|
|
||||||
|
psl@1.15.0:
|
||||||
|
dependencies:
|
||||||
|
punycode: 2.3.1
|
||||||
|
|
||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.4
|
end-of-stream: 1.4.4
|
||||||
@@ -10646,6 +10764,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
|
||||||
|
querystringify@2.2.0: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
quick-lru@5.1.1: {}
|
quick-lru@5.1.1: {}
|
||||||
@@ -10896,6 +11016,8 @@ snapshots:
|
|||||||
|
|
||||||
require-from-string@2.0.2: {}
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
|
requires-port@1.0.0: {}
|
||||||
|
|
||||||
resedit@1.7.2:
|
resedit@1.7.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
pe-library: 0.4.1
|
pe-library: 0.4.1
|
||||||
@@ -11624,6 +11746,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
streamx: 2.22.0
|
streamx: 2.22.0
|
||||||
|
|
||||||
|
tough-cookie@4.1.4:
|
||||||
|
dependencies:
|
||||||
|
psl: 1.15.0
|
||||||
|
punycode: 2.3.1
|
||||||
|
universalify: 0.2.0
|
||||||
|
url-parse: 1.5.10
|
||||||
|
|
||||||
tough-cookie@5.1.2:
|
tough-cookie@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
tldts: 6.1.86
|
tldts: 6.1.86
|
||||||
@@ -11747,6 +11876,8 @@ snapshots:
|
|||||||
|
|
||||||
universalify@0.1.2: {}
|
universalify@0.1.2: {}
|
||||||
|
|
||||||
|
universalify@0.2.0: {}
|
||||||
|
|
||||||
universalify@2.0.1: {}
|
universalify@2.0.1: {}
|
||||||
|
|
||||||
unzip-crx-3@0.2.0:
|
unzip-crx-3@0.2.0:
|
||||||
@@ -11773,6 +11904,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
url-parse@1.5.10:
|
||||||
|
dependencies:
|
||||||
|
querystringify: 2.2.0
|
||||||
|
requires-port: 1.0.0
|
||||||
|
|
||||||
use-callback-ref@1.3.3(@types/react@19.2.5)(react@19.1.0):
|
use-callback-ref@1.3.3(@types/react@19.2.5)(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@@ -12169,6 +12305,15 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
ytmusic-api@5.3.0:
|
||||||
|
dependencies:
|
||||||
|
'@biomejs/biome': 1.8.3
|
||||||
|
axios: 1.13.2
|
||||||
|
tough-cookie: 4.1.4
|
||||||
|
zod: 3.25.76
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
|
||||||
zod-validation-error@4.0.2(zod@3.25.76):
|
zod-validation-error@4.0.2(zod@3.25.76):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ import './player';
|
|||||||
import './remote';
|
import './remote';
|
||||||
import './settings';
|
import './settings';
|
||||||
import './discord-rpc';
|
import './discord-rpc';
|
||||||
|
import './youtube';
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import YTMusic from 'ytmusic-api';
|
||||||
|
|
||||||
|
let youtubeApi: InstanceType<typeof YTMusic> | null = null;
|
||||||
|
|
||||||
|
const getYoutubeApi = async (): Promise<InstanceType<typeof YTMusic>> => {
|
||||||
|
if (!youtubeApi) {
|
||||||
|
youtubeApi = new YTMusic();
|
||||||
|
await youtubeApi.initialize();
|
||||||
|
}
|
||||||
|
return youtubeApi;
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcMain.handle('youtube-search', async (_event, query: string) => {
|
||||||
|
const api = await getYoutubeApi();
|
||||||
|
const results = await api.search(query);
|
||||||
|
return results;
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ import { mpris } from './mpris';
|
|||||||
import { mpvPlayer, mpvPlayerListener } from './mpv-player';
|
import { mpvPlayer, mpvPlayerListener } from './mpv-player';
|
||||||
import { remote } from './remote';
|
import { remote } from './remote';
|
||||||
import { utils } from './utils';
|
import { utils } from './utils';
|
||||||
|
import { youtube } from './youtube';
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
@@ -25,6 +26,7 @@ const api = {
|
|||||||
mpvPlayerListener,
|
mpvPlayerListener,
|
||||||
remote,
|
remote,
|
||||||
utils,
|
utils,
|
||||||
|
youtube,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PreloadApi = typeof api;
|
export type PreloadApi = typeof api;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
const search = (query: string) => {
|
||||||
|
return ipcRenderer.invoke('youtube-search', query);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const youtube = {
|
||||||
|
search,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Youtube = typeof youtube;
|
||||||
@@ -3,11 +3,9 @@ import memoize from 'lodash/memoize';
|
|||||||
import {
|
import {
|
||||||
IArtist,
|
IArtist,
|
||||||
IBrowseReleasesResult,
|
IBrowseReleasesResult,
|
||||||
IMedium,
|
|
||||||
IRelation,
|
IRelation,
|
||||||
IRelease,
|
IRelease,
|
||||||
IReleaseGroup,
|
IReleaseGroup,
|
||||||
ITrack,
|
|
||||||
IWork,
|
IWork,
|
||||||
MusicBrainzApi,
|
MusicBrainzApi,
|
||||||
} from 'musicbrainz-api';
|
} from 'musicbrainz-api';
|
||||||
@@ -15,14 +13,17 @@ import {
|
|||||||
import packageJson from '../../../../../package.json';
|
import packageJson from '../../../../../package.json';
|
||||||
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { getImageUrl } from '/@/renderer/features/musicbrainz/utils';
|
import {
|
||||||
|
collectWorksFromRelease,
|
||||||
|
getImageUrl,
|
||||||
|
normalizeReleaseToAlbum,
|
||||||
|
} from '/@/renderer/features/musicbrainz/utils';
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
RelatedArtist,
|
RelatedArtist,
|
||||||
ServerType,
|
ServerType,
|
||||||
Song,
|
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const musicbrainzApi = new MusicBrainzApi({
|
export const musicbrainzApi = new MusicBrainzApi({
|
||||||
@@ -33,6 +34,8 @@ export const musicbrainzApi = new MusicBrainzApi({
|
|||||||
|
|
||||||
const CACHE_TIME = 1000 * 60 * 5;
|
const CACHE_TIME = 1000 * 60 * 5;
|
||||||
|
|
||||||
|
export type IRelationWithWork = IRelation & { work?: IWork };
|
||||||
|
|
||||||
export type MusicBrainzArtistSelectMeta = {
|
export type MusicBrainzArtistSelectMeta = {
|
||||||
albumArtist: AlbumArtist;
|
albumArtist: AlbumArtist;
|
||||||
albums?: Album[];
|
albums?: Album[];
|
||||||
@@ -40,8 +43,6 @@ export type MusicBrainzArtistSelectMeta = {
|
|||||||
prioritizeCountries?: string[];
|
prioritizeCountries?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type IRelationWithWork = IRelation & { work?: IWork };
|
|
||||||
|
|
||||||
const artistSelect = memoize(
|
const artistSelect = memoize(
|
||||||
({
|
({
|
||||||
data,
|
data,
|
||||||
@@ -222,27 +223,6 @@ const artistSelect = memoize(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function collectWorksFromRelease(release: IRelease): IWork[] {
|
|
||||||
const works: IWork[] = [];
|
|
||||||
const seenIds = new Set<string>();
|
|
||||||
|
|
||||||
for (const medium of release.media ?? []) {
|
|
||||||
for (const track of medium.tracks ?? []) {
|
|
||||||
const recording = track.recording;
|
|
||||||
const relations = (recording as { relations?: IRelationWithWork[] })?.relations ?? [];
|
|
||||||
for (const rel of relations) {
|
|
||||||
const work = (rel as IRelationWithWork).work;
|
|
||||||
if (work?.id && !seenIds.has(work.id)) {
|
|
||||||
seenIds.add(work.id);
|
|
||||||
works.push(work);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return works;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchMbzReleasesByArtistId(mbzArtistId: string): Promise<IBrowseReleasesResult> {
|
async function fetchMbzReleasesByArtistId(mbzArtistId: string): Promise<IBrowseReleasesResult> {
|
||||||
const PAGE_SIZE = 100;
|
const PAGE_SIZE = 100;
|
||||||
const includes: Array<'media' | 'release-groups'> = ['media', 'release-groups'];
|
const includes: Array<'media' | 'release-groups'> = ['media', 'release-groups'];
|
||||||
@@ -303,187 +283,6 @@ const RELEASE_INCLUDES: Array<
|
|||||||
| 'release-groups'
|
| 'release-groups'
|
||||||
> = ['artist-credits', 'artists', 'media', 'recording-level-rels', 'recordings', 'release-groups'];
|
> = ['artist-credits', 'artists', 'media', 'recording-level-rels', 'recordings', 'release-groups'];
|
||||||
|
|
||||||
function normalizeArtistCreditToRelatedArtists(
|
|
||||||
artistCredit: Array<{ artist: IArtist; name: string }>,
|
|
||||||
): RelatedArtist[] {
|
|
||||||
return artistCredit.map((ac) => ({
|
|
||||||
id: `musicbrainz-${ac.artist.id}`,
|
|
||||||
imageId: null,
|
|
||||||
imageUrl: null,
|
|
||||||
name: ac.name || ac.artist.name,
|
|
||||||
userFavorite: false,
|
|
||||||
userRating: null,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRecordingToSong(
|
|
||||||
release: IRelease,
|
|
||||||
medium: IMedium,
|
|
||||||
track: ITrack,
|
|
||||||
albumArtistName: string,
|
|
||||||
albumArtists: RelatedArtist[],
|
|
||||||
albumId: string,
|
|
||||||
imageUrl: null | string,
|
|
||||||
releaseDate: null | string,
|
|
||||||
releaseYear: null | number,
|
|
||||||
): Song {
|
|
||||||
const recording = track.recording;
|
|
||||||
const trackArtistCredit = track['artist-credit'] ?? recording['artist-credit'] ?? [];
|
|
||||||
|
|
||||||
const artistName =
|
|
||||||
trackArtistCredit.map((ac) => ac.name).join('') || recording.title || track.title;
|
|
||||||
|
|
||||||
const artists = normalizeArtistCreditToRelatedArtists(
|
|
||||||
trackArtistCredit as Array<{ artist: IArtist; name: string }>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const durationMilliseconds = track.length || recording.length || 0;
|
|
||||||
const trackNumber = track.position || parseInt(track.number, 10) || 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
_itemType: LibraryItem.SONG,
|
|
||||||
_serverId: 'musicbrainz',
|
|
||||||
_serverType: ServerType.EXTERNAL,
|
|
||||||
album: release.title,
|
|
||||||
albumArtistName,
|
|
||||||
albumArtists,
|
|
||||||
albumId,
|
|
||||||
artistName,
|
|
||||||
artists,
|
|
||||||
bitDepth: null,
|
|
||||||
bitRate: 0,
|
|
||||||
bpm: null,
|
|
||||||
channels: null,
|
|
||||||
comment: null,
|
|
||||||
compilation: null,
|
|
||||||
container: null,
|
|
||||||
createdAt: '',
|
|
||||||
discNumber: medium.position || 1,
|
|
||||||
discSubtitle: medium.title || null,
|
|
||||||
duration: durationMilliseconds,
|
|
||||||
explicitStatus: null,
|
|
||||||
gain: null,
|
|
||||||
genres: [],
|
|
||||||
id: `musicbrainz-${release.id}-${recording.id}-${track.position}-${track.number}`,
|
|
||||||
imageId: null,
|
|
||||||
imageUrl,
|
|
||||||
lastPlayedAt: null,
|
|
||||||
lyrics: null,
|
|
||||||
mbzRecordingId: recording.id,
|
|
||||||
mbzTrackId: track.id,
|
|
||||||
name: track.title || recording.title,
|
|
||||||
participants: {},
|
|
||||||
path: null,
|
|
||||||
peak: null,
|
|
||||||
playCount: 0,
|
|
||||||
releaseDate,
|
|
||||||
releaseYear,
|
|
||||||
sampleRate: null,
|
|
||||||
size: 0,
|
|
||||||
sortName: track.title || recording.title,
|
|
||||||
tags: null,
|
|
||||||
trackNumber,
|
|
||||||
trackSubtitle: null,
|
|
||||||
updatedAt: '',
|
|
||||||
userFavorite: false,
|
|
||||||
userRating: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeReleaseToAlbum(release: IRelease): Album {
|
|
||||||
const releaseGroup = release['release-group'];
|
|
||||||
const artistCredit = release['artist-credit'] ?? releaseGroup?.['artist-credit'] ?? [];
|
|
||||||
const albumArtistName = artistCredit.map((ac) => ac.name).join('') || release.title;
|
|
||||||
const albumArtists: RelatedArtist[] = (artistCredit as { artist: IArtist; name: string }[]).map(
|
|
||||||
(ac) => ({
|
|
||||||
id: `musicbrainz-${ac.artist.id}`,
|
|
||||||
imageId: null,
|
|
||||||
imageUrl: null,
|
|
||||||
name: ac.name || ac.artist.name,
|
|
||||||
userFavorite: false,
|
|
||||||
userRating: null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasArtwork =
|
|
||||||
release['cover-art-archive']?.artwork === true &&
|
|
||||||
release['cover-art-archive']?.front === true;
|
|
||||||
const primaryReleaseType = releaseGroup?.['primary-type']?.toLowerCase() || null;
|
|
||||||
const secondaryReleaseTypes =
|
|
||||||
releaseGroup?.['secondary-types']?.map((type) => type.toLowerCase()) || [];
|
|
||||||
const releaseTypes = [primaryReleaseType, ...secondaryReleaseTypes].filter(
|
|
||||||
(type) => type !== null,
|
|
||||||
) as string[];
|
|
||||||
const isCompilation = releaseTypes.includes('compilation');
|
|
||||||
const originalDate = releaseGroup?.['first-release-date'] || null;
|
|
||||||
const originalYear = originalDate ? Number(originalDate.split('-')[0]) : null;
|
|
||||||
const releaseDate = release.date ? release.date : null;
|
|
||||||
const releaseYear = release.date ? Number(release.date.split('-')[0]) : null;
|
|
||||||
const imageUrl = hasArtwork ? getImageUrl(release.id) : null;
|
|
||||||
const albumId = `musicbrainz-${release.id}`;
|
|
||||||
|
|
||||||
const songs: Song[] = [];
|
|
||||||
for (const medium of release.media ?? []) {
|
|
||||||
for (const track of medium.tracks ?? []) {
|
|
||||||
songs.push(
|
|
||||||
normalizeRecordingToSong(
|
|
||||||
release,
|
|
||||||
medium,
|
|
||||||
track,
|
|
||||||
albumArtistName,
|
|
||||||
albumArtists,
|
|
||||||
albumId,
|
|
||||||
imageUrl,
|
|
||||||
releaseDate,
|
|
||||||
releaseYear,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalDuration = songs.reduce((sum, s) => sum + s.duration, 0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
_itemType: LibraryItem.ALBUM,
|
|
||||||
_serverId: 'musicbrainz',
|
|
||||||
_serverType: ServerType.EXTERNAL,
|
|
||||||
albumArtistName,
|
|
||||||
albumArtists,
|
|
||||||
artists: [],
|
|
||||||
comment: null,
|
|
||||||
createdAt: '',
|
|
||||||
duration: totalDuration || null,
|
|
||||||
explicitStatus: null,
|
|
||||||
genres: [],
|
|
||||||
id: albumId,
|
|
||||||
imageId: null,
|
|
||||||
imageUrl,
|
|
||||||
isCompilation,
|
|
||||||
lastPlayedAt: null,
|
|
||||||
mbzId: release.id,
|
|
||||||
mbzReleaseGroupId: releaseGroup?.id || null,
|
|
||||||
name: release.title,
|
|
||||||
originalDate,
|
|
||||||
originalYear,
|
|
||||||
participants: {},
|
|
||||||
playCount: null,
|
|
||||||
recordLabels: [],
|
|
||||||
releaseDate,
|
|
||||||
releaseType: primaryReleaseType,
|
|
||||||
releaseTypes,
|
|
||||||
releaseYear,
|
|
||||||
size: null,
|
|
||||||
songCount: songs.length,
|
|
||||||
songs,
|
|
||||||
sortName: release.title,
|
|
||||||
tags: {},
|
|
||||||
updatedAt: '',
|
|
||||||
userFavorite: false,
|
|
||||||
userRating: null,
|
|
||||||
version: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const musicbrainzQueries = {
|
export const musicbrainzQueries = {
|
||||||
artist: (args: {
|
artist: (args: {
|
||||||
excludeReleaseTypes?: string[];
|
excludeReleaseTypes?: string[];
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
async function searchYoutube(query: string): Promise<Array<{ type: string; videoId?: string }>> {
|
||||||
|
if (typeof window !== 'undefined' && window.api?.youtube) {
|
||||||
|
return window.api.youtube.search(query);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const youtubeQueries = {
|
||||||
|
search: (args: { query: string }) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: () => searchYoutube(args.query),
|
||||||
|
queryKey: ['youtube', 'search', args.query],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { MUSICBRAINZ_ID_PREFIX } from '/@/renderer/features/musicbrainz/api/musicbrainz-api';
|
import { IArtist, IRelease, IMedium, ITrack, IWork } from 'musicbrainz-api';
|
||||||
|
import {
|
||||||
|
IRelationWithWork,
|
||||||
|
MUSICBRAINZ_ID_PREFIX,
|
||||||
|
} from '/@/renderer/features/musicbrainz/api/musicbrainz-api';
|
||||||
|
import { RelatedArtist, Song, LibraryItem, ServerType, Album } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export function getImageUrl(releaseId: string): string {
|
export function getImageUrl(releaseId: string): string {
|
||||||
return `https://coverartarchive.org/release/${releaseId}/front-250.jpg`;
|
return `https://coverartarchive.org/release/${releaseId}/front-250.jpg`;
|
||||||
@@ -293,3 +298,201 @@ const MBZ_RELEASE_TYPES = {
|
|||||||
soundtrack: 'soundtrack',
|
soundtrack: 'soundtrack',
|
||||||
spokenword: 'spokenword',
|
spokenword: 'spokenword',
|
||||||
};
|
};
|
||||||
|
function normalizeArtistCreditToRelatedArtists(
|
||||||
|
artistCredit: Array<{ artist: IArtist; name: string }>,
|
||||||
|
): RelatedArtist[] {
|
||||||
|
return artistCredit.map((ac) => ({
|
||||||
|
id: `musicbrainz-${ac.artist.id}`,
|
||||||
|
imageId: null,
|
||||||
|
imageUrl: null,
|
||||||
|
name: ac.name || ac.artist.name,
|
||||||
|
userFavorite: false,
|
||||||
|
userRating: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function normalizeRecordingToSong(
|
||||||
|
release: IRelease,
|
||||||
|
medium: IMedium,
|
||||||
|
track: ITrack,
|
||||||
|
albumArtistName: string,
|
||||||
|
albumArtists: RelatedArtist[],
|
||||||
|
albumId: string,
|
||||||
|
imageUrl: null | string,
|
||||||
|
releaseDate: null | string,
|
||||||
|
releaseYear: null | number,
|
||||||
|
): Song {
|
||||||
|
const recording = track.recording;
|
||||||
|
const trackArtistCredit = track['artist-credit'] ?? recording['artist-credit'] ?? [];
|
||||||
|
|
||||||
|
const artistName =
|
||||||
|
trackArtistCredit.map((ac) => ac.name).join('') || recording.title || track.title;
|
||||||
|
|
||||||
|
const artists = normalizeArtistCreditToRelatedArtists(
|
||||||
|
trackArtistCredit as Array<{ artist: IArtist; name: string }>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const durationMilliseconds = track.length || recording.length || 0;
|
||||||
|
const trackNumber = track.position || parseInt(track.number, 10) || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
_itemType: LibraryItem.SONG,
|
||||||
|
_serverId: 'musicbrainz',
|
||||||
|
_serverType: ServerType.EXTERNAL,
|
||||||
|
album: release.title,
|
||||||
|
albumArtistName,
|
||||||
|
albumArtists,
|
||||||
|
albumId,
|
||||||
|
artistName,
|
||||||
|
artists,
|
||||||
|
bitDepth: null,
|
||||||
|
bitRate: 0,
|
||||||
|
bpm: null,
|
||||||
|
channels: null,
|
||||||
|
comment: null,
|
||||||
|
compilation: null,
|
||||||
|
container: null,
|
||||||
|
createdAt: '',
|
||||||
|
discNumber: medium.position || 1,
|
||||||
|
discSubtitle: medium.title || null,
|
||||||
|
duration: durationMilliseconds,
|
||||||
|
explicitStatus: null,
|
||||||
|
gain: null,
|
||||||
|
genres: [],
|
||||||
|
id: `musicbrainz-${release.id}-${recording.id}-${track.position}-${track.number}`,
|
||||||
|
imageId: null,
|
||||||
|
imageUrl,
|
||||||
|
lastPlayedAt: null,
|
||||||
|
lyrics: null,
|
||||||
|
mbzRecordingId: recording.id,
|
||||||
|
mbzTrackId: track.id,
|
||||||
|
name: track.title || recording.title,
|
||||||
|
participants: {},
|
||||||
|
path: null,
|
||||||
|
peak: null,
|
||||||
|
playCount: 0,
|
||||||
|
releaseDate,
|
||||||
|
releaseYear,
|
||||||
|
sampleRate: null,
|
||||||
|
size: 0,
|
||||||
|
sortName: track.title || recording.title,
|
||||||
|
tags: null,
|
||||||
|
trackNumber,
|
||||||
|
trackSubtitle: null,
|
||||||
|
updatedAt: '',
|
||||||
|
userFavorite: false,
|
||||||
|
userRating: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function normalizeReleaseToAlbum(release: IRelease): Album {
|
||||||
|
const releaseGroup = release['release-group'];
|
||||||
|
const artistCredit = release['artist-credit'] ?? releaseGroup?.['artist-credit'] ?? [];
|
||||||
|
const albumArtistName = artistCredit.map((ac) => ac.name).join('') || release.title;
|
||||||
|
const albumArtists: RelatedArtist[] = (artistCredit as { artist: IArtist; name: string }[]).map(
|
||||||
|
(ac) => ({
|
||||||
|
id: `musicbrainz-${ac.artist.id}`,
|
||||||
|
imageId: null,
|
||||||
|
imageUrl: null,
|
||||||
|
name: ac.name || ac.artist.name,
|
||||||
|
userFavorite: false,
|
||||||
|
userRating: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasArtwork =
|
||||||
|
release['cover-art-archive']?.artwork === true &&
|
||||||
|
release['cover-art-archive']?.front === true;
|
||||||
|
const primaryReleaseType = releaseGroup?.['primary-type']?.toLowerCase() || null;
|
||||||
|
const secondaryReleaseTypes =
|
||||||
|
releaseGroup?.['secondary-types']?.map((type) => type.toLowerCase()) || [];
|
||||||
|
const releaseTypes = [primaryReleaseType, ...secondaryReleaseTypes].filter(
|
||||||
|
(type) => type !== null,
|
||||||
|
) as string[];
|
||||||
|
const isCompilation = releaseTypes.includes('compilation');
|
||||||
|
const originalDate = releaseGroup?.['first-release-date'] || null;
|
||||||
|
const originalYear = originalDate ? Number(originalDate.split('-')[0]) : null;
|
||||||
|
const releaseDate = release.date ? release.date : null;
|
||||||
|
const releaseYear = release.date ? Number(release.date.split('-')[0]) : null;
|
||||||
|
const imageUrl = hasArtwork ? getImageUrl(release.id) : null;
|
||||||
|
const albumId = `musicbrainz-${release.id}`;
|
||||||
|
|
||||||
|
const songs: Song[] = [];
|
||||||
|
for (const medium of release.media ?? []) {
|
||||||
|
for (const track of medium.tracks ?? []) {
|
||||||
|
songs.push(
|
||||||
|
normalizeRecordingToSong(
|
||||||
|
release,
|
||||||
|
medium,
|
||||||
|
track,
|
||||||
|
albumArtistName,
|
||||||
|
albumArtists,
|
||||||
|
albumId,
|
||||||
|
imageUrl,
|
||||||
|
releaseDate,
|
||||||
|
releaseYear,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDuration = songs.reduce((sum, s) => sum + s.duration, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
_itemType: LibraryItem.ALBUM,
|
||||||
|
_serverId: 'musicbrainz',
|
||||||
|
_serverType: ServerType.EXTERNAL,
|
||||||
|
albumArtistName,
|
||||||
|
albumArtists,
|
||||||
|
artists: [],
|
||||||
|
comment: null,
|
||||||
|
createdAt: '',
|
||||||
|
duration: totalDuration || null,
|
||||||
|
explicitStatus: null,
|
||||||
|
genres: [],
|
||||||
|
id: albumId,
|
||||||
|
imageId: null,
|
||||||
|
imageUrl,
|
||||||
|
isCompilation,
|
||||||
|
lastPlayedAt: null,
|
||||||
|
mbzId: release.id,
|
||||||
|
mbzReleaseGroupId: releaseGroup?.id || null,
|
||||||
|
name: release.title,
|
||||||
|
originalDate,
|
||||||
|
originalYear,
|
||||||
|
participants: {},
|
||||||
|
playCount: null,
|
||||||
|
recordLabels: [],
|
||||||
|
releaseDate,
|
||||||
|
releaseType: primaryReleaseType,
|
||||||
|
releaseTypes,
|
||||||
|
releaseYear,
|
||||||
|
size: null,
|
||||||
|
songCount: songs.length,
|
||||||
|
songs,
|
||||||
|
sortName: release.title,
|
||||||
|
tags: {},
|
||||||
|
updatedAt: '',
|
||||||
|
userFavorite: false,
|
||||||
|
userRating: null,
|
||||||
|
version: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function collectWorksFromRelease(release: IRelease): IWork[] {
|
||||||
|
const works: IWork[] = [];
|
||||||
|
const seenIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const medium of release.media ?? []) {
|
||||||
|
for (const track of medium.tracks ?? []) {
|
||||||
|
const recording = track.recording;
|
||||||
|
const relations = (recording as { relations?: IRelationWithWork[] })?.relations ?? [];
|
||||||
|
for (const rel of relations) {
|
||||||
|
const work = (rel as IRelationWithWork).work;
|
||||||
|
if (work?.id && !seenIds.has(work.id)) {
|
||||||
|
seenIds.add(work.id);
|
||||||
|
works.push(work);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return works;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
|
import { youtubeQueries } from '/@/renderer/features/musicbrainz/api/youtube-api';
|
||||||
import { TranscodingConfig } from '/@/renderer/store';
|
import { TranscodingConfig } from '/@/renderer/store';
|
||||||
import { QueueSong } from '/@/shared/types/domain-types';
|
import { QueueSong, ServerType } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
const YOUTUBE_WATCH_BASE = 'https://www.youtube.com/watch?v=';
|
||||||
|
|
||||||
export function useSongUrl(
|
export function useSongUrl(
|
||||||
song: QueueSong | undefined,
|
song: QueueSong | undefined,
|
||||||
@@ -11,10 +15,36 @@ export function useSongUrl(
|
|||||||
): string | undefined {
|
): string | undefined {
|
||||||
const prior = useRef(['', '']);
|
const prior = useRef(['', '']);
|
||||||
|
|
||||||
|
const isExternal = song?._serverType === ServerType.EXTERNAL;
|
||||||
|
const searchQuery =
|
||||||
|
song && isExternal ? `${song.artistName ?? ''} ${song.name ?? ''}`.trim() : '';
|
||||||
|
|
||||||
|
const youtubeSearch = useQuery({
|
||||||
|
...youtubeQueries.search({ query: searchQuery }),
|
||||||
|
enabled: Boolean(song && isExternal && searchQuery),
|
||||||
|
});
|
||||||
|
|
||||||
|
const externalUrl = useMemo(() => {
|
||||||
|
if (!song || !isExternal) return undefined;
|
||||||
|
if (current && prior.current[0] === song._uniqueId && prior.current[1]) {
|
||||||
|
return prior.current[1];
|
||||||
|
}
|
||||||
|
const url = getYoutubeUrlFromSearchResults(youtubeSearch.data);
|
||||||
|
if (url) prior.current = [song._uniqueId, url];
|
||||||
|
return url;
|
||||||
|
}, [song, isExternal, current, youtubeSearch.data]);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (song?._serverId) {
|
if (!song) {
|
||||||
// If we are the current track, we do not want a transcoding
|
prior.current = ['', ''];
|
||||||
// reconfiguration to force a restart.
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExternal) {
|
||||||
|
return externalUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (song._serverId) {
|
||||||
if (current && prior.current[0] === song._uniqueId) {
|
if (current && prior.current[0] === song._uniqueId) {
|
||||||
return prior.current[1];
|
return prior.current[1];
|
||||||
}
|
}
|
||||||
@@ -29,18 +59,16 @@ export function useSongUrl(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// transcoding enabled; save the updated result
|
|
||||||
prior.current = [song._uniqueId, url];
|
prior.current = [song._uniqueId, url];
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no track; clear result
|
|
||||||
prior.current = ['', ''];
|
prior.current = ['', ''];
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [
|
}, [
|
||||||
song?._serverId,
|
song,
|
||||||
song?._uniqueId,
|
isExternal,
|
||||||
song?.id,
|
externalUrl,
|
||||||
current,
|
current,
|
||||||
transcode.bitrate,
|
transcode.bitrate,
|
||||||
transcode.format,
|
transcode.format,
|
||||||
@@ -48,6 +76,16 @@ export function useSongUrl(
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getYoutubeUrlFromSearchResults(
|
||||||
|
results: Array<{ type: string; videoId?: string }> | undefined,
|
||||||
|
): string | undefined {
|
||||||
|
if (!results?.length) return undefined;
|
||||||
|
const first = results.find((r) => r.type === 'SONG' || r.type === 'VIDEO');
|
||||||
|
return first && 'videoId' in first && first.videoId
|
||||||
|
? `${YOUTUBE_WATCH_BASE}${first.videoId}`
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const getSongUrl = (song: QueueSong, transcode: TranscodingConfig) => {
|
export const getSongUrl = (song: QueueSong, transcode: TranscodingConfig) => {
|
||||||
return api.controller.getStreamUrl({
|
return api.controller.getStreamUrl({
|
||||||
apiClientProps: { serverId: song._serverId },
|
apiClientProps: { serverId: song._serverId },
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
|
|||||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||||
import { updateQueueSong } from '/@/renderer/store/player.store';
|
import { updateQueueSong } from '/@/renderer/store/player.store';
|
||||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
import { QueueSong, SongDetailQuery } from '/@/shared/types/domain-types';
|
import { QueueSong, ServerType, SongDetailQuery } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const useUpdateCurrentSong = () => {
|
export const useUpdateCurrentSong = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -16,7 +16,11 @@ export const useUpdateCurrentSong = () => {
|
|||||||
async (properties: { index: number; song: QueueSong | undefined }) => {
|
async (properties: { index: number; song: QueueSong | undefined }) => {
|
||||||
const currentSong = properties.song;
|
const currentSong = properties.song;
|
||||||
|
|
||||||
if (!currentSong?.id || !currentSong?._serverId) {
|
if (
|
||||||
|
!currentSong?.id ||
|
||||||
|
!currentSong?._serverId ||
|
||||||
|
currentSong?._serverType === ServerType.EXTERNAL
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user