mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Compare commits
963 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d57faa197 | |||
| 4129a8f56e | |||
| 1aa91fe2f5 | |||
| 5c399f7117 | |||
| 3c07f03651 | |||
| b26b6eab09 | |||
| b2579c031d | |||
| 98e2458a03 | |||
| b30e26ae7e | |||
| fd158b956a | |||
| 109788ebbb | |||
| ffdef596ad | |||
| 0a54f7c44c | |||
| d5d995de5f | |||
| 304c38db1e | |||
| ef631d12cc | |||
| 4006980b29 | |||
| f9c3c107bd | |||
| 7106b100ce | |||
| e2d56c70b1 | |||
| 1a930021b6 | |||
| 66699b9572 | |||
| 99be12e648 | |||
| dde4e1b33c | |||
| 88711eac2f | |||
| f21ca83179 | |||
| f43950874d | |||
| a3794158f0 | |||
| 7f52b31b40 | |||
| 18a864a049 | |||
| 4eac6457ea | |||
| df0d4b7032 | |||
| f0942c7795 | |||
| 396325f397 | |||
| 63015195b0 | |||
| e821397e6c | |||
| aae68853ef | |||
| f904aafd4a | |||
| 38b2508de6 | |||
| 8fee57157a | |||
| 6207cea9f1 | |||
| c299752e44 | |||
| 82e4f832eb | |||
| c8221c07ef | |||
| 710fc16f62 | |||
| 331cddcabb | |||
| b9a0d9b847 | |||
| 9c59a38f7a | |||
| b573999d33 | |||
| 35d8698ca0 | |||
| 23e4574667 | |||
| 7db15c7c72 | |||
| d94b220319 | |||
| acfc106f40 | |||
| 856400048b | |||
| a7c2a92f16 | |||
| fc3d700a57 | |||
| a60973ffee | |||
| a1114235d6 | |||
| 928b0b6f4d | |||
| 60d6d49eaa | |||
| 804a670bf1 | |||
| e51bb05564 | |||
| a21ee21652 | |||
| 8b2d162733 | |||
| cc76c9f31e | |||
| aa7a5037fa | |||
| 0acb1f54fc | |||
| 403ed8cae6 | |||
| 3db229ef68 | |||
| f0d22267c3 | |||
| 796e511626 | |||
| 06e757d3b2 | |||
| 0c8032d097 | |||
| 7cd86d1301 | |||
| 781df3ab06 | |||
| 88b9124185 | |||
| 48724f816c | |||
| 1a184a73de | |||
| e4b5cf36e1 | |||
| dff182cbc5 | |||
| fb8245539f | |||
| bb3cb4a6ad | |||
| fb2e30c484 | |||
| 73c5292cc1 | |||
| 800074dced | |||
| f78a572a3c | |||
| 97b20cec19 | |||
| fd833f683b | |||
| 076d9b3083 | |||
| 3a2a1b0dc8 | |||
| 20c19cac6f | |||
| 8205eeed22 | |||
| 5eb2cff6e9 | |||
| d822d9cd29 | |||
| 4142132ebc | |||
| fb2746323b | |||
| d9172efae9 | |||
| 8e04f98e26 | |||
| 51587fbb6b | |||
| cf06d69822 | |||
| 22751de2f6 | |||
| 04fbf5d3d2 | |||
| 936ba73fe4 | |||
| 05efd0f318 | |||
| ce570eddd2 | |||
| 5b1f269344 | |||
| 0806d9852a | |||
| c3e38d7133 | |||
| a322717e0e | |||
| dcb84dd442 | |||
| ac257a9dc1 | |||
| 25bfb65b6d | |||
| 96f38e597c | |||
| 383b728ddc | |||
| 003cfbdd6c | |||
| a67ae50d16 | |||
| ac3dcb5e17 | |||
| 833f82edff | |||
| 76f55111ec | |||
| f418bbfd2f | |||
| b02eba510d | |||
| 7a77b9bfe7 | |||
| c34b6774b9 | |||
| f3fe5b013a | |||
| 37be2cc8fa | |||
| e3c26aa5fa | |||
| e21f538aa4 | |||
| c9cd87bae5 | |||
| 9a8cb45510 | |||
| 68b6a58ac5 | |||
| 5b5cdbfb7f | |||
| cf4e505743 | |||
| 8464ed439e | |||
| 9e49a45db9 | |||
| 8dc5f2a580 | |||
| 6bb848a675 | |||
| 8edf61f9e7 | |||
| 96d2699a2d | |||
| 614761efd7 | |||
| 154f0180e1 | |||
| 593ad7ad12 | |||
| 2372f1890c | |||
| ad5485506d | |||
| ea7399541f | |||
| 455452bf77 | |||
| b2941b67d6 | |||
| bc898d6097 | |||
| 88b27fa378 | |||
| 3c2af4bd1a | |||
| f5d6c4983c | |||
| 08a8f7f500 | |||
| f92754c2ac | |||
| d4e39ca12d | |||
| 868c64dda5 | |||
| 09dfc1ff1d | |||
| 303eba87ca | |||
| d8c46ba7ef | |||
| 649f541077 | |||
| 858678ea93 | |||
| 48bba76c74 | |||
| 3f32e6cd2f | |||
| 253a78b236 | |||
| 69de9a98f0 | |||
| 0f4534c34c | |||
| a9476a98b1 | |||
| e56d885007 | |||
| b6e7bfb9bc | |||
| a377e3d3bb | |||
| 752b191ad7 | |||
| b19e752314 | |||
| 1e8e1789cd | |||
| 1bc1f619ff | |||
| b4b106222e | |||
| 4cc51c3700 | |||
| 8b0632495b | |||
| 7ed847fecb | |||
| f61d34c340 | |||
| 5c8d18d1c9 | |||
| 401d0e5c19 | |||
| e7bdb2d4ed | |||
| f9ce076a1a | |||
| a14e5f08ee | |||
| ab1176d4f6 | |||
| eb100351a6 | |||
| a546a4d57b | |||
| 1ac267fa99 | |||
| 2a36fa932c | |||
| 8f1141b6e0 | |||
| ed5d590a6b | |||
| 13afd3d9c4 | |||
| d821e0a1f8 | |||
| f850c52f5d | |||
| 22dd20884d | |||
| 578083d994 | |||
| 29a5fa3f74 | |||
| 1bcc23862c | |||
| 4103ed7221 | |||
| ce6155fe47 | |||
| e147a78ee0 | |||
| 4b66c86fd6 | |||
| aa435bbfc4 | |||
| a4dbeff5ea | |||
| 61e70342a4 | |||
| f7d488ba84 | |||
| 01a95f5445 | |||
| bdce8ecafe | |||
| bb55fc2278 | |||
| a778f4c715 | |||
| bea5bd7857 | |||
| 10aec7bcac | |||
| c1ea39aa14 | |||
| 5da119e065 | |||
| 27eebe474e | |||
| 16d656f010 | |||
| eeb0a786fd | |||
| 32062d7c0f | |||
| b627f4e489 | |||
| 389dfd08f7 | |||
| 8a681a7e95 | |||
| 6eba434c3e | |||
| 816df56ef1 | |||
| a02fc28785 | |||
| 753f35f81b | |||
| fcfe39aa2b | |||
| d5799af44e | |||
| 3d07290231 | |||
| a19a6815e9 | |||
| 628b0184de | |||
| 38b06fdd06 | |||
| 498a0f4b5d | |||
| a96ea6c5ac | |||
| b6ac954910 | |||
| 76b59921f1 | |||
| 507bf66f47 | |||
| d3191611c6 | |||
| 7f540472da | |||
| cd2d8ae3c9 | |||
| 01a412efd0 | |||
| 2929603565 | |||
| e4b5158fe3 | |||
| 4f4300042b | |||
| bd8503b25d | |||
| 3c8054c93b | |||
| 20aa65f16a | |||
| c82762a3fc | |||
| 4ddada1fe3 | |||
| 2f2dbbde3e | |||
| adbcca00de | |||
| 65a7c3440b | |||
| 4862a65b21 | |||
| 68fa8ac058 | |||
| d8b190c2b7 | |||
| 20a6fcbeb7 | |||
| 2fb40fd050 | |||
| 9634d7c50d | |||
| 9dbee39d34 | |||
| 56098c6617 | |||
| 9c81487f98 | |||
| 8841781e6b | |||
| ce7e5ad7ad | |||
| 512a769742 | |||
| 1507aff8e6 | |||
| a74ed3e4c7 | |||
| 1d6019c9d2 | |||
| 9385c25ea9 | |||
| e84d5eca8f | |||
| 84621474c0 | |||
| 42fca271ce | |||
| 766756f64a | |||
| 0a7029f7bc | |||
| 126b5ed67d | |||
| 4531f88e71 | |||
| dace0259aa | |||
| 2d5671323f | |||
| 7db3eab38c | |||
| c7b0759ff0 | |||
| 202c5da350 | |||
| 8bb46d78aa | |||
| 64a67cf169 | |||
| 83886ed4ba | |||
| 40af5fb945 | |||
| 3554572dc2 | |||
| 999976645d | |||
| 458ede252a | |||
| 1438f42616 | |||
| b72af98a1b | |||
| d463030271 | |||
| de86f687ea | |||
| 566d938b6c | |||
| 7ac47377f1 | |||
| c27a9bf95b | |||
| 9004485aa9 | |||
| 845b678e18 | |||
| ab9da5207c | |||
| e37c76301d | |||
| 8c545f6d21 | |||
| 286b6d1a11 | |||
| b66530f8fd | |||
| 7c5dc5bb39 | |||
| 19a0529b42 | |||
| 424c269c22 | |||
| 376c09d1ea | |||
| c14877916a | |||
| 4d626377ef | |||
| 69fa5bc733 | |||
| 20830cb979 | |||
| ccdd16292a | |||
| 5540ca4e32 | |||
| a5372313c4 | |||
| 091625d7d9 | |||
| a27ddfe746 | |||
| 5ea7798c52 | |||
| ee5d2b12c1 | |||
| 06e502c63d | |||
| 92682c62dc | |||
| d8df70eddd | |||
| cb2581252b | |||
| a47f94ebb2 | |||
| 9d498d4ca2 | |||
| 85c0d8503b | |||
| d604074d82 | |||
| d511b58c8f | |||
| 49ce670281 | |||
| 55ebc7d74a | |||
| a7e6a75c68 | |||
| c738725d94 | |||
| b87d63ced9 | |||
| 75ef5995ea | |||
| 16971aaa9c | |||
| 0b5b554eec | |||
| 917bf91583 | |||
| 355257104d | |||
| 47d44851f0 | |||
| f84506ce01 | |||
| 854a26e3f4 | |||
| ddb6447165 | |||
| 4d2721db50 | |||
| d22b24d98f | |||
| 7701ea0a8c | |||
| a35577444b | |||
| cab16b0893 | |||
| bc5d0cf994 | |||
| f2b59d531b | |||
| 9699092d9f | |||
| 7d9cd39d85 | |||
| bbd6457341 | |||
| 9f957c2197 | |||
| 248b71311d | |||
| 578a373248 | |||
| 59ae291e1f | |||
| 0472e6711b | |||
| d9c8e5ccba | |||
| 080258dc49 | |||
| 7237b4d136 | |||
| a58b3394e9 | |||
| d5144d0dd4 | |||
| 677c41efb9 | |||
| 765f96254e | |||
| dd8bf8db35 | |||
| e0a12bc859 | |||
| 1d2eda4175 | |||
| e4b648a99c | |||
| 533d22b050 | |||
| 2b9cb196d0 | |||
| 517dfcf2a7 | |||
| b715bd178f | |||
| 97af90c004 | |||
| 8d53358d33 | |||
| 2c026837e8 | |||
| 6d8947fe74 | |||
| 65f24c2c03 | |||
| 4889f2b51a | |||
| 2b2dd34c60 | |||
| 07523f82ce | |||
| aff7a61bca | |||
| 542dd573f2 | |||
| 4abfbd1973 | |||
| 93d4536ba6 | |||
| 72e7006cc3 | |||
| 94ed91f95c | |||
| baf40ba50e | |||
| 51c546fa5d | |||
| 84b464090c | |||
| 7db0b59895 | |||
| 94b9a3a4dc | |||
| 1c5bbb8fa0 | |||
| 994d8bac71 | |||
| bfac1524a8 | |||
| 2ceca9c034 | |||
| 02144db221 | |||
| 8d0eb55691 | |||
| 5442107405 | |||
| fe493d1dab | |||
| 0afbfb12cd | |||
| b5780094e7 | |||
| 0fd6032bfc | |||
| 65c54655ff | |||
| 286651c1b3 | |||
| c7bf0d8fb8 | |||
| 0b8ae55150 | |||
| b99bc62065 | |||
| 615280e01b | |||
| 72ef6da243 | |||
| 5cab727b2d | |||
| b9b39caf86 | |||
| 96acf759ff | |||
| c5c2b24a9d | |||
| 6d87da2474 | |||
| d75d1687a4 | |||
| f425e6ba63 | |||
| fd2023e9d6 | |||
| e8db28e112 | |||
| 78fcc5c1c7 | |||
| 08e8c65a18 | |||
| dd3d05c813 | |||
| 8777da9491 | |||
| cb3c0fe0d4 | |||
| 1c0cbafa3e | |||
| c5112e5031 | |||
| 196289edb3 | |||
| aab19b289b | |||
| 801e2cdcf4 | |||
| 0b70ed158b | |||
| de3c7de7e5 | |||
| cba7cbfdfa | |||
| f5ab1d8e8a | |||
| 7744f58c8c | |||
| 981e250d8c | |||
| 297db71850 | |||
| 24846fbae4 | |||
| ad198ea047 | |||
| 526ba338a8 | |||
| cfb289f906 | |||
| 8d829b2886 | |||
| bdc52ece9d | |||
| 224fcf94b8 | |||
| 3f704f1f35 | |||
| 8611bb9755 | |||
| 108ba53be2 | |||
| 96e9d54f4e | |||
| 7f95c520b2 | |||
| b097d67d71 | |||
| 3a5eb96410 | |||
| 576d14dca0 | |||
| a6945bc1f3 | |||
| 6094a520e2 | |||
| d22fee887c | |||
| 83bd83b629 | |||
| abd2f9485c | |||
| f448572e0a | |||
| 2fa6c9de94 | |||
| 5e12a666e3 | |||
| bb1705a774 | |||
| 974e96c7b4 | |||
| 4e3b4c0118 | |||
| e69d0c8922 | |||
| 5e8d17f144 | |||
| 774495262e | |||
| a708162b15 | |||
| 61bfca90d1 | |||
| c4bf3da5d4 | |||
| 93ba99e36f | |||
| a99a02c94c | |||
| 998eb02621 | |||
| a18d2ee305 | |||
| 663fdd426f | |||
| 72e2e6daca | |||
| 9b17d3513a | |||
| 06d0c715af | |||
| 9d3c44ef15 | |||
| 8f06b177b5 | |||
| 9e1cdcc93e | |||
| 2101b2e1c7 | |||
| c376f3add3 | |||
| 503dfd6a55 | |||
| 4fc036f4ea | |||
| dbad1088ea | |||
| b7c413c96a | |||
| 1c65ca4a5a | |||
| 5d0124369e | |||
| 3f8a3a5e03 | |||
| 60d8d18a0f | |||
| 212d9e9f55 | |||
| f0c35be21f | |||
| 2f32534cf9 | |||
| 7016446be7 | |||
| 0927ff0e5e | |||
| 4ef2956eb1 | |||
| a893b063de | |||
| 7cc7086dbb | |||
| 092a9c3f19 | |||
| ac5de29c71 | |||
| 6b307b3bd4 | |||
| 9e0c4d4b2a | |||
| d66b1c1bbb | |||
| 3a2c5f7b11 | |||
| 411f661174 | |||
| a238927749 | |||
| 10c9bec2cf | |||
| ffa9d165f2 | |||
| 6a8e55ce15 | |||
| 2c9ea6d19c | |||
| 902ac91b95 | |||
| 60cdea6787 | |||
| 948f428546 | |||
| 94a7829882 | |||
| 4b3fda18d4 | |||
| 778d878349 | |||
| c77d38fca0 | |||
| 042047d7c1 | |||
| 6f132f745b | |||
| 2264fa0d29 | |||
| 8ad5e26c2f | |||
| 22fae938c4 | |||
| 359e442947 | |||
| 0a6b9a1040 | |||
| 8d72596a33 | |||
| e78a46ab24 | |||
| d3132ad570 | |||
| eba485034a | |||
| 1b8afe3134 | |||
| 04c2bec58f | |||
| 6dc110e776 | |||
| a4f53e5273 | |||
| 5e5b4f1536 | |||
| a2926ef47e | |||
| 7cc5ccd2c5 | |||
| 80419a1edf | |||
| db110733a4 | |||
| 1763f666b5 | |||
| 16c703fe31 | |||
| bbcee3f461 | |||
| 56162b650c | |||
| af7e52295a | |||
| c23e459b89 | |||
| 70cffda3f9 | |||
| 7c71f1058d | |||
| a32f76720a | |||
| 84419820b8 | |||
| 0bc3ea51ec | |||
| f3b7d776e8 | |||
| cfb1f63567 | |||
| 54a8c8c3a4 | |||
| 6d0f80f06a | |||
| 4b2708e1e4 | |||
| 80b4730749 | |||
| 00371ef09e | |||
| 646eb4a3b0 | |||
| 0aee428aaf | |||
| 6a003c9035 | |||
| 7fd084bb0c | |||
| 1747395b3e | |||
| b24faa1e08 | |||
| 0a25df39ca | |||
| c0f18d7a10 | |||
| 8225803ec0 | |||
| 42e90734b2 | |||
| 06481118ad | |||
| 875e178c0b | |||
| c5c3c596a0 | |||
| a5fa5727d3 | |||
| 59cdc66355 | |||
| 73551847fb | |||
| 2269eee8cc | |||
| 958ded6988 | |||
| 2b70adc1f6 | |||
| 6a0b36cfa6 | |||
| e80fc1a513 | |||
| 19a51d82be | |||
| 9801337c61 | |||
| 6978516f79 | |||
| ee4a1f762e | |||
| ab49735268 | |||
| 1c5212d756 | |||
| b159dd4452 | |||
| e137e727f6 | |||
| 606aaa0d56 | |||
| ca78149ba3 | |||
| 70a6ca5d77 | |||
| d83d1f3e5a | |||
| ae954b00cb | |||
| d3634d2c8a | |||
| 41a394367b | |||
| 676f963e19 | |||
| b6e4302087 | |||
| 75bf33fbba | |||
| c76ac7b249 | |||
| 57779e960b | |||
| da863956b3 | |||
| d6a40a7bc9 | |||
| 233c35647d | |||
| 43eb6607ba | |||
| 3212a35efb | |||
| c4f94495a8 | |||
| 948fc40b3e | |||
| 427cfe0796 | |||
| 4b135d7586 | |||
| f86b00eced | |||
| 7fb0dffc40 | |||
| da82581eb0 | |||
| ece82d813c | |||
| eb8dcfa053 | |||
| 82e1ce4d7a | |||
| c25c339feb | |||
| 1a4c909cc3 | |||
| ff776293a6 | |||
| 8ac0a27f33 | |||
| f2680bb1de | |||
| bbaf305129 | |||
| c21e2f4a1e | |||
| c763824803 | |||
| 485fe8085c | |||
| c691c349dd | |||
| 0dff13c43f | |||
| 725e44f048 | |||
| 12e916fd0d | |||
| 05da8c0456 | |||
| 1735b64d76 | |||
| 1785e5c3a6 | |||
| ab5eb5c34c | |||
| 94f15d0f4f | |||
| 4fc346ac90 | |||
| ec135e30ed | |||
| dff3d0b04c | |||
| aba8896ecf | |||
| a386a1baad | |||
| cf50132870 | |||
| 62ace421e6 | |||
| dde2e4e780 | |||
| 6c785c7ea2 | |||
| d977407766 | |||
| 71bea66ab7 | |||
| bc13c7b176 | |||
| 9f5ec9113c | |||
| 00565cccb9 | |||
| 8e1cdac3a2 | |||
| 46c0a309da | |||
| 70242c4044 | |||
| 92d4681a23 | |||
| 781582c043 | |||
| e986557d87 | |||
| ef78196d1e | |||
| d8e6aec93d | |||
| 1acde80d61 | |||
| 808c0167f1 | |||
| 69f7f5c236 | |||
| 63e6df0481 | |||
| ea95a7bbd1 | |||
| c77ba121a6 | |||
| fe60d2e26f | |||
| dfb01ce165 | |||
| e3402a1e44 | |||
| e45c126a3f | |||
| 7b9007c699 | |||
| 142a6d6512 | |||
| 18a7875504 | |||
| f3bb4187d7 | |||
| d349cc3e8d | |||
| f6ad67693e | |||
| 43d409b0d7 | |||
| 09a9498d0d | |||
| 05deafdffb | |||
| bd5f2b8f68 | |||
| fd85f1f51a | |||
| 8433ce7f3e | |||
| 94c128ea3d | |||
| a4a0a1d227 | |||
| 47e47e3cc3 | |||
| eb1443a45b | |||
| 5892d62391 | |||
| 9113756923 | |||
| 60c7a4a9a1 | |||
| 79af774569 | |||
| 001ad7490c | |||
| c6ab0e7b8a | |||
| 1b0ea06c6b | |||
| a92a829ca7 | |||
| 199a67fdf3 | |||
| 4451389b6a | |||
| 243d29f7a7 | |||
| 0fd55a3f7c | |||
| a78d917fd2 | |||
| 6842da1d68 | |||
| 21081edfa3 | |||
| 2bdb5a52c4 | |||
| e0b326c565 | |||
| 9490bf29cc | |||
| 30377ab84e | |||
| 99dea8891e | |||
| 820a4efc76 | |||
| 2f6ef7906f | |||
| dc15ede3dc | |||
| f52bcd2415 | |||
| f366b50550 | |||
| 31d9ab048d | |||
| 4ba7306855 | |||
| 3adfb3711f | |||
| 4e9a1839eb | |||
| 0de8035ca9 | |||
| b5eadb64a1 | |||
| f2b629fe6d | |||
| 2fc14ecd0e | |||
| 60cc564743 | |||
| 81d3d2e620 | |||
| 48feb9f656 | |||
| e246e4c0b7 | |||
| 2c9edc47e1 | |||
| 00a012df78 | |||
| 6d50454e72 | |||
| 8eb90ebf06 | |||
| ec0590c79a | |||
| 9c2ed36b5c | |||
| f48c26915f | |||
| 1ab9012446 | |||
| d25b62f7d9 | |||
| cccb5d7785 | |||
| 06d9245778 | |||
| 3d4f35e881 | |||
| b6c3200419 | |||
| 56d0669510 | |||
| 05c4fd37cc | |||
| af00110973 | |||
| da691fa978 | |||
| 3f4148258f | |||
| a03ea3b4d8 | |||
| 4c92da9ab5 | |||
| 31a2fdbcb6 | |||
| 7921f1e548 | |||
| 8ac3f2a6f7 | |||
| 2da6894ee5 | |||
| 9265bc86bc | |||
| c5cd71c8c3 | |||
| 3ecf59c32a | |||
| da2109b310 | |||
| 6d6caa0406 | |||
| e82c1d3a20 | |||
| 500947eb1f | |||
| fe51b671c7 | |||
| 65b72298df | |||
| ba66b246d2 | |||
| 0c6a993f29 | |||
| 8a8de4a1b6 | |||
| 164332d752 | |||
| 173e00af3d | |||
| 4f1d39d3c4 | |||
| 72f20ddd11 | |||
| 0ba481a7a6 | |||
| 31b64b317b | |||
| 302107ed64 | |||
| ef7d3f1c52 | |||
| dffd3efd36 | |||
| a75f64d204 | |||
| c5e11cca58 | |||
| 744780861f | |||
| 498abf3c3d | |||
| 5e45897b8e | |||
| c7dc2d4969 | |||
| 8c163be070 | |||
| 75de4bd305 | |||
| b8fc0fb668 | |||
| 2c7b134931 | |||
| 1eb60f93e6 | |||
| d2e6658c36 | |||
| a24b870faa | |||
| af480e8283 | |||
| a786e4f40c | |||
| f6b65fe0fe | |||
| 1b80d7fd27 | |||
| 7d5be53c4d | |||
| a566509f5b | |||
| 528205f113 | |||
| 9fda3cd49a | |||
| a484628e13 | |||
| 595d92efd9 | |||
| f06dbdec56 | |||
| 9f5f77cbf9 | |||
| dcc1ef311c | |||
| f2d655d25a | |||
| 8d1e78485c | |||
| 10085f9bd9 | |||
| 219c650585 | |||
| ee1803448c | |||
| dee73d5632 | |||
| 6bde14be49 | |||
| 27e84ce518 | |||
| 9dbe3d8d0f | |||
| e6dd302be2 | |||
| 57dbe85ec7 | |||
| 7a36360c4a | |||
| a2eb0bf8d3 | |||
| 4360906883 | |||
| 6541a6d583 | |||
| 0baae9cf5a | |||
| 1a16b74f98 | |||
| 17c4d3e5fb | |||
| fda7165580 | |||
| 489daa6353 | |||
| d7e2ec0860 | |||
| ad409fecfa | |||
| 7d4a17e89c | |||
| a87d5ef8d8 | |||
| 7a4326f98d | |||
| 3c996407d5 | |||
| fb75717ae0 | |||
| 9fafb4f397 | |||
| b4558491e7 | |||
| 0cfa07bfbc | |||
| cb232ab5af | |||
| 71bd124088 | |||
| c615f63673 | |||
| c644224072 | |||
| 0e9f9f2fe1 | |||
| 5f0309d12b | |||
| 1368c2bd50 | |||
| 64e84b092f | |||
| e5cafab9c2 | |||
| 0b56524b7d | |||
| 62127df4f4 | |||
| 62b0ea6616 | |||
| 34dc917271 | |||
| dae04e2aeb | |||
| 17f5c5cd99 | |||
| bf75dec0ce | |||
| 2504c0ec10 | |||
| 08ab0715bd | |||
| 17e4c5cbb3 | |||
| fe0813502d | |||
| 6ff5affb58 | |||
| 56907436a3 | |||
| 33735c1314 | |||
| 872543b5aa | |||
| 1a5b771ae0 | |||
| 26d635791a | |||
| 7701135e67 | |||
| cdf783f2a6 | |||
| af89def3f9 | |||
| cd578db53a | |||
| f6a7af2b12 | |||
| d8877befeb | |||
| fcdd543616 | |||
| 86b680cd41 | |||
| 51b1945957 | |||
| f90a11e9cf | |||
| 4d4e88fb66 | |||
| dffa8e710a | |||
| e53b434dfe | |||
| bc4edb9bde | |||
| aaff54c490 | |||
| ae0a6497cb | |||
| 3a30f536d4 | |||
| 34bd9d93ad | |||
| f7f1d5f54d | |||
| ac625a9032 | |||
| 804d8c766f | |||
| f19da36005 | |||
| dddc38af4c | |||
| 241e265e02 | |||
| b2dd3ed699 | |||
| e67a1a6a1b | |||
| dd0586df6d | |||
| 24f9753bc0 | |||
| c56baf65e3 | |||
| 4dba1e3d94 | |||
| 1172152018 | |||
| 943b26dfea | |||
| c2715a2d7e | |||
| 33b0cda2a0 | |||
| c5c9311d00 | |||
| 076710672c | |||
| 53daa90bff | |||
| 3b4a667f61 | |||
| b6e38815e3 | |||
| 6471114c28 | |||
| 667aacc6c8 | |||
| e9300825dc | |||
| c4f937b4da | |||
| 4600e56b94 | |||
| a068b9ca3d | |||
| dafd914696 | |||
| e4189e92d0 | |||
| 361569ee3f | |||
| c08deb980b | |||
| b9f600409e | |||
| f09a8e3306 | |||
| d9e8625b15 | |||
| 545ea25e43 | |||
| 02d9e8328f | |||
| 42a0530777 | |||
| 3ce6a6fe95 | |||
| 9d1b2a7c72 | |||
| 06a3686739 | |||
| 3c2f186891 | |||
| 15c27c164f | |||
| ae8ca54a07 | |||
| c27c05ac5d | |||
| 3e0e3f9984 | |||
| 00e7b4a9d5 | |||
| f610489a61 | |||
| 9d15e8d0a4 | |||
| 6ae103850b | |||
| f1f3223922 | |||
| 8e392a9bff | |||
| e89cf9dbb4 | |||
| f8af307159 | |||
| 74473427df | |||
| 18f448c733 | |||
| 0a591a3f09 | |||
| 8dcc28376c | |||
| 3f3a02ba8c | |||
| d4b8b12687 | |||
| 9cfe396d0f | |||
| 85d8ad09c6 | |||
| 3fc2583470 | |||
| df9e039fce | |||
| 3efa54b68a | |||
| 90e7541bc1 | |||
| 91deb9b7c1 | |||
| 66c5424549 | |||
| c43751d2dc | |||
| 80baa6798b | |||
| 85407f3e11 | |||
| 789512b10a | |||
| 820d8da7d5 | |||
| 9db7830726 | |||
| ff83ce5254 | |||
| 69d16edfc1 | |||
| 77bd483ce4 | |||
| 5ff9efb7d6 | |||
| e4574b0260 | |||
| 126ab38475 | |||
| 18390443ff | |||
| 71e280061c | |||
| 7c50ee814d | |||
| 3ed6d4b2f7 | |||
| 7a2af3d013 | |||
| 1108cb7e9a | |||
| c0317aca58 | |||
| 015d5a4d65 | |||
| ea646a5723 | |||
| 9e508f0a02 | |||
| 070bf7c0f9 | |||
| 7224255775 | |||
| 6ff535c406 | |||
| 02e721ce17 | |||
| e0256dd535 | |||
| 328015bce7 | |||
| b03a33f534 | |||
| 7cd012de70 | |||
| 6ced14d0e8 | |||
| 9fd4abec25 | |||
| a452495c22 | |||
| 3c93167abb | |||
| 7717bff367 | |||
| be6db801ee | |||
| 427f808180 | |||
| 76bf4ae825 | |||
| e02a518583 | |||
| 79ddd122a4 | |||
| b1fa7e4e09 | |||
| 015c3004f5 | |||
| 550ba4f768 | |||
| 1d4069d4fa |
@@ -4,9 +4,24 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- development
|
||||
paths:
|
||||
- 'src/**'
|
||||
|
||||
jobs:
|
||||
wait-for-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Wait for Test workflow to complete
|
||||
uses: lewagon/wait-on-check-action@v1.4.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
check-name: 'lint'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
allowed-conclusions: success
|
||||
|
||||
publish:
|
||||
needs: wait-for-lint
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
|
||||
Vendored
+5
-4
@@ -14,9 +14,7 @@
|
||||
".eslintignore": "ignore"
|
||||
},
|
||||
"eslint.validate": ["typescript", "typescriptreact"],
|
||||
"eslint.workingDirectories": [
|
||||
{ "directory": "./", "changeProcessCWD": true },
|
||||
],
|
||||
"eslint.workingDirectories": [{ "directory": "./", "changeProcessCWD": true }],
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
@@ -50,7 +48,10 @@
|
||||
"typescript.preferences.autoImportFileExcludePatterns": [
|
||||
"@mantine/core",
|
||||
"@mantine/modals",
|
||||
"@mantine/dates"
|
||||
"@mantine/dates",
|
||||
"@mantine/hooks",
|
||||
"@mantine/form",
|
||||
"@radix-ui/react-context-menu"
|
||||
],
|
||||
"[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
||||
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true,
|
||||
|
||||
@@ -98,25 +98,22 @@ docker run --name feishin -p 9180:9180 feishin
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
To install via Docker Compose use the following snippit. This also works on Portainer.
|
||||
To install via Docker Compose, use the following snippet. This also works on Portainer.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
feishin:
|
||||
container_name: feishin
|
||||
image: 'ghcr.io/jeffvli/feishin:latest'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SERVER_NAME=jellyfin # pre defined server name
|
||||
- SERVER_NAME=jellyfin # pre-defined server name
|
||||
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
|
||||
- SERVER_TYPE=jellyfin # navidrome also works
|
||||
- SERVER_URL= # http://address:port
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- UMASK=002
|
||||
- TZ=America/Los_Angeles
|
||||
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
|
||||
- SERVER_URL= # http://address:port or https://address:port
|
||||
ports:
|
||||
- 9180:9180
|
||||
restart: unless-stopped
|
||||
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
|
||||
```
|
||||
|
||||
### Configuration
|
||||
@@ -126,11 +123,11 @@ services:
|
||||
2. After restarting the app, you will be prompted to select a server. Click the `Open menu` button and select `Manage servers`. Click the `Add server` button in the popup and fill out all applicable details. You will need to enter the full URL to your server, including the protocol and port if applicable (e.g. `https://navidrome.my-server.com` or `http://192.168.0.1:4533`).
|
||||
|
||||
- **Navidrome** - For the best experience, select "Save password" when creating the server and configure the `SessionTimeout` setting in your Navidrome config to a larger value (e.g. 72h).
|
||||
- **Linux users** - The default password store uses `libsecret`. `kwallet4/5/6` are also supported, but must be explicitly set in Settings > Window > Passwords/secret score.
|
||||
- **Linux users** - The default password store uses `libsecret`. `kwallet4/5/6` are also supported, but must be explicitly set in Settings > Window > Passwords/secret store.
|
||||
|
||||
3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`.
|
||||
|
||||
4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set.
|
||||
4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome` or `subsonic`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set.
|
||||
|
||||
## FAQ
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7
-7
@@ -1,13 +1,13 @@
|
||||
version: '3.5'
|
||||
services:
|
||||
feishin:
|
||||
container_name: feishin
|
||||
image: ghcr.io/jeffvli/feishin:latest
|
||||
image: 'ghcr.io/jeffvli/feishin:latest'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SERVER_NAME=jellyfin # pre-defined server name
|
||||
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
|
||||
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
|
||||
- SERVER_URL= # http://address:port or https://address:port
|
||||
ports:
|
||||
- 9180:9180
|
||||
environment:
|
||||
- SERVER_NAME=jellyfin # pre defined server name
|
||||
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
|
||||
- SERVER_TYPE=jellyfin # navidrome also works
|
||||
- SERVER_URL= # http://address:port
|
||||
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
|
||||
@@ -44,6 +44,7 @@ dmg:
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- deb
|
||||
- tar.xz
|
||||
category: AudioVideo;Audio;Player
|
||||
icon: assets/icons/icon.png
|
||||
|
||||
@@ -44,6 +44,7 @@ dmg:
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- deb
|
||||
- tar.xz
|
||||
category: AudioVideo;Audio;Player
|
||||
icon: assets/icons/icon.png
|
||||
|
||||
@@ -45,6 +45,11 @@ const config: UserConfig = {
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
build: {
|
||||
cssMinify: 'esbuild',
|
||||
minify: 'esbuild',
|
||||
sourcemap: true,
|
||||
},
|
||||
css: {
|
||||
modules: {
|
||||
generateScopedName: 'fs-[name]-[local]',
|
||||
|
||||
@@ -43,6 +43,8 @@ export default tseslint.config(
|
||||
'no-unused-vars': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
quotes: ['error', 'single'],
|
||||
'react-hooks/refs': 'off',
|
||||
'react-hooks/set-state-in-effect': 'off',
|
||||
'react-refresh/only-export-components': 'off',
|
||||
'react/display-name': 'off',
|
||||
semi: ['error', 'always'],
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Generated
+18279
File diff suppressed because it is too large
Load Diff
+76
-76
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "feishin",
|
||||
"version": "0.22.0",
|
||||
"version": "1.0.1",
|
||||
"description": "A modern self-hosted music player.",
|
||||
"keywords": [
|
||||
"subsonic",
|
||||
@@ -16,11 +16,12 @@
|
||||
"license": "GPL-3.0",
|
||||
"author": {
|
||||
"name": "jeffvli",
|
||||
"email": "feishin@users.noreply.github.com",
|
||||
"url": "https://github.com/jeffvli/"
|
||||
},
|
||||
"main": "./out/main/index.js",
|
||||
"scripts": {
|
||||
"build": "pnpm run typecheck && pnpm run build:electron && pnpm run build:remote",
|
||||
"build": "pnpm run build:electron && pnpm run build:remote",
|
||||
"build:electron": "electron-vite build",
|
||||
"build:remote": "vite build --config remote.vite.config.ts",
|
||||
"build:web": "vite build --config web.vite.config.ts",
|
||||
@@ -60,75 +61,75 @@
|
||||
"postversion": "node ./scripts/update-app-stream.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-grid-community/client-side-row-model": "^28.2.1",
|
||||
"@ag-grid-community/core": "^28.2.1",
|
||||
"@ag-grid-community/infinite-row-model": "^28.2.1",
|
||||
"@ag-grid-community/react": "^28.2.1",
|
||||
"@ag-grid-community/styles": "^28.2.1",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.4.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@mantine/colors-generator": "^8.2.8",
|
||||
"@mantine/core": "^8.2.8",
|
||||
"@mantine/dates": "^8.2.8",
|
||||
"@mantine/form": "^8.2.8",
|
||||
"@mantine/hooks": "^8.2.8",
|
||||
"@mantine/modals": "^8.2.8",
|
||||
"@mantine/notifications": "^8.2.8",
|
||||
"@tanstack/react-query": "^5.89.0",
|
||||
"@tanstack/react-query-devtools": "^5.89.0",
|
||||
"@tanstack/react-query-persist-client": "^5.89.0",
|
||||
"@ts-rest/core": "^3.23.0",
|
||||
"@mantine/colors-generator": "^8.3.8",
|
||||
"@mantine/core": "^8.3.8",
|
||||
"@mantine/dates": "^8.3.8",
|
||||
"@mantine/form": "^8.3.8",
|
||||
"@mantine/hooks": "^8.3.8",
|
||||
"@mantine/modals": "^8.3.8",
|
||||
"@mantine/notifications": "^8.3.8",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@tanstack/react-query": "^5.90.9",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tanstack/react-query-persist-client": "^5.90.11",
|
||||
"@ts-rest/core": "^3.52.1",
|
||||
"@wavesurfer/react": "^1.0.11",
|
||||
"@xhayper/discord-rpc": "^1.3.0",
|
||||
"audiomotion-analyzer": "^4.5.0",
|
||||
"auto-text-size": "^0.2.3",
|
||||
"axios": "^1.12.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"dayjs": "^1.11.6",
|
||||
"dompurify": "^3.1.6",
|
||||
"audiomotion-analyzer": "^4.5.1",
|
||||
"axios": "^1.13.2",
|
||||
"butterchurn": "^2.6.7",
|
||||
"butterchurn-presets": "^2.4.7",
|
||||
"cheerio": "^1.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dayjs": "^1.11.19",
|
||||
"dompurify": "^3.3.0",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-log": "^5.1.1",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"fast-average-color": "^9.3.0",
|
||||
"fast-xml-parser": "^5.3.0",
|
||||
"format-duration": "^2.0.0",
|
||||
"fuse.js": "^6.6.2",
|
||||
"i18next": "^21.10.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"immer": "^9.0.21",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"fast-average-color": "^9.5.0",
|
||||
"fast-xml-parser": "^5.3.2",
|
||||
"format-duration": "^3.0.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"i18next": "^25.6.2",
|
||||
"icecast-metadata-stats": "^0.1.12",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"immer": "^10.2.0",
|
||||
"is-electron": "^2.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"motion": "^12.18.1",
|
||||
"motion": "^12.23.24",
|
||||
"mpris-service": "^2.1.2",
|
||||
"nanoid": "^3.3.3",
|
||||
"nanoid": "^3.3.11",
|
||||
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
|
||||
"nuqs": "^2.7.1",
|
||||
"overlayscrollbars": "^2.11.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"qs": "^6.14.0",
|
||||
"react": "^19.1.0",
|
||||
"react-call": "^1.8.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-i18next": "^16.3.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-image": "^4.1.0",
|
||||
"react-loading-skeleton": "^3.5.0",
|
||||
"react-player": "^2.11.0",
|
||||
"react-router": "^6.16.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.17",
|
||||
"react-window": "^1.8.9",
|
||||
"react-window-infinite-loader": "^1.0.9",
|
||||
"react-player": "^2.16.0",
|
||||
"react-router": "^7.9.6",
|
||||
"react-split-pane": "^3.0.4",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "1.8.11",
|
||||
"react-window-v2": "npm:react-window@^2.2.3",
|
||||
"semver": "^7.5.4",
|
||||
"swiper": "^9.3.1",
|
||||
"use-sync-external-store": "^1.5.0",
|
||||
"string-to-color": "^2.2.2",
|
||||
"wavesurfer.js": "^7.11.1",
|
||||
"ws": "^8.18.2",
|
||||
"zod": "^3.22.3",
|
||||
"zustand": "^5.0.5"
|
||||
@@ -136,45 +137,44 @@
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||
"@types/electron-localshortcut": "^3.1.0",
|
||||
"@types/lodash": "^4.17.18",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"concurrently": "^7.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"electron": "^38.5.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-vite": "^3.1.0",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
"electron-vite": "^4.0.1",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-perfectionist": "^4.13.0",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"i18next-parser": "^9.0.2",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-packagejson": "^2.5.14",
|
||||
"sass-embedded": "^1.89.0",
|
||||
"stylelint": "^16.14.1",
|
||||
"stylelint-config-css-modules": "^4.4.0",
|
||||
"stylelint-config-recess-order": "^7.1.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"i18next-parser": "^9.3.0",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-packagejson": "^2.5.19",
|
||||
"stylelint": "^16.25.0",
|
||||
"stylelint-config-css-modules": "^4.5.1",
|
||||
"stylelint-config-recess-order": "^7.4.0",
|
||||
"stylelint-config-standard": "^39.0.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.4.1",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-conditional-import": "^0.1.7",
|
||||
"vite-plugin-dynamic-import": "^1.6.0",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"vite-plugin-pwa": "^1.0.3"
|
||||
"vite-plugin-pwa": "^1.1.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
||||
Generated
+1868
-1572
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,16 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-2xl': '120em',
|
||||
'mantine-breakpoint-3xl': '160em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,9 @@ import { version } from './package.json';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
cssMinify: 'esbuild',
|
||||
emptyOutDir: true,
|
||||
minify: 'esbuild',
|
||||
outDir: path.resolve(__dirname, './out/remote'),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
|
||||
+7
-2
@@ -1,4 +1,4 @@
|
||||
import { PostProcessorModule, StringMap, TOptions } from 'i18next';
|
||||
import { PostProcessorModule, TOptions } from 'i18next';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
@@ -207,7 +207,12 @@ const ignoreSentenceCaseLanguages = ['de'];
|
||||
|
||||
const sentenceCasePostProcessor: PostProcessorModule = {
|
||||
name: 'sentenceCase',
|
||||
process: (value: string, _key: string, _options: TOptions<StringMap>, translator: any) => {
|
||||
process: (
|
||||
value: string,
|
||||
_key: string,
|
||||
_options: TOptions<Record<string, string>>,
|
||||
translator: any,
|
||||
) => {
|
||||
const sentences = value.split('. ');
|
||||
|
||||
return sentences
|
||||
|
||||
+240
-35
@@ -13,7 +13,9 @@
|
||||
"settings": "$t(common.setting_other)",
|
||||
"tracks": "$t(entity.track_other)",
|
||||
"nowPlaying": "ara sona",
|
||||
"shared": "$t(entity.playlist_other) compartida"
|
||||
"shared": "$t(entity.playlist_other) compartida",
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"albumArtistDetail": {
|
||||
"relatedArtists": "$t(entity.artist_other) similars",
|
||||
@@ -51,7 +53,11 @@
|
||||
"version": "versió {{version}}",
|
||||
"openBrowserDevtools": "obre les eines de desenvolupament del navegador",
|
||||
"privateModeOff": "desactiva el mode privat",
|
||||
"privateModeOn": "activa el mode privat"
|
||||
"privateModeOn": "activa el mode privat",
|
||||
"commandPalette": "obre la paleta d'ordres",
|
||||
"selectMusicFolder": "selecciona una carpeta de música",
|
||||
"noMusicFolder": "no s'ha seleccionat cap carpeta de música",
|
||||
"multipleMusicFolders": "{{count}} carpetes de música seleccionades"
|
||||
},
|
||||
"contextMenu": {
|
||||
"addFavorite": "$t(action.addToFavorites)",
|
||||
@@ -77,7 +83,9 @@
|
||||
"numberSelected": "{{count}} seleccionat",
|
||||
"shareItem": "comparteix l'element",
|
||||
"goToAlbumArtist": "Ves a $t(entity.albumArtist_one)",
|
||||
"goToAlbum": "ves a $t(entity.album_one)"
|
||||
"goToAlbum": "ves a $t(entity.album_one)",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"goTo": "ves a"
|
||||
},
|
||||
"genreList": {
|
||||
"title": "$t(entity.genre_other)",
|
||||
@@ -90,7 +98,8 @@
|
||||
"newlyAdded": "afegits recentment",
|
||||
"mostPlayed": "els més reproduïts",
|
||||
"recentlyPlayed": "reproduït recentment",
|
||||
"recentlyReleased": "estrenat fa poc"
|
||||
"recentlyReleased": "estrenat fa poc",
|
||||
"genres": "$t(entity.genre_other)"
|
||||
},
|
||||
"playlistList": {
|
||||
"title": "$t(entity.playlist_other)"
|
||||
@@ -136,7 +145,24 @@
|
||||
"generalTab": "general",
|
||||
"hotkeysTab": "tecles d'accés ràpid",
|
||||
"playbackTab": "reproducció",
|
||||
"windowTab": "finestra"
|
||||
"windowTab": "finestra",
|
||||
"analytics": "analítiques",
|
||||
"updates": "actualitza",
|
||||
"cache": "memòria cau",
|
||||
"application": "aplicació",
|
||||
"queryBuilder": "constructor de consultes",
|
||||
"theme": "tema",
|
||||
"controls": "controls",
|
||||
"sidebar": "barra lateral",
|
||||
"remote": "remot",
|
||||
"exportImport": "importa/exporta",
|
||||
"scrobble": "scrobble",
|
||||
"audio": "àudio",
|
||||
"lyrics": "lletra",
|
||||
"transcoding": "transcodificació",
|
||||
"discord": "discord",
|
||||
"logger": "registres",
|
||||
"playerFilters": "filtres de reproducció"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
@@ -153,6 +179,15 @@
|
||||
},
|
||||
"playlist": {
|
||||
"reorder": "el reordenament només s'activa quan s'ordena per id"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite_other)"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "emissores de ràdio"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -260,7 +295,18 @@
|
||||
"private": "privat",
|
||||
"public": "públic",
|
||||
"recordLabel": "segell discogràfic",
|
||||
"releaseType": "tipus de llançament"
|
||||
"releaseType": "tipus de llançament",
|
||||
"doNotShowAgain": "no tornis a mostrar això",
|
||||
"view": "mostra",
|
||||
"externalLinks": "enllaços externs",
|
||||
"faster": "més ràpid",
|
||||
"noFilters": "cap filtre configurat",
|
||||
"slower": "més lent",
|
||||
"sort": "ordre",
|
||||
"gridRows": "files de la quadrícula",
|
||||
"tableColumns": "columnes de la taula",
|
||||
"itemsMore": "{{count}} més",
|
||||
"countSelected": "{{count}} seleccionats"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "àlbum",
|
||||
@@ -314,7 +360,13 @@
|
||||
"song_other": "cançons",
|
||||
"favorite_one": "preferit",
|
||||
"favorite_many": "preferits",
|
||||
"favorite_other": "preferits"
|
||||
"favorite_other": "preferits",
|
||||
"radioStation_one": "emissora de ràdio",
|
||||
"radioStation_many": "emissores de ràdio",
|
||||
"radioStation_other": "emissores de ràdio",
|
||||
"radioStationWithCount_one": "{{count}} emissora de ràdio",
|
||||
"radioStationWithCount_many": "{{count}} emissores de ràdio",
|
||||
"radioStationWithCount_other": "{{count}} emissores de ràdio"
|
||||
},
|
||||
"form": {
|
||||
"addToPlaylist": {
|
||||
@@ -323,7 +375,7 @@
|
||||
"input_skipDuplicates": "salta't els duplicats",
|
||||
"success": "s'ha afegit $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"create": "crea $t(entity.playlist_one) {{playlist}}",
|
||||
"searchOrCreate": "cerca $t(entity.playlist_other) o tipus per crear-ne un de nou"
|
||||
"searchOrCreate": "cerca $t(entity.playlist_other) o escriu per crear-ne una de nova"
|
||||
},
|
||||
"createPlaylist": {
|
||||
"input_description": "$t(common.description)",
|
||||
@@ -341,7 +393,8 @@
|
||||
"editPlaylist": {
|
||||
"success": "$t(entity.playlist_one) s'ha actualitzat amb èxit",
|
||||
"title": "editar la $t(entity.playlist_one)",
|
||||
"publicJellyfinNote": "Per algun motiu, Jellyfin no exposa si una llista de reproducció és pública o no. Si voleu que es mantingui pública, seleccioneu la següent entrada"
|
||||
"publicJellyfinNote": "Per algun motiu, Jellyfin no exposa si una llista de reproducció és pública o no. Si voleu que es mantingui pública, seleccioneu la següent entrada",
|
||||
"editNote": "es recomana no editar manualment les llistes de reproducció grans. segur que accepteu el risc de perdre dades si sobreescriviu la llista de reproducció existent?"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_artist": "$t(entity.artist_one)",
|
||||
@@ -378,24 +431,53 @@
|
||||
"queryEditor": {
|
||||
"title": "editor de consultes",
|
||||
"input_optionMatchAll": "coincidències totals",
|
||||
"input_optionMatchAny": "coincidències parcials"
|
||||
"input_optionMatchAny": "coincidències parcials",
|
||||
"addRuleGroup": "afegeix el grup de regles",
|
||||
"removeRuleGroup": "elimina el grup de regles",
|
||||
"resetToDefault": "reestableix als valors predeterminats",
|
||||
"clearFilters": "neteja els filtres"
|
||||
},
|
||||
"privateMode": {
|
||||
"enabled": "mode privat actiu; l'estat de reproducció ara està ocult d'integracions externes",
|
||||
"disabled": "mode privat inactiu; l'estat de reproducció ara és visible per les integracions externes",
|
||||
"title": "mode privat"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "afegeix elements a la cua",
|
||||
"description": "Aquesta acció afegirà tots els elements a la vista filtrada actual"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "reprodueix a l'atzar",
|
||||
"input_genre": "$t(entity.genre_one)",
|
||||
"input_limit": "quantes cançons?",
|
||||
"input_minYear": "de l'any",
|
||||
"input_maxYear": "fins a l'any",
|
||||
"input_played": "reprodueix el filtre",
|
||||
"input_played_optionAll": "totes les pistes",
|
||||
"input_played_optionUnplayed": "només les pistes sense reproduir",
|
||||
"input_played_optionPlayed": "només les pistes reproduïdes"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "emissora de ràdio creada amb èxit",
|
||||
"title": "crea una emissora de ràdio",
|
||||
"input_homepageUrl": "URL de la pàgina d'inici",
|
||||
"input_name": "nom",
|
||||
"input_streamUrl": "URL de transmissió"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "cua de reproducció desada al servidor"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"addToFavorites": "afegir als $t(entity.favorite_other)",
|
||||
"addToPlaylist": "afegir a $t(entity.playlist_one)",
|
||||
"createPlaylist": "crear $t(entity.playlist_one)",
|
||||
"addToFavorites": "afegeix a $t(entity.favorite_other)",
|
||||
"addToPlaylist": "afegeix a $t(entity.playlist_one)",
|
||||
"createPlaylist": "crea $t(entity.playlist_one)",
|
||||
"deletePlaylist": "elimina la $t(entity.playlist_one)",
|
||||
"editPlaylist": "edita la $t(entity.playlist_one)",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromFavorites": "elimina dels $t(entity.favorite_other)",
|
||||
"removeFromPlaylist": "elimina de $t(entity.playlist_one)",
|
||||
"clearQueue": "buidar la cua",
|
||||
"clearQueue": "buida la cua",
|
||||
"removeFromQueue": "treure de la cua",
|
||||
"goToPage": "anar a la pàgina",
|
||||
"openIn": {
|
||||
@@ -408,7 +490,23 @@
|
||||
"moveToBottom": "anar al final",
|
||||
"moveToTop": "anar al principi",
|
||||
"setRating": "Qualifica",
|
||||
"toggleSmartPlaylistEditor": "canvia l'editor $t(entity.smartPlaylist)"
|
||||
"toggleSmartPlaylistEditor": "canvia l'editor $t(entity.smartPlaylist)",
|
||||
"downloadStarted": "s'ha iniciat la descàrrega de {{count}} elements",
|
||||
"moveUp": "mou amunt",
|
||||
"moveDown": "mou avall",
|
||||
"holdToMoveToTop": "mantingueu premut per moure al capdamunt",
|
||||
"holdToMoveToBottom": "mantingueu premut per moure al capdavall",
|
||||
"moveItems": "mou elements",
|
||||
"shuffle": "mescla",
|
||||
"shuffleAll": "mescla-ho tot",
|
||||
"shuffleSelected": "mescla els seleccionats",
|
||||
"viewMore": "mostra'n més",
|
||||
"createRadioStation": "crea $t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "elimina $t(entity.radioStation_one)",
|
||||
"addOrRemoveFromSelection": "afegeix o elimina de la selecció",
|
||||
"selectRangeOfItems": "selecciona un interval d'elements",
|
||||
"selectAll": "selecciona-ho tot",
|
||||
"openApplicationDirectory": "obre el directori de l'aplicació"
|
||||
},
|
||||
"setting": {
|
||||
"language_description": "estableix l'idioma de l'aplicació ($t(common.restartRequired))",
|
||||
@@ -490,14 +588,10 @@
|
||||
"discordServeImage": "serveix imatges de {{discord}} des del servidor",
|
||||
"discordServeImage_description": "comparteix la caràtula per l'estat d'activitat de {{discord}} des del servidor; només disponible per Jellyfin i Navidrome. {{discord}} fa ser un bot per trobar les imatges, de manera que el vostre servidor ha de ser visible per l'internet públic",
|
||||
"discordUpdateInterval": "interval d'actualització de l'estat d'activitat de {{discord}}",
|
||||
"doubleClickBehavior": "posa en cua totes les pistes cercades en fer doble clic",
|
||||
"doubleClickBehavior_description": "si està actiu, totes les pistes coincidents en una cerca de pistes es posaran a la cua. altrament, només la que seleccioneu s'afegirà a la cua",
|
||||
"externalLinks": "mostra enllaços externs",
|
||||
"externalLinks_description": "permet mostrar enllaços externs (Last.fm, MusicBrainz) a les pàgines d'artista/àlbum",
|
||||
"exitToTray": "surt a la safata",
|
||||
"exitToTray_description": "en sortir de l'aplicació, minimitza-la a la safa del sistema",
|
||||
"floatingQueueArea": "mostra la zona flotant de la cua",
|
||||
"floatingQueueArea_description": "mostra una icona flotant al costat dret de la pantalla per veure la cua de reproducció",
|
||||
"followLyric": "segueix la lletra actual",
|
||||
"followLyric_description": "desplaça la lletra a la posició de reproducció actual",
|
||||
"preferLocalLyrics": "prefereix les lletres locals",
|
||||
@@ -507,8 +601,6 @@
|
||||
"gaplessAudio": "àudio sense pauses",
|
||||
"gaplessAudio_description": "estableix la configuració d'àudio sense pauses per mpv",
|
||||
"gaplessAudio_optionWeak": "feble (recomanat)",
|
||||
"genreBehavior": "comportament predeterminat per les pàgines de gènere",
|
||||
"genreBehavior_description": "determina si clicar sobre un gènere obre per defecte la llista de pistes o d'àlbums",
|
||||
"globalMediaHotkeys": "tecles de drecera globals",
|
||||
"globalMediaHotkeys_description": "activa o desactiva l'ús de les tecles multimèdia del sistema per controlar la reproducció",
|
||||
"homeConfiguration_description": "configura quins objectes es mostren, i en quin ordre, a la pàgina d'inici",
|
||||
@@ -578,8 +670,6 @@
|
||||
"playbackStyle_optionNormal": "normal",
|
||||
"playButtonBehavior": "comportament del botó de reproducció",
|
||||
"playButtonBehavior_description": "estableix el comportament predeterminat del botó de reproducció quan s'afegeixen cançons a la cua",
|
||||
"playerAlbumArtResolution": "resolució de la caràtula de l'àlbum al reproductor",
|
||||
"playerAlbumArtResolution_description": "la resolució de la previsualització gran de la caràtula al reproductor. si és més alta, serà més nítida, però es carregarà més lent. el valor predeterminat 0 vol dir automàtic",
|
||||
"playerbarOpenDrawer": "activa el reproductor en pantalla completa",
|
||||
"playerbarOpenDrawer_description": "permet fer clic a la barra de reproducció per obrir el reproductor de pantalla completa",
|
||||
"remotePassword": "contrasenya del servidor de control remot",
|
||||
@@ -679,7 +769,52 @@
|
||||
"exportImportSettings_importSuccess": "la configuració s'ha importat correctament!",
|
||||
"exportImportSettings_notValidJSON": "el fitxer que heu seleccionat no és un JSON vàlid",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" és incorrecte: {{reason}}",
|
||||
"language": "llengua"
|
||||
"language": "llengua",
|
||||
"notify": "activa les notificacions de cançons",
|
||||
"notify_description": "mostra notificacions quan la cançó actual canviï",
|
||||
"transcode": "activa la transcodificació",
|
||||
"autoDJ": "DJ automàtic",
|
||||
"autoDJ_description": "afegeix cançons similars a la cua automàticament",
|
||||
"autoDJ_itemCount": "número d'elements",
|
||||
"autoDJ_itemCount_description": "el nombre d'elements que s'intenten afegir a la cua quan el DJ automàtic està activat",
|
||||
"autoDJ_timing": "temps",
|
||||
"autoDJ_timing_description": "el nombre de cançons que han de quedar a la cua per activar el DJ automàtic",
|
||||
"analyticsDisable": "Desactiva les analítiques basades en l'ús",
|
||||
"analyticsDisable_description": "S'envien dades d'ús anonimitzades al desenvolupador per ajudar a millorar l'aplicació",
|
||||
"followCurrentSong_description": "desplaça automàticament la cua de reproducció a la cançó en reproducció",
|
||||
"followCurrentSong": "segueix la cançó actual",
|
||||
"logLevel": "nivell de registre",
|
||||
"logLevel_description": "estableix el nivell mínim de registre que s'ha de mostrar. debug mostra tots els missatges, error mostra només els errors",
|
||||
"logLevel_optionDebug": "debug",
|
||||
"logLevel_optionError": "error",
|
||||
"logLevel_optionInfo": "info",
|
||||
"logLevel_optionWarn": "avís",
|
||||
"playerFilters": "filtra les cançons de la cua",
|
||||
"playerFilters_description": "evita afegir cançons a la cua segons els següents criteris",
|
||||
"playerbarSlider": "barra lliscadora de reproducció",
|
||||
"playerbarSlider_description": "la forma d'ona no es recomana si utilitzeu una connexió d'internet lenta o mesurada",
|
||||
"playerbarSliderType_optionSlider": "lliscador",
|
||||
"playerbarSliderType_optionWaveform": "forma d'ona",
|
||||
"playerbarWaveformAlign": "alineament de la forma d'ona",
|
||||
"playerbarWaveformAlign_optionTop": "superior",
|
||||
"playerbarWaveformAlign_optionCenter": "centrat",
|
||||
"playerbarWaveformAlign_optionBottom": "inferior",
|
||||
"playerbarWaveformBarWidth": "amplada de la forma d'ona",
|
||||
"playerbarWaveformGap": "buits de la forma d'ona",
|
||||
"playerbarWaveformRadius": "radi de la forma d'ona",
|
||||
"showLyricsInSidebar_description": "s'afegirà un tauler a la cua de reproducció que mostra la lletra",
|
||||
"showLyricsInSidebar": "mostra la lletra a la barra lateral del reproductor",
|
||||
"showVisualizerInSidebar_description": "s'afegirà un tauler a la barra lateral de reproducció que mostra el visualitzador",
|
||||
"showVisualizerInSidebar": "mostra el visualitzador a la barra laterla de reproducció",
|
||||
"audioFadeOnStatusChange": "fosa d'àudio en canviar d'estat",
|
||||
"audioFadeOnStatusChange_description": "activa la fosa de sortida i entrada en reproduir o pausar",
|
||||
"queryBuilder": "constructor de consultes",
|
||||
"queryBuilderCustomFields_inputLabel": "discogràfica",
|
||||
"queryBuilderCustomFields_inputTag": "etiqueta",
|
||||
"queryBuilderCustomFields": "camps personalitzats",
|
||||
"queryBuilderCustomFields_description": "afegeix camps personalitzats pel constructor de consultes",
|
||||
"useThemeAccentColor": "fes servir el color d'accent del tema",
|
||||
"useThemeAccentColor_description": "fes servir el color primari definit pel tema seleccionat en comptes del color d'accent personalitzat"
|
||||
},
|
||||
"table": {
|
||||
"column": {
|
||||
@@ -706,7 +841,10 @@
|
||||
"lastPlayed": "última reproducció",
|
||||
"path": "ruta",
|
||||
"rating": "qualificació",
|
||||
"title": "títol"
|
||||
"title": "títol",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)",
|
||||
"owner": "propietari"
|
||||
},
|
||||
"config": {
|
||||
"general": {
|
||||
@@ -717,7 +855,28 @@
|
||||
"displayType": "tipus de visualització",
|
||||
"itemGap": "espai entre elements (px)",
|
||||
"itemSize": "mida dels elements (px)",
|
||||
"tableColumns": "columnes de la taula"
|
||||
"tableColumns": "columnes de la taula",
|
||||
"advancedSettings": "opcions avançades",
|
||||
"autosize": "dimensions automàtiques",
|
||||
"moveUp": "mou amunt",
|
||||
"moveDown": "mou avall",
|
||||
"pinToLeft": "ancora a l'esquerra",
|
||||
"pinToRight": "ancora a la dreta",
|
||||
"alignLeft": "alinea a l'esquerra",
|
||||
"alignCenter": "alinea al centre",
|
||||
"alignRight": "alinea a la dreta",
|
||||
"itemsPerRow": "elements per fila",
|
||||
"size_default": "predeterminat",
|
||||
"size_compact": "compacte",
|
||||
"size_large": "gran",
|
||||
"pagination": "paginació",
|
||||
"pagination_itemsPerPage": "elements per pàgina",
|
||||
"pagination_infinite": "infinita",
|
||||
"pagination_paginate": "paginada",
|
||||
"alternateRowColors": "colors de fila alternants",
|
||||
"horizontalBorders": "vores de fila",
|
||||
"rowHoverHighlight": "ressalta en passar el cursor per la fila",
|
||||
"verticalBorders": "vores de columna"
|
||||
},
|
||||
"label": {
|
||||
"actions": "$t(common.action_other)",
|
||||
@@ -747,14 +906,17 @@
|
||||
"discNumber": "número de disc",
|
||||
"lastPlayed": "última reproducció",
|
||||
"rowIndex": "índex de files",
|
||||
"titleCombined": "$t(common.title) (combinat)"
|
||||
"titleCombined": "$t(common.title) (combinat)",
|
||||
"albumCount": "$t(entity.album_other)",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"genreBadge": "$t(entity.genre_one) (insígnies)",
|
||||
"image": "imatge",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
},
|
||||
"view": {
|
||||
"table": "taula",
|
||||
"card": "targeta",
|
||||
"grid": "quadrícula",
|
||||
"list": "llista",
|
||||
"poster": "pòster"
|
||||
"list": "llista"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -816,10 +978,10 @@
|
||||
"playSimilarSongs": "reproduir cançons similars",
|
||||
"repeat_off": "repetició desactivada",
|
||||
"repeat_all": "repetició",
|
||||
"shuffle": "reproducció aleatòria",
|
||||
"shuffle": "reprodueix (mesclat)",
|
||||
"shuffle_off": "reproducció aleatòria desactivada",
|
||||
"addLast": "afegeix al final",
|
||||
"addNext": "afegeix a continuació",
|
||||
"addLast": "al final",
|
||||
"addNext": "a continuació",
|
||||
"favorite": "marcar com a preferida",
|
||||
"mute": "silencia",
|
||||
"next": "següent",
|
||||
@@ -834,7 +996,16 @@
|
||||
"skip_forward": "salta endavant",
|
||||
"toggleFullscreenPlayer": "activa el reproductor de pantalla completa",
|
||||
"unfavorite": "elimina de preferits",
|
||||
"pause": "pausa"
|
||||
"pause": "pausa",
|
||||
"addLastShuffled": "al final (mesclat)",
|
||||
"addNextShuffled": "a continuació (mesclat)",
|
||||
"holdToShuffle": "mantén premut per mesclar",
|
||||
"queueType": "tipus de cua",
|
||||
"queueType_default": "predeterminat",
|
||||
"queueType_priority": "prioritat",
|
||||
"lyrics": "lletra",
|
||||
"restoreQueueFromServer": "restaura la cua del servidor",
|
||||
"saveQueueToServer": "desa la cua al servidor"
|
||||
},
|
||||
"error": {
|
||||
"credentialsRequired": "credencials requerides",
|
||||
@@ -860,7 +1031,10 @@
|
||||
"notificationDenied": "s'han negat els permisos per enviar notificacions. aquesta opció no té cap efecte",
|
||||
"playbackError": "hi ha hagut un error en intentar reproduir el mitjà",
|
||||
"remoteDisableError": "hi ha hagut un error en intentar $t(common.disable) el servidor remot",
|
||||
"endpointNotImplementedError": "el punt final {{endpoint}} no està implementat per {{serverType}}"
|
||||
"endpointNotImplementedError": "el punt final {{endpoint}} no està implementat per {{serverType}}",
|
||||
"multipleServerSaveQueueError": "la cua de reproducció té una o més cançons que no són del servidor actual, cosa que no és compatible",
|
||||
"saveQueueFailed": "error en desar la cua",
|
||||
"settingsSyncError": "hi ha discrepàncies entre la configuració del renderitzador i el procés principal. reinicieu l'aplicació per aplicar els canvis"
|
||||
},
|
||||
"releaseType": {
|
||||
"primary": {
|
||||
@@ -889,5 +1063,36 @@
|
||||
"error_oneFileOnly": "Seleccioneu un sol fitxer",
|
||||
"error_readingFile": "hi ha hagut un error en llegir el fitxer: {{errorMessage}}",
|
||||
"mainText": "deixeu anar un fitxer aquí"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "és posterior",
|
||||
"afterDate": "és posterior a (data)",
|
||||
"before": "és anterior",
|
||||
"beforeDate": "és anterior a (data)",
|
||||
"contains": "conté",
|
||||
"endsWith": "acaba en",
|
||||
"inPlaylist": "és a",
|
||||
"inTheLast": "és a l'últim",
|
||||
"inTheRange": "és entre",
|
||||
"inTheRangeDate": "és entre (data)",
|
||||
"is": "és",
|
||||
"isNot": "no és",
|
||||
"isGreaterThan": "és més gran que",
|
||||
"isLessThan": "és més petit que",
|
||||
"matchesRegex": "coincideix amb l'expressió regular",
|
||||
"notContains": "no conté",
|
||||
"notInPlaylist": "no és a",
|
||||
"notInTheLast": "no és a l'últim",
|
||||
"startsWith": "comença amb"
|
||||
},
|
||||
"queryBuilder": {
|
||||
"standardTags": "etiquetes estàndard",
|
||||
"customTags": "etiquetes personalitzades"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "min",
|
||||
"secondShort": "s",
|
||||
"hourShort": "h",
|
||||
"dayShort": "d"
|
||||
}
|
||||
}
|
||||
|
||||
+399
-29
@@ -11,10 +11,10 @@
|
||||
"skip_back": "přeskočit dozadu",
|
||||
"favorite": "oblíbené",
|
||||
"next": "další",
|
||||
"shuffle": "přehrát náhodně",
|
||||
"shuffle": "přehrát (náhodně)",
|
||||
"playbackFetchNoResults": "nenalezeny žádné skladby",
|
||||
"playbackFetchInProgress": "načítání skladeb…",
|
||||
"addNext": "přidat další",
|
||||
"addNext": "další",
|
||||
"playbackSpeed": "rychlost přehrávání",
|
||||
"playbackFetchCancel": "chvíli to trvá… zavřete oznámení pro zrušení akce",
|
||||
"play": "přehrát",
|
||||
@@ -26,11 +26,22 @@
|
||||
"queue_moveToTop": "přesunout vybrané dolů",
|
||||
"queue_moveToBottom": "přesunout vybrané nahoru",
|
||||
"shuffle_off": "náhodně zakázáno",
|
||||
"addLast": "přidat poslední",
|
||||
"addLast": "poslední",
|
||||
"mute": "ztlumit",
|
||||
"skip_forward": "přeskočit dopředu",
|
||||
"playSimilarSongs": "přehrát podobné skladby",
|
||||
"viewQueue": "zobrazit frontu"
|
||||
"viewQueue": "zobrazit frontu",
|
||||
"addLastShuffled": "poslední (náhodně)",
|
||||
"addNextShuffled": "další (náhodně)",
|
||||
"queueType": "typ fronty",
|
||||
"queueType_default": "výchozí",
|
||||
"queueType_priority": "priorita",
|
||||
"holdToShuffle": "podržte pro zamíchání",
|
||||
"lyrics": "texty",
|
||||
"restoreQueueFromServer": "obnovit frontu ze serveru",
|
||||
"saveQueueToServer": "uložit frontu na server",
|
||||
"artistRadio": "rádio umělce",
|
||||
"trackRadio": "rádio skladby"
|
||||
},
|
||||
"setting": {
|
||||
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
|
||||
@@ -144,7 +155,6 @@
|
||||
"replayGainMode": "režim {{ReplayGain}}",
|
||||
"playbackStyle_optionNormal": "normální",
|
||||
"windowBarStyle": "styl záhlaví okna",
|
||||
"floatingQueueArea": "zobrazit plovoucí oblast přejetí nad frontou",
|
||||
"replayGainFallback_description": "zesílení v db k použití, když nemá soubor žádné značky {{ReplayGain}}",
|
||||
"replayGainPreamp_description": "úprava předběžného zesílení použitého na hodnoty {{ReplayGain}}",
|
||||
"hotkey_toggleRepeat": "přepnutí opakování",
|
||||
@@ -170,7 +180,6 @@
|
||||
"hotkey_rate0": "vymazání hodnocení",
|
||||
"discordApplicationId": "id aplikace pro {{discord}}",
|
||||
"applicationHotkeys_description": "nastavení klávesových zkratek aplikace. přepněte pole pro nastavení jako globální zkratku (pouze na počítači)",
|
||||
"floatingQueueArea_description": "zobrazit ikonu přejetí myší na pravé straně obrazovky pro zobrazení fronty",
|
||||
"hotkey_volumeMute": "ztlumení",
|
||||
"hotkey_toggleCurrentSongFavorite": "přepnutí oblíbení u $t(common.currentSong)",
|
||||
"remoteUsername": "uživatelské jméno serveru vzdáleného ovládání",
|
||||
@@ -200,11 +209,7 @@
|
||||
"passwordStore": "ukládání hesel / tajných klíčů",
|
||||
"mpvExtraParameters_help": "jeden na řádek",
|
||||
"homeConfiguration": "nastavení domovské stránky",
|
||||
"playerAlbumArtResolution_description": "rozlišení náhledu obalu alba ve velkém přehrávači. větší hodnota znamená kvalitnější obrázek, ale může se déle načítat. výchozí hodnota je 0, což znamená automatické rozlišení",
|
||||
"playerAlbumArtResolution": "rozlišení obalu alba v přehrávači",
|
||||
"genreBehavior": "výchozí chování stránky žánrů",
|
||||
"externalLinks_description": "zapne zobrazování externích odkazů (Last.fm, MusicBrainz) na stránce umělce/alba",
|
||||
"genreBehavior_description": "určuje, zda kliknutí na žánr otevře seznam skladeb nebo alb",
|
||||
"clearCacheSuccess": "mezipaměť úspěšně vymazána",
|
||||
"externalLinks": "zobrazit externí odkazy",
|
||||
"startMinimized_description": "spustit aplikaci do systémové lišty",
|
||||
@@ -213,8 +218,6 @@
|
||||
"homeFeature_description": "ovládá, zda se má zobrazovat velký carousel s doporučenými alby na domovské stránce",
|
||||
"imageAspectRatio": "použít nativní poměr stran obalů alb",
|
||||
"imageAspectRatio_description": "pokud je povoleno, budou obaly alb zobrazeny s jejich nativním poměrem stran. u obalů, které nemají poměr 1:1, bude zbývající místo prázdné",
|
||||
"doubleClickBehavior": "dvojitým kliknutím zařadit všechny vyhledané skladby do fronty",
|
||||
"doubleClickBehavior_description": "pokud je zapnuto, budou všechny odpovídající skladby ve vyhledávání zařazeny do fronty. v opačném případě bude zařazena pouze ta, na kterou kliknete",
|
||||
"volumeWidth": "šířka posuvníku hlasitosti",
|
||||
"volumeWidth_description": "horizontální velikost posuvníku hlasitosti",
|
||||
"discordListening": "zobrazit stav jako „Poslouchá“",
|
||||
@@ -304,7 +307,60 @@
|
||||
"language": "jazyk",
|
||||
"notify": "povolit oznámení o skladbách",
|
||||
"notify_description": "zobrazit oznámení při změně aktuální skladby",
|
||||
"transcode": "povolit překódování"
|
||||
"transcode": "povolit překódování",
|
||||
"analyticsDisable": "Odhlásit se z analytiky používání aplikace",
|
||||
"analyticsDisable_description": "Pro zlepšení aplikace jsou vývojáři odesílána anonymizovaná data o používání",
|
||||
"playerbarSlider": "posuvník lišty přehrávače",
|
||||
"playerbarSliderType_optionSlider": "posuvník",
|
||||
"playerbarSliderType_optionWaveform": "vlnová křivka",
|
||||
"playerbarWaveformAlign": "pozice vlnové křivky",
|
||||
"playerbarWaveformAlign_optionTop": "nahoře",
|
||||
"playerbarWaveformAlign_optionCenter": "uprostřed",
|
||||
"playerbarWaveformAlign_optionBottom": "dole",
|
||||
"playerbarWaveformBarWidth": "šířka sloupců vlnové křivky",
|
||||
"playerbarWaveformGap": "mezera vlnové křivky",
|
||||
"playerbarWaveformRadius": "poloměr vlnové křivky",
|
||||
"showLyricsInSidebar_description": "do připojené fronty přehrávání bude přidán panel, který zobrazuje texty",
|
||||
"showLyricsInSidebar": "zobrazit texty v postranní liště přehrávače",
|
||||
"showVisualizerInSidebar_description": "do postranní lišty přehrávače bude přidán panel, který zobrazuje vizualizér",
|
||||
"showVisualizerInSidebar": "zobrazit vizualizér v postranní liště přehrávače",
|
||||
"queryBuilder": "sestavení dotazu",
|
||||
"queryBuilderCustomFields_inputLabel": "štítek",
|
||||
"queryBuilderCustomFields_inputTag": "značka",
|
||||
"queryBuilderCustomFields": "vlastní pole",
|
||||
"queryBuilderCustomFields_description": "přidat vlasntí pole k použití při sestavování dotazů",
|
||||
"audioFadeOnStatusChange": "zeslabení zvuku při změně stavu",
|
||||
"audioFadeOnStatusChange_description": "povolí postupné zeslabení a zesílení zvuku při změně stavu přehrávání/pozastavení",
|
||||
"followCurrentSong_description": "automaticky posouvat frontu přehrávání na právě hrající skladbu",
|
||||
"followCurrentSong": "následovat aktuální skladbu",
|
||||
"playerFilters": "Filtrovat skladby z fronty",
|
||||
"playerFilters_description": "vynechat skladby z přidání do fronty na základě následujících kritérií",
|
||||
"playerbarSlider_description": "vlnová křivka není doporučena, pokud se nacházíte na pomalém nebo měřeném internetovém připojení",
|
||||
"autoDJ": "automatický DJ",
|
||||
"autoDJ_description": "automaticky přidávat podobné skladby do fronty",
|
||||
"autoDJ_itemCount": "počet položek",
|
||||
"autoDJ_itemCount_description": "počet položek, které se pokusíme přidat do fronty po povolení automatického DJ",
|
||||
"autoDJ_timing": "časování",
|
||||
"autoDJ_timing_description": "počet skladeb zbývajících ve frontě před spuštěním automatického DJ",
|
||||
"logLevel": "úroveň protokolu",
|
||||
"logLevel_description": "nastaví minimální úroveň protokolu k zobrazení. ladění zobrazuje vše, možnost chyba zobrazí pouze chyby",
|
||||
"logLevel_optionDebug": "ladění",
|
||||
"logLevel_optionError": "chyba",
|
||||
"logLevel_optionInfo": "informace",
|
||||
"logLevel_optionWarn": "varování",
|
||||
"useThemeAccentColor": "použít barvu motivu",
|
||||
"useThemeAccentColor_description": "použít primární barvu definovanou ve zvoleném motivu namísto vlastní barvy rozhraní",
|
||||
"artistRadioCount_description": "nastaví počet skladeb, které načíst pro rádio umělce a rádio skladby",
|
||||
"artistRadioCount": "počet skladeb pro rádio umělce/skladby",
|
||||
"imageResolution": "rozlišení obrázků",
|
||||
"imageResolution_description": "rozlišení obrázků používaných napříč aplikací. nastavení hodnoty 0 použije nativní rozlišení obrázku",
|
||||
"imageResolution_optionTable": "tabulka",
|
||||
"imageResolution_optionItemCard": "karta položky",
|
||||
"imageResolution_optionSidebar": "postranní lišta",
|
||||
"imageResolution_optionHeader": "záhlaví",
|
||||
"imageResolution_optionFullScreenPlayer": "přehrávač na celé obrazovce",
|
||||
"combinedLyricsAndVisualizer_description": "spojit texty a vizualizér do jednoho panelu",
|
||||
"combinedLyricsAndVisualizer": "spojit texty a vizualizér v postranní liště přehrávače"
|
||||
},
|
||||
"action": {
|
||||
"editPlaylist": "upravit $t(entity.playlist_one)",
|
||||
@@ -328,7 +384,23 @@
|
||||
"lastfm": "Otevřít v Last.fm",
|
||||
"musicbrainz": "Otevřít v MusicBrainz"
|
||||
},
|
||||
"moveToNext": "přesunout na další"
|
||||
"moveToNext": "přesunout na další",
|
||||
"downloadStarted": "spuštěno stahování {{count}} položek",
|
||||
"moveItems": "přesunout položky",
|
||||
"shuffle": "náhodně",
|
||||
"shuffleAll": "vše náhodně",
|
||||
"shuffleSelected": "vybrané náhodně",
|
||||
"viewMore": "zobrazit více",
|
||||
"moveUp": "posunout nahoru",
|
||||
"moveDown": "posunout dolů",
|
||||
"holdToMoveToTop": "podržte pro přesunutí nahoru",
|
||||
"holdToMoveToBottom": "podržte pro přesunutí dolů",
|
||||
"createRadioStation": "vytvořit $t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "odstranit $t(entity.radioStation_one)",
|
||||
"openApplicationDirectory": "otevřít adresář aplikace",
|
||||
"addOrRemoveFromSelection": "přidat nebo odebrat z výběru",
|
||||
"selectRangeOfItems": "vyberte rozsah položek",
|
||||
"selectAll": "vybrat vše"
|
||||
},
|
||||
"common": {
|
||||
"backward": "zpátky",
|
||||
@@ -435,14 +507,24 @@
|
||||
"private": "soukromý",
|
||||
"public": "veřejný",
|
||||
"recordLabel": "vydavatelství",
|
||||
"releaseType": "typ vydání"
|
||||
"releaseType": "typ vydání",
|
||||
"doNotShowAgain": "již nezobrazovat",
|
||||
"externalLinks": "externí odkazy",
|
||||
"faster": "rychlejší",
|
||||
"slower": "pomalejší",
|
||||
"sort": "seřadit",
|
||||
"gridRows": "řádky mřížky",
|
||||
"tableColumns": "sloupce tabulky",
|
||||
"itemsMore": "{{count}} dalších",
|
||||
"noFilters": "nejsou nastaveny žádné filtry",
|
||||
"view": "zobrazit",
|
||||
"countSelected": "vybráno {{count}}",
|
||||
"retry": "zkusit znovu"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
"view": {
|
||||
"card": "karta",
|
||||
"table": "tabulka",
|
||||
"poster": "plakát",
|
||||
"list": "seznam",
|
||||
"grid": "mřížka"
|
||||
},
|
||||
@@ -454,7 +536,28 @@
|
||||
"size": "$t(common.size)",
|
||||
"itemGap": "mezera mezi položkami (px)",
|
||||
"itemSize": "velikost položek (px)",
|
||||
"followCurrentSong": "následovat aktuální skladbu"
|
||||
"followCurrentSong": "následovat aktuální skladbu",
|
||||
"advancedSettings": "pokročilá nastavení",
|
||||
"autosize": "automatická velikost",
|
||||
"moveUp": "posunout nahoru",
|
||||
"moveDown": "posunout dolů",
|
||||
"pinToLeft": "připnout doleva",
|
||||
"pinToRight": "připnout doprava",
|
||||
"alignLeft": "zarovnat doleva",
|
||||
"alignCenter": "zarovnat doprostřed",
|
||||
"alignRight": "zarovat doprava",
|
||||
"itemsPerRow": "položky na řádek",
|
||||
"size_default": "výchozí",
|
||||
"size_compact": "kompaktní",
|
||||
"size_large": "velký",
|
||||
"pagination": "stránkování",
|
||||
"pagination_itemsPerPage": "položky na stránku",
|
||||
"pagination_infinite": "nekonečno",
|
||||
"pagination_paginate": "stránkované",
|
||||
"alternateRowColors": "střídat barvy řádků",
|
||||
"horizontalBorders": "okraje řádků",
|
||||
"rowHoverHighlight": "zvýraznění řádku při přejetí myší",
|
||||
"verticalBorders": "okraje sloupců"
|
||||
},
|
||||
"label": {
|
||||
"releaseDate": "datum vydání",
|
||||
@@ -484,7 +587,12 @@
|
||||
"year": "$t(common.year)",
|
||||
"albumArtist": "$t(entity.albumArtist_one)",
|
||||
"codec": "$t(common.codec)",
|
||||
"songCount": "$t(entity.track_other)"
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"albumCount": "$t(entity.album_other)",
|
||||
"genreBadge": "$t(entity.genre_one) (značky)",
|
||||
"image": "obrázek",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
}
|
||||
},
|
||||
"column": {
|
||||
@@ -511,7 +619,10 @@
|
||||
"discNumber": "disk",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"size": "$t(common.size)",
|
||||
"codec": "$t(common.codec)"
|
||||
"codec": "$t(common.codec)",
|
||||
"owner": "majitel",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -538,7 +649,12 @@
|
||||
"networkError": "vyskytla se chyba sítě",
|
||||
"openError": "nepodařilo se otevřít soubor",
|
||||
"badValue": "neplatná možnost „{{value}}“. tato možnost již neexistuje",
|
||||
"notificationDenied": "oprávnění k posílání oznámení byla zamítnuta. toto nastavení nemá žádný vliv"
|
||||
"notificationDenied": "oprávnění k posílání oznámení byla zamítnuta. toto nastavení nemá žádný vliv",
|
||||
"multipleServerSaveQueueError": "fronta přehrávání má jednu nebo více skladeb, které nejsou z aktuálního serveru. tato funkce není podporována",
|
||||
"saveQueueFailed": "nepodařilo se uložit frontu",
|
||||
"settingsSyncError": "byly zjištěny nesrovnalosti mezi nastavením v rendereru a hlavním procesem. restartujte aplikaci, aby se změny projevily",
|
||||
"noNetwork": "server je nedostupný",
|
||||
"noNetworkDescription": "k tomuto serveru se nepodařilo připojit"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "nejvíce přehráváno",
|
||||
@@ -599,7 +715,9 @@
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "$t(entity.playlist_other) sdíleny",
|
||||
"myLibrary": "moje knihovna"
|
||||
"myLibrary": "moje knihovna",
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
@@ -636,7 +754,11 @@
|
||||
"goBack": "přejít zpět",
|
||||
"goForward": "přejít vpřed",
|
||||
"privateModeOff": "vypnout soukromý režim",
|
||||
"privateModeOn": "zapnout soukromý režim"
|
||||
"privateModeOn": "zapnout soukromý režim",
|
||||
"selectMusicFolder": "vybrat složku s hudbou",
|
||||
"noMusicFolder": "není vybrána žádná složka s hudbou",
|
||||
"multipleMusicFolders": "Vybráno {{count}} složek s hudbou",
|
||||
"commandPalette": "otevřít paletu příkazů"
|
||||
},
|
||||
"contextMenu": {
|
||||
"addToPlaylist": "$t(action.addToPlaylist)",
|
||||
@@ -662,7 +784,9 @@
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"goToAlbum": "přejít na $t(entity.album_one)",
|
||||
"goToAlbumArtist": "přejít na $t(entity.albumArtist_one)"
|
||||
"goToAlbumArtist": "přejít na $t(entity.albumArtist_one)",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"goTo": "přejít na"
|
||||
},
|
||||
"home": {
|
||||
"mostPlayed": "nejpřehrávanější",
|
||||
@@ -670,7 +794,8 @@
|
||||
"title": "$t(common.home)",
|
||||
"explore": "procházet z vaší knihovny",
|
||||
"recentlyPlayed": "nedávno přehráno",
|
||||
"recentlyReleased": "nedávno vydáno"
|
||||
"recentlyReleased": "nedávno vydáno",
|
||||
"genres": "$t(entity.genre_other)"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "více od tohoto $t(entity.artist_one)",
|
||||
@@ -682,7 +807,25 @@
|
||||
"generalTab": "obecné",
|
||||
"hotkeysTab": "klávesové zkratky",
|
||||
"windowTab": "okno",
|
||||
"advanced": "pokročilé"
|
||||
"advanced": "pokročilé",
|
||||
"analytics": "analytika",
|
||||
"updates": "aktualizace",
|
||||
"cache": "mezipaměť",
|
||||
"application": "aplikace",
|
||||
"queryBuilder": "sestavení dotazu",
|
||||
"theme": "motiv",
|
||||
"controls": "ovládání",
|
||||
"sidebar": "postranní lišta",
|
||||
"remote": "vzdálené",
|
||||
"exportImport": "import/export",
|
||||
"scrobble": "scrobblování",
|
||||
"audio": "zvuk",
|
||||
"lyrics": "texty",
|
||||
"transcoding": "překódování",
|
||||
"discord": "discord",
|
||||
"playerFilters": "filtry přehrávače",
|
||||
"logger": "protokol",
|
||||
"lyricsDisplay": "zobrazení textů"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
@@ -739,6 +882,15 @@
|
||||
"removeServer": "odstranit server",
|
||||
"serverDetails": "podrobnosti o serveru",
|
||||
"title": "správa serverů"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite_other)"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "stanice rádia"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -785,7 +937,11 @@
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "shoda všeho",
|
||||
"input_optionMatchAny": "shoda libovolného",
|
||||
"title": "editor dotazů"
|
||||
"title": "editor dotazů",
|
||||
"addRuleGroup": "přidat skupinu pravidel",
|
||||
"removeRuleGroup": "odstranit skupinu pravidel",
|
||||
"resetToDefault": "resetovat na výchozí",
|
||||
"clearFilters": "vymazat filtry"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_name": "$t(common.name)",
|
||||
@@ -795,7 +951,8 @@
|
||||
"editPlaylist": {
|
||||
"title": "upravit $t(entity.playlist_one)",
|
||||
"success": "$t(entity.playlist_one) úspěšně aktualizován",
|
||||
"publicJellyfinNote": "Jellyfin z nějakého důvodu neukazuje, zda je seznam skladeb veřejný, nebo ne. Pokud si přejete, aby zůstal veřejný, zvolte prosím následující vstup"
|
||||
"publicJellyfinNote": "Jellyfin z nějakého důvodu neukazuje, zda je seznam skladeb veřejný, nebo ne. Pokud si přejete, aby zůstal veřejný, zvolte prosím následující vstup",
|
||||
"editNote": "ruční úpravy velkých seznamů skladeb nejsou doporučeny. opravdu přijímáte riziko ztráty dat, které může vzniknout přepsáním existujícího seznamu skladeb?"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "umožnit stahování",
|
||||
@@ -809,6 +966,36 @@
|
||||
"enabled": "soukromý režim povolen, stav přehrávání je nyní skryt před externími integracemi",
|
||||
"disabled": "soukromý režim povolen, stav přehrávání je nyní viditelný pro externími integrace",
|
||||
"title": "soukromý režim"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "přidat položky do fronty",
|
||||
"description": "Tato akce přidá všechny položky v aktuálně filtrovaném zobrazení"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "přehrát náhodně",
|
||||
"input_genre": "$t(entity.genre_one)",
|
||||
"input_limit": "kolik skladeb?",
|
||||
"input_minYear": "od roku",
|
||||
"input_maxYear": "do roku",
|
||||
"input_played": "přehrát filtr",
|
||||
"input_played_optionAll": "všechny skladby",
|
||||
"input_played_optionUnplayed": "pouze nepřehrané skladby",
|
||||
"input_played_optionPlayed": "pouze přehrané skladby"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "fronta přehrávání uložena na server"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "stanice rádia úspěšně vytvořena",
|
||||
"title": "vytvořit stanici rádia",
|
||||
"input_homepageUrl": "adresa domovské stránky",
|
||||
"input_name": "název",
|
||||
"input_streamUrl": "adresa streamu"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "exportovat texty",
|
||||
"input_synced": "exportovat synchronizované texty",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -863,7 +1050,13 @@
|
||||
"play_other": "{{count}} přehrání",
|
||||
"song_one": "píseň",
|
||||
"song_few": "písničky",
|
||||
"song_other": "písní"
|
||||
"song_other": "písní",
|
||||
"radioStation_one": "stanice rádia",
|
||||
"radioStation_few": "stanice rádia",
|
||||
"radioStation_other": "stanice rádia",
|
||||
"radioStationWithCount_one": "{{count}} stanice rádia",
|
||||
"radioStationWithCount_few": "{{count}} stanice rádia",
|
||||
"radioStationWithCount_other": "{{count}} stanic rádia"
|
||||
},
|
||||
"dragDropZone": {
|
||||
"error_oneFileOnly": "Vyberte prosím pouze 1 soubor",
|
||||
@@ -892,5 +1085,182 @@
|
||||
"soundtrack": "soundtrack",
|
||||
"spokenWord": "mluvené slovo"
|
||||
}
|
||||
},
|
||||
"queryBuilder": {
|
||||
"standardTags": "standardní značky",
|
||||
"customTags": "vlastní značky"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "je po",
|
||||
"afterDate": "je po (datum)",
|
||||
"before": "je před",
|
||||
"beforeDate": "je před (datum)",
|
||||
"contains": "obsahuje",
|
||||
"endsWith": "končí na",
|
||||
"inPlaylist": "je v",
|
||||
"inTheLast": "je v posledním",
|
||||
"inTheRange": "je v rozsahu",
|
||||
"inTheRangeDate": "je v rozsahu (datum)",
|
||||
"is": "je",
|
||||
"isNot": "není",
|
||||
"isGreaterThan": "je větší než",
|
||||
"isLessThan": "je menší než",
|
||||
"matchesRegex": "odpovídá regulárnímu výrazu",
|
||||
"notContains": "neobsahuje",
|
||||
"notInPlaylist": "není v",
|
||||
"notInTheLast": "není v posledním",
|
||||
"startsWith": "začíná na"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "min.",
|
||||
"secondShort": "s",
|
||||
"hourShort": "h.",
|
||||
"dayShort": "den"
|
||||
},
|
||||
"visualizer": {
|
||||
"visualizerType": "Typ vizualizéru",
|
||||
"cyclePresets": "Cyklicky procházet předvolby",
|
||||
"cycleTime": "Čas cyklování (sekundy)",
|
||||
"includeAllPresets": "Zahrnout všechny předvolby",
|
||||
"ignoredPresets": "Ignorované předvolby",
|
||||
"selectedPresets": "Vybrané předvolby",
|
||||
"randomizeNextPreset": "Náhodně vybrat další předvolbu",
|
||||
"blendTime": "Prolnout čas",
|
||||
"presets": "Předvolby",
|
||||
"selectPreset": "Vybrat předvolbu",
|
||||
"applyPreset": "Použít předvolbu",
|
||||
"saveAsPreset": "Uložit jako předvolbu",
|
||||
"updatePreset": "Aktualizovat předvolbu",
|
||||
"copyConfiguration": "Kopírovat konfiguraci",
|
||||
"pasteConfiguration": "Vložit konfiguraci",
|
||||
"pasteConfigurationPlaceholder": "Sem vložte konfiguraci JSON…",
|
||||
"pasteFromClipboard": "Vložit ze schránky",
|
||||
"applyConfiguration": "Použít konfiguraci",
|
||||
"configCopied": "Konfigurace zkopírována do schránky",
|
||||
"configCopyFailed": "Nepodařilo se zkopírovat konfiguraci",
|
||||
"configPasted": "Konfigurace úspěšně použita",
|
||||
"configPasteFailed": "Nepodařilo se použít konfiguraci. Zkontrolujte prosím formát.",
|
||||
"configPasteReadFailed": "Nepodařilo se přečíst schránku",
|
||||
"presetName": "Název předvolby",
|
||||
"presetNamePlaceholder": "Zadejte název předvolby",
|
||||
"general": "Obecné",
|
||||
"mode": "Režim",
|
||||
"mode1To8": "Režim 1–8",
|
||||
"mode10": "Režim 10",
|
||||
"barSpace": "Mezera mezi sloupci",
|
||||
"lineWidth": "Šířka linky",
|
||||
"fillAlpha": "Vyplnit alfu",
|
||||
"channelLayout": "Rozložení kanálů",
|
||||
"maxFPS": "Max. počet snímků za sekundu",
|
||||
"opacity": "Neprůhlednost",
|
||||
"customGradients": "Vlastní přechody",
|
||||
"addCustomGradient": "Přidat vlastní přechod",
|
||||
"gradientName": "Název přechodu",
|
||||
"gradientNamePlaceholder": "Název přechodu",
|
||||
"vertical": "Vertikální",
|
||||
"horizontal": "Horizontální",
|
||||
"colorStops": "Ukončení barev",
|
||||
"addColor": "Přidat barvu",
|
||||
"position": "Pozice",
|
||||
"level": "Úroveň",
|
||||
"remove": "Odstranit",
|
||||
"custom": "Vlastní",
|
||||
"builtIn": "Vestavěné",
|
||||
"colors": "Barvy",
|
||||
"colorMode": "Režim barev",
|
||||
"gradient": "Přechod",
|
||||
"gradientLeft": "Přechod zleva",
|
||||
"gradientRight": "Přechod zprava",
|
||||
"fft": "FFT",
|
||||
"fftSize": "Velikost FFT",
|
||||
"smoothing": "Vyhlazování",
|
||||
"frequencyRangeAndScaling": "Rozsah a škálování frekvencí",
|
||||
"minimumFrequency": "Minimální frekvence",
|
||||
"maximumFrequency": "Maximální frekvence",
|
||||
"frequencyScale": "Škála frekvence",
|
||||
"sensitivity": "Citlivost",
|
||||
"weightingFilter": "Filtr váhy",
|
||||
"minimumDecibels": "Minimální decibely",
|
||||
"maximumDecibels": "Maximální decibely",
|
||||
"linearAmplitude": "Lineární amplituda",
|
||||
"linearBoost": "Lineární zesílení",
|
||||
"peakBehavior": "Chování ve špičce",
|
||||
"showPeaks": "Zobrazit špičky",
|
||||
"fadePeaks": "Prolnout špičky",
|
||||
"peakLine": "Linka špiček",
|
||||
"gravity": "Gravitace",
|
||||
"peakFadeTime": "Čas pádu ze špičky (ms)",
|
||||
"peakHoldTime": "Čas udržení na špičce (ms)",
|
||||
"radialSpectrum": "Kruhové spektrum",
|
||||
"radial": "Kruhové",
|
||||
"radialInvert": "Kruhové invertované",
|
||||
"spinSpeed": "Rychlost rotace",
|
||||
"radius": "Poloměr",
|
||||
"reflexMirror": "Reflexní zrcadlení",
|
||||
"reflexFit": "Reflexní vyplnění",
|
||||
"reflexRatio": "Reflexní poměr",
|
||||
"reflexAlpha": "Reflexní alfa",
|
||||
"reflexBrightness": "Reflexní jas",
|
||||
"mirror": "Zrcadlení",
|
||||
"miscellaneousSettings": "Různá nastavení",
|
||||
"alphaBars": "Alfa sloupce",
|
||||
"ansiBands": "ANSI sloupce",
|
||||
"ledBars": "LED sloupce",
|
||||
"trueLeds": "Pravé LED",
|
||||
"lumiBars": "Lumi sloupce",
|
||||
"outlineBars": "Obrysové sloupce",
|
||||
"roundBars": "Zaoblené sloupce",
|
||||
"lowResolution": "Nízké rozlišení",
|
||||
"splitGradient": "Přechod rozdělení",
|
||||
"showFPS": "Zobrazit FPS",
|
||||
"showScaleX": "Zobrazit osu X",
|
||||
"noteLabels": "Štítky not",
|
||||
"showScaleY": "Zobrazit osu Y",
|
||||
"options": {
|
||||
"mode": {
|
||||
"bars": "[0] Sloupce",
|
||||
"circle": "[1] Kruh",
|
||||
"wave": "[2] Vlna",
|
||||
"rainbow": "[3] Duha",
|
||||
"rings": "[4] Prstence",
|
||||
"mirror": "[5] Zrcadlo",
|
||||
"line": "[6] Linka",
|
||||
"particles": "[7] Částice",
|
||||
"fullOctave": "[8] Plná oktáva / 10 pásem",
|
||||
"outlineBars": "[10] Obrysové sloupce"
|
||||
},
|
||||
"colorMode": {
|
||||
"gradient": "Přechod",
|
||||
"barIndex": "Index sloupce",
|
||||
"barLevel": "Úroveň sloupce"
|
||||
},
|
||||
"gradient": {
|
||||
"classic": "Klasický",
|
||||
"prism": "Prism",
|
||||
"rainbow": "Duha",
|
||||
"steelblue": "Ocelově modrá",
|
||||
"orangered": "Oranžová"
|
||||
},
|
||||
"channelLayout": {
|
||||
"single": "Jeden",
|
||||
"dualCombined": "Duální kombinované",
|
||||
"dualHorizontal": "Duální horizontální",
|
||||
"dualVertical": "Duální vertikální"
|
||||
},
|
||||
"frequencyScale": {
|
||||
"bark": "Bark",
|
||||
"linear": "Lineární",
|
||||
"log": "Log",
|
||||
"mel": "Mel"
|
||||
},
|
||||
"weightingFilter": {
|
||||
"none": "Žádný",
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "C",
|
||||
"d": "D",
|
||||
"z": "Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+404
-111
@@ -1,30 +1,42 @@
|
||||
{
|
||||
"action": {
|
||||
"editPlaylist": "bearbeiten $t(entity.playlist_one)",
|
||||
"clearQueue": "Warteschlange löschen",
|
||||
"addToFavorites": "hinzufügen zu $t(entity.favorite_other)",
|
||||
"addToPlaylist": "hinzufügen zu $t(entity.playlist_one)",
|
||||
"createPlaylist": "erstelle $t(entity.playlist_one)",
|
||||
"deletePlaylist": "löschen $t(entity.playlist_one)",
|
||||
"editPlaylist": "$t(entity.playlist_one) bearbeiten",
|
||||
"clearQueue": "Wiedergabeliste leeren",
|
||||
"addToFavorites": "Zu $t(entity.favorite_other) hinzufügen",
|
||||
"addToPlaylist": "Zu $t(entity.playlist_one) hinzufügen",
|
||||
"createPlaylist": "$t(entity.playlist_one) erstellen",
|
||||
"deletePlaylist": "$t(entity.playlist_one) löschen",
|
||||
"deselectAll": "Alle abwählen",
|
||||
"goToPage": "Gehe zur Seite",
|
||||
"moveToTop": "Nach oben",
|
||||
"moveToBottom": "Nach unten",
|
||||
"removeFromPlaylist": "Entfernen von $t(entity.playlist_one)",
|
||||
"viewPlaylists": "Ansicht $t(entity.playlist_other)",
|
||||
"goToPage": "Zu Seite gehen",
|
||||
"moveToTop": "Als erstes",
|
||||
"moveToBottom": "Als letztes",
|
||||
"removeFromPlaylist": "Aus $t(entity.playlist_one) entfernen",
|
||||
"viewPlaylists": "$t(entity.playlist_other) anzeigen",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromQueue": "Von Warteschlange entfernen",
|
||||
"setRating": "Bewertung festlegen",
|
||||
"toggleSmartPlaylistEditor": "Editor $t(entity.smartPlaylist) umschalten",
|
||||
"removeFromFavorites": "Entfernen von $t(entity.favorite_other)",
|
||||
"removeFromQueue": "Aus Wiedergabeliste entfernen",
|
||||
"setRating": "Bewerten",
|
||||
"toggleSmartPlaylistEditor": "Editor für $t(entity.smartPlaylist) ein-/ausblenden",
|
||||
"removeFromFavorites": "Aus $t(entity.favorite_other) entfernen",
|
||||
"openIn": {
|
||||
"lastfm": "In Last.fm öffnen",
|
||||
"musicbrainz": "In MusicBrainz öffnen"
|
||||
"lastfm": "Auf Last.fm öffnen",
|
||||
"musicbrainz": "Auf MusicBrainz öffnen"
|
||||
},
|
||||
"moveToNext": "nach unten verschieben"
|
||||
"moveToNext": "Als nächstes",
|
||||
"downloadStarted": "Download von {{count}} Elementen gestartet",
|
||||
"moveItems": "Elemente verschieben",
|
||||
"shuffle": "Zufällig wiedergeben",
|
||||
"shuffleAll": "Alle zufällig wiedergeben",
|
||||
"shuffleSelected": "Ausgewählte zufällig wiedergeben",
|
||||
"viewMore": "Mehr zeigen",
|
||||
"moveUp": "Nach oben bewegen",
|
||||
"moveDown": "Nach unten bewegen",
|
||||
"createRadioStation": "$t(entity.radioStation_one) erstellen",
|
||||
"deleteRadioStation": "$t(entity.radioStation_one) löschen",
|
||||
"selectAll": "alle auswählen",
|
||||
"openApplicationDirectory": "Anwendungsverzeichnis öffnen"
|
||||
},
|
||||
"common": {
|
||||
"backward": "rückwärts",
|
||||
"backward": "zurück",
|
||||
"increase": "erhöhen",
|
||||
"rating": "Wertung",
|
||||
"bpm": "bpm",
|
||||
@@ -33,11 +45,11 @@
|
||||
"areYouSure": "Bist Du sicher?",
|
||||
"edit": "Bearbeiten",
|
||||
"favorite": "Favorit",
|
||||
"left": "links",
|
||||
"left": "Linksbündig",
|
||||
"save": "Speichern",
|
||||
"right": "rechts",
|
||||
"currentSong": "momentaner $t(entity.track_one)",
|
||||
"collapse": "Zusammenklappen",
|
||||
"right": "Rechtsbündig",
|
||||
"currentSong": "Aktueller $t(entity.track_one)",
|
||||
"collapse": "Verkleinern",
|
||||
"trackNumber": "Track",
|
||||
"descending": "absteigend",
|
||||
"add": "Hinzufügen",
|
||||
@@ -57,11 +69,11 @@
|
||||
"description": "Beschreibung",
|
||||
"configure": "Konfigurieren",
|
||||
"path": "Pfad",
|
||||
"center": "Zentrieren",
|
||||
"center": "Zentriert",
|
||||
"no": "Nein",
|
||||
"owner": "Eigentümer",
|
||||
"enable": "Aktivieren",
|
||||
"clear": "Bereinigen",
|
||||
"clear": "Leeren",
|
||||
"forward": "vorwärts",
|
||||
"delete": "Löschen",
|
||||
"cancel": "Abbrechen",
|
||||
@@ -95,10 +107,10 @@
|
||||
"previousSong": "vorheriger $t(entity.track_one)",
|
||||
"noResultsFromQuery": "Die Abfrage brachte keine Ergebnisse",
|
||||
"quit": "verlassen",
|
||||
"expand": "expandieren",
|
||||
"expand": "Vergrößern",
|
||||
"search": "Suchen",
|
||||
"saveAs": "Speichern unter",
|
||||
"disc": "Disk",
|
||||
"disc": "CD",
|
||||
"yes": "Ja",
|
||||
"random": "zufällig",
|
||||
"size": "Größe",
|
||||
@@ -110,17 +122,35 @@
|
||||
"close": "schließen",
|
||||
"share": "Teilen",
|
||||
"translation": "Übersetzung",
|
||||
"trackGain": "Track-Pegelverstärkung",
|
||||
"trackPeak": "Track-Spitzenpegel",
|
||||
"trackGain": "Track Gain",
|
||||
"trackPeak": "Track Peak",
|
||||
"codec": "Codec",
|
||||
"albumPeak": "Album-Spitzenpegel",
|
||||
"albumGain": "Album-Pegelverstärkung",
|
||||
"albumGain": "Album Gain",
|
||||
"tags": "tags",
|
||||
"viewReleaseNotes": "release notes anzeigen",
|
||||
"viewReleaseNotes": "Veröffentlichungsnotizen anzeigen",
|
||||
"newVersion": "eine neue Version wurde installiert ({{version}})",
|
||||
"bitDepth": "Bittiefe",
|
||||
"sampleRate": "Abtastrate",
|
||||
"additionalParticipants": "weitere Beteiligte"
|
||||
"additionalParticipants": "weitere Beteiligte",
|
||||
"explicitStatus": "Anstößig-Status",
|
||||
"doNotShowAgain": "Nicht wieder zeigen",
|
||||
"explicit": "Anstößig",
|
||||
"gridRows": "Rasterzeilen",
|
||||
"tableColumns": "Tabellenspalten",
|
||||
"itemsMore": "{{count}} weitere",
|
||||
"externalLinks": "externe Links",
|
||||
"faster": "schneller",
|
||||
"noFilters": "Keine Filter konfiguriert",
|
||||
"private": "privat",
|
||||
"public": "öffentlich",
|
||||
"sort": "sortieren",
|
||||
"clean": "Jugendfrei",
|
||||
"recordLabel": "Plattenlabel",
|
||||
"slower": "langsamer",
|
||||
"releaseType": "Veröffentlichungsformat",
|
||||
"view": "Betrachten",
|
||||
"countSelected": "{{count}} ausgewählt"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
|
||||
@@ -142,11 +172,13 @@
|
||||
"audioDeviceFetchError": "Beim Versuch, Audiogeräte abzurufen, ist ein Fehler aufgetreten",
|
||||
"invalidServer": "Ungültiger Server",
|
||||
"loginRateError": "Zu viele Anmeldeversuche, bitte versuche es in einigen Sekunden erneut",
|
||||
"badAlbum": "sie sehen diese Seite, weil dieses Lied nicht Teil eines Albums ist. Wahrscheinlich sehen Sie dieses Problem, wenn Sie einen Song in Ihrem Musikordner auf oberster Ebene haben. Jellyfin gruppiert nur Songs, wenn sie sich in einem Ordner befinden",
|
||||
"badAlbum": "sie sehen diese Seite, weil dieses Lied nicht Teil eines Albums ist. Dieses Problem tritt meist auf, wenn sich ein Lied im Überordner befindet. Jellyfin gruppiert Tracks nur, wenn diese sich innerhalb eines Ordners befinden",
|
||||
"networkError": "ein Netzwerkfehler ist aufgetreten",
|
||||
"openError": "datei kann nicht geöffnet werden",
|
||||
"badValue": "ungültige option \"{{value}}\". Dieser Wert existiert nicht mehr",
|
||||
"notificationDenied": "Berechtigungen über Benachrichtigungen wurden verweigert. Diese Einstellung hat keinen Effekt"
|
||||
"notificationDenied": "Berechtigungen über Benachrichtigungen wurden verweigert. Diese Einstellung hat keinen Effekt",
|
||||
"saveQueueFailed": "Wiedergabeliste konnte nicht gespeichert werden",
|
||||
"multipleServerSaveQueueError": "die Wiedergabeliste enthält einen oder mehrere Titel, die nicht vom aktuellen Server stammen. dies wird nicht unterstützt"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "Meistgespielt",
|
||||
@@ -190,7 +222,8 @@
|
||||
"channels": "$t(common.channel_other)",
|
||||
"owner": "$t(common.owner)",
|
||||
"genre": "$t(entity.genre_one)",
|
||||
"artist": "$t(entity.artist_one)"
|
||||
"artist": "$t(entity.artist_one)",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
},
|
||||
"form": {
|
||||
"deletePlaylist": {
|
||||
@@ -211,19 +244,23 @@
|
||||
"input_username": "Benutzername",
|
||||
"input_url": "URL",
|
||||
"input_password": "Passwort",
|
||||
"input_legacyAuthentication": "Aktivieren der Legacy-Authentifizierung",
|
||||
"input_name": "Server Name",
|
||||
"input_legacyAuthentication": "Alte Authentifizierung verwenden",
|
||||
"input_name": "Servername",
|
||||
"success": "Server erfolgreich hinzugefügt",
|
||||
"input_savePassword": "Passwort speichern",
|
||||
"ignoreSsl": "ignoriere ssl $t(common.restartRequired)",
|
||||
"ignoreCors": "ignoriere cors $t(common.restartRequired)",
|
||||
"error_savePassword": "Beim Versuch, das Passwort zu speichern, ist ein Fehler aufgetreten"
|
||||
"ignoreSsl": "SSL ignorieren $t(common.restartRequired)",
|
||||
"ignoreCors": "CORS ignorieren $t(common.restartRequired)",
|
||||
"error_savePassword": "Beim Speichern des Passworts ist ein Fehler aufgetreten",
|
||||
"input_preferInstantMix": "Instant-Mix bevorzugen",
|
||||
"input_preferInstantMixDescription": "nur Instant-Mix verwenden, um ähnliche Songs zu erhalten. Nützlich bei Verwendung von Plugins, die in dieses Verhalten eingreifen"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"success": "{{message}} $t(entity.track_other) zu {{numOfPlaylists}} $t(entity.playlist_other) hinzugefügt",
|
||||
"success": "$t(entity.trackWithCount, {\"count\": {{message}} }) zu $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} }) hinzugefügt",
|
||||
"title": "Zu $t(entity.playlist_one) hinzufügen",
|
||||
"input_skipDuplicates": "Duplikate überspringen",
|
||||
"input_playlists": "$t(entity.playlist_other)"
|
||||
"input_playlists": "$t(entity.playlist_other)",
|
||||
"create": "$t(entity.playlist_one) {{playlist}} erstellen",
|
||||
"searchOrCreate": "Nach $t(entity.playlist_other) suchen oder Namen eingeben, um eine neue zu erstellen"
|
||||
},
|
||||
"updateServer": {
|
||||
"title": "Server aktualisieren",
|
||||
@@ -232,12 +269,17 @@
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "Treffer Alle",
|
||||
"input_optionMatchAny": "Treffer Einige",
|
||||
"title": "query bearbeiten"
|
||||
"title": "query bearbeiten",
|
||||
"clearFilters": "Filter löschen",
|
||||
"addRuleGroup": "Regelgruppe hinzufügen",
|
||||
"removeRuleGroup": "Regelgruppe entfernen",
|
||||
"resetToDefault": "auf Standard zurücksetzen"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"title": "Bearbeite $t(entity.playlist_one)",
|
||||
"success": "$t(entity.playlist_one) erfolgreich aktualisiert",
|
||||
"publicJellyfinNote": "Jellyfin legt aus irgendwelchen Gründen nicht offen ob eine Playlist öffentlich ist oder nicht. Wenn du möchtest, dass sie öffentlich bleibt, wähle bitte diese Option aus"
|
||||
"publicJellyfinNote": "Jellyfin legt aus irgendwelchen Gründen nicht offen ob eine Playlist öffentlich ist oder nicht. Wenn du möchtest, dass sie öffentlich bleibt, wähle bitte diese Option aus",
|
||||
"editNote": "Manuelles Bearbeiten wird für große Wiedergabelisten nicht empfohlen. Bist Du sicher, dass Du die aktuelle Wiedergabeliste unter dem Risiko von Datenverlust überschrieben möchtest?"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"title": "Songtext Suche",
|
||||
@@ -249,13 +291,38 @@
|
||||
"setExpiration": "Ablaufdatum setzen",
|
||||
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
|
||||
"allowDownloading": "Herunterladen zulassen",
|
||||
"success": "Link in die Zwischenablage kopiert (oder hier klicken um zu öffnen)",
|
||||
"success": "Link in die Zwischenablage kopiert (oder hier klicken, um zu öffnen)",
|
||||
"createFailed": "fehler beim Teilen (Ist Teilen aktiviert?)"
|
||||
},
|
||||
"privateMode": {
|
||||
"enabled": "Privatmodus aktiviert, Wiedergabe-Status wird externen Quellen nicht preisgegeben",
|
||||
"disabled": "Privatmodus deaktiviert, Wiedergabe-Status wird externen Quellen preisgegeben",
|
||||
"title": "Privatmodus"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "Elemente der Wiedergabeliste hinzufügen",
|
||||
"description": "Diese Aktion fügt alle Elemente in der aktuell gefilterten Ansicht hinzu"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "Zufallswiedergabe",
|
||||
"input_genre": "$t(entity.genre_one)",
|
||||
"input_limit": "Wie viele Songs?",
|
||||
"input_minYear": "ab Jahr",
|
||||
"input_maxYear": "bis Jahr",
|
||||
"input_played_optionAll": "alle Tracks",
|
||||
"input_played_optionUnplayed": "nur nicht gespielte Tracks",
|
||||
"input_played_optionPlayed": "nur gespielte Tracks",
|
||||
"input_played": "Wiedergabefilter"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "Wiedergabeliste auf Server gespeichert"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "Radiosender erfolgreich erstellt",
|
||||
"title": "Radiosender erstellen",
|
||||
"input_homepageUrl": "Homepage URL",
|
||||
"input_name": "Name",
|
||||
"input_streamUrl": "Stream URL"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -289,25 +356,41 @@
|
||||
"genreWithCount_other": "{{count}} Genres",
|
||||
"trackWithCount_one": "{{count}} Track",
|
||||
"trackWithCount_other": "{{count}} Tracks",
|
||||
"smartPlaylist": "Smart $t(entity.playlist_one)",
|
||||
"smartPlaylist": "Intelligente $t(entity.playlist_one)",
|
||||
"play_one": "{{count}} Wiedergabe",
|
||||
"play_other": "{{count}} Wiedergaben",
|
||||
"song_one": "Lied",
|
||||
"song_other": "Lieder"
|
||||
"song_other": "Lieder",
|
||||
"radioStation_one": "Radiosender",
|
||||
"radioStation_other": "Radiosender",
|
||||
"radioStationWithCount_one": "{{count}} Radiosender",
|
||||
"radioStationWithCount_other": "{{count}} Radiosender"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
"view": {
|
||||
"table": "Tabelle",
|
||||
"card": "Karte",
|
||||
"poster": "Poster"
|
||||
"grid": "Raster",
|
||||
"list": "Liste"
|
||||
},
|
||||
"general": {
|
||||
"tableColumns": "Tabellenspalten",
|
||||
"gap": "$t(common.gap)",
|
||||
"size": "$t(common.size)",
|
||||
"displayType": "Anzeigestil",
|
||||
"autoFitColumns": "automatisch Spalten einpassen"
|
||||
"autoFitColumns": "automatisch Spalten einpassen",
|
||||
"size_default": "Standard",
|
||||
"followCurrentSong": "aktuellem Titel folgen",
|
||||
"advancedSettings": "erweiterte Einstellungen",
|
||||
"autosize": "automatische Größe",
|
||||
"alignLeft": "linksbündig",
|
||||
"alignCenter": "mittig",
|
||||
"alignRight": "rechtsbündig",
|
||||
"size_compact": "kompakt",
|
||||
"size_large": "groß",
|
||||
"pagination": "Seitenzahlen",
|
||||
"pagination_itemsPerPage": "Elemente pro Seite",
|
||||
"pagination_infinite": "unendlich"
|
||||
},
|
||||
"label": {
|
||||
"dateAdded": "Hinzugefügt am",
|
||||
@@ -335,7 +418,14 @@
|
||||
"title": "$t(common.title)",
|
||||
"year": "$t(common.year)",
|
||||
"discNumber": "disk-Nummer",
|
||||
"playCount": "anzahl abgespielt"
|
||||
"playCount": "Wiedergaben",
|
||||
"albumCount": "$t(entity.album_other)",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"codec": "$t(common.codec)",
|
||||
"image": "Bild",
|
||||
"sampleRate": "$t(common.sampleRate)",
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"genreBadge": "$t(entity.genre_one) (Abzeichen)"
|
||||
}
|
||||
},
|
||||
"column": {
|
||||
@@ -361,7 +451,11 @@
|
||||
"genre": "$t(entity.genre_one)",
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"trackNumber": "titel",
|
||||
"size": "$t(common.size)"
|
||||
"size": "$t(common.size)",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"codec": "$t(common.codec)",
|
||||
"sampleRate": "$t(common.sampleRate)",
|
||||
"owner": "Besitzer"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
@@ -380,12 +474,12 @@
|
||||
"lyricGap": "Songtext-Lücke",
|
||||
"dynamicIsImage": "Hintergrundbild aktivieren",
|
||||
"dynamicImageBlur": "Größe der Bildunschärfe",
|
||||
"lyricOffset": "Zeitversetzung des Liedtexts (ms)"
|
||||
"lyricOffset": "Zeitversatz des Songtextes (ms)"
|
||||
},
|
||||
"upNext": "als nächstes",
|
||||
"lyrics": "Songtexte",
|
||||
"related": "Ähnliche",
|
||||
"noLyrics": "Keine Liedtexte gefunden",
|
||||
"noLyrics": "Songtext nicht gefunden",
|
||||
"visualizer": "visualizer"
|
||||
},
|
||||
"appMenu": {
|
||||
@@ -400,7 +494,11 @@
|
||||
"settings": "$t(common.setting_other)",
|
||||
"quit": "$t(common.quit)",
|
||||
"privateModeOff": "Privatmodus deaktivieren",
|
||||
"privateModeOn": "Privatmodus aktivieren"
|
||||
"privateModeOn": "Privatmodus aktivieren",
|
||||
"commandPalette": "Kommandopalette öffnen",
|
||||
"selectMusicFolder": "Musikordner wählen",
|
||||
"noMusicFolder": "kein Musikordner gewählt",
|
||||
"multipleMusicFolders": "{{count}} Musikordner ausgewählt"
|
||||
},
|
||||
"home": {
|
||||
"mostPlayed": "Meistgespielt",
|
||||
@@ -408,7 +506,8 @@
|
||||
"explore": "Entdecke deine Bibliothek",
|
||||
"recentlyPlayed": "Kürzlich gespielt",
|
||||
"title": "$t(common.home)",
|
||||
"recentlyReleased": "kürzlich veröffentlicht"
|
||||
"recentlyReleased": "kürzlich veröffentlicht",
|
||||
"genres": "$t(entity.genre_other)"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "mehr von diesem $t(entity.artist_one)",
|
||||
@@ -447,7 +546,9 @@
|
||||
"shareItem": "teilen",
|
||||
"showDetails": "Informationen",
|
||||
"goToAlbum": "zu $t(entity.album_one) gehen",
|
||||
"goToAlbumArtist": "zu $t(entity.albumArtist_one) gehen"
|
||||
"goToAlbumArtist": "zu $t(entity.albumArtist_one) gehen",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"goTo": "Gehe zu"
|
||||
},
|
||||
"sidebar": {
|
||||
"nowPlaying": "läuft gerade",
|
||||
@@ -462,14 +563,33 @@
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "$t(entity.playlist_other) geteilt",
|
||||
"myLibrary": "meine bibliothek"
|
||||
"myLibrary": "meine bibliothek",
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"setting": {
|
||||
"playbackTab": "Wiedergabe",
|
||||
"generalTab": "Allgemein",
|
||||
"hotkeysTab": "Kurzbefehle",
|
||||
"windowTab": "Fenster",
|
||||
"advanced": "Erweitert"
|
||||
"advanced": "Erweitert",
|
||||
"discord": "Discord",
|
||||
"exportImport": "Importieren/Exportieren",
|
||||
"analytics": "Analyse",
|
||||
"updates": "Update",
|
||||
"cache": "Cache",
|
||||
"application": "App",
|
||||
"queryBuilder": "Abfrage-Editor",
|
||||
"theme": "Erscheinungsbild",
|
||||
"controls": "Steuerelemente",
|
||||
"sidebar": "Seitenleiste",
|
||||
"scrobble": "Scrobbeln",
|
||||
"audio": "Audio",
|
||||
"lyrics": "Songtexte",
|
||||
"transcoding": "Transcoding",
|
||||
"logger": "Logger",
|
||||
"playerFilters": "Player-Filter",
|
||||
"remote": "Fernsteuerung"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
@@ -515,14 +635,26 @@
|
||||
"copyPath": "Pfad in Zwischenablage kopieren",
|
||||
"copiedPath": "Pfad erfolgreich kopiert",
|
||||
"openFile": "Track im Dateiexplorer anzeigen"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite_other)"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"playlist": {
|
||||
"reorder": "Neuanordnung nur bei Sortierung nach ID möglich"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "Radiosender"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"next": "nächster",
|
||||
"addNext": "Als nächstes spielen",
|
||||
"addNext": "als Nächstes",
|
||||
"play": "Abspielen",
|
||||
"muted": "stummgeschaltet",
|
||||
"addLast": "ans ende einzufügen",
|
||||
"addLast": "als Letztes",
|
||||
"mute": "Stumm",
|
||||
"playRandom": "Zufällige Wiedergabe",
|
||||
"previous": "Vorheriger",
|
||||
@@ -531,11 +663,11 @@
|
||||
"playbackFetchInProgress": "lieder werden geladen…",
|
||||
"playbackSpeed": "Wiedergabegeschwindigkeit",
|
||||
"playbackFetchCancel": "Das dauert eine Weile. Schließen Sie die Benachrichtigung, um den Vorgang abzubrechen",
|
||||
"queue_clear": "Bereinige Warteschlange",
|
||||
"queue_clear": "Wiedergabeliste bereinigen",
|
||||
"repeat_all": "Alle wiederholen",
|
||||
"repeat": "Wiederholen",
|
||||
"queue_remove": "Ausgewählte entfernen",
|
||||
"shuffle": "zufallswiedergabe",
|
||||
"shuffle": "Wiedergabe (zufällig)",
|
||||
"repeat_off": "nicht wiederholen",
|
||||
"queue_moveToTop": "Ausgewählte nach unten verschieben",
|
||||
"queue_moveToBottom": "Ausgewählte nach oben verschieben",
|
||||
@@ -548,7 +680,15 @@
|
||||
"skip_forward": "vorspulen",
|
||||
"skip": "Überspringen",
|
||||
"playSimilarSongs": "Ähnliche Lieder abspielen",
|
||||
"viewQueue": "Warteschlange anzeigen"
|
||||
"viewQueue": "Wiedergabeliste anzeigen",
|
||||
"addLastShuffled": "als Letztes (zufällige Wiedergabe)",
|
||||
"addNextShuffled": "als Nächstes (zufällige Wiedergabe)",
|
||||
"queueType_default": "Standard",
|
||||
"queueType_priority": "Priorität",
|
||||
"holdToShuffle": "Halten für Zufallswiedergabe",
|
||||
"queueType": "Wiedergabelistentyp",
|
||||
"restoreQueueFromServer": "Wiedergabeliste von Server wiederherstellen",
|
||||
"saveQueueToServer": "Wiedergabeliste auf Server speichern"
|
||||
},
|
||||
"setting": {
|
||||
"audioDevice_description": "wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer)",
|
||||
@@ -559,10 +699,10 @@
|
||||
"applicationHotkeys": "anwendungs Kurzbefehle",
|
||||
"applicationHotkeys_description": "konfiguriere die Tastenkombinationen der Anwendung. Setze einen Haken, um die Tastenkombination global zu verwenden (nur für die Desktopanwendung)",
|
||||
"crossfadeStyle_description": "Wählen Sie Art des Überblendungseffekts aus, welcher für den Audioplayer verwendet werden soll",
|
||||
"discordIdleStatus_description": "wenn aktiviert wird der rich presence status aktiviert, wenn sich der Player im Leerlauf befindet",
|
||||
"discordIdleStatus_description": "Status aktualisieren, während die Wiedergabe pausiert ist",
|
||||
"audioExclusiveMode_description": "Aktivieren Sie den exklusiven Ausgabemodus. In diesem Modus ist das System normalerweise gesperrt und nur MPV ist in der Lage Audio ausgeben",
|
||||
"disableLibraryUpdateOnStartup": "beim Start nicht nach neuen Versionen suchen",
|
||||
"discordApplicationId_description": "die Application-ID für {{discord}} rich presence (Standard: {{defaultId}})",
|
||||
"discordApplicationId_description": "die Application-ID für {{discord}} Rich Presence (Standard: {{defaultId}})",
|
||||
"audioPlayer_description": "Wählen Sie den Audioplayer aus, der für die Wiedergabe verwendet werden soll",
|
||||
"disableAutomaticUpdates": "Automatische Updates deaktivieren",
|
||||
"crossfadeDuration_description": "Legt die Dauer der Überblendung fest",
|
||||
@@ -575,26 +715,26 @@
|
||||
"remotePort_description": "Legt den Port des Fernsteuerungsserver fest",
|
||||
"hotkey_skipBackward": "rückwärts springen",
|
||||
"replayGainMode_description": "Passen Sie die Lautstärkeverstärkung entsprechend den in den Dateimetadaten gespeicherten {{ReplayGain}}-Werten an",
|
||||
"volumeWheelStep_description": "die Lautstärke, die beim Scrollen des Mausrads auf dem Lautstärkeregler geändert werden soll",
|
||||
"theme_description": "Legt das für die Anwendung zu verwendende Thema fest",
|
||||
"volumeWheelStep_description": "Die Lautstärkeänderung beim Scrollen mit dem Mausrad auf dem Lautstärkeregler",
|
||||
"theme_description": "Legt das für die Anwendung zu verwendende Erscheinungsbild fest",
|
||||
"hotkey_playbackPause": "Pause",
|
||||
"sidebarCollapsedNavigation_description": "Zeigt die Navigation in der minimierten Seitenleiste an oder verbirgt sie",
|
||||
"hotkey_volumeUp": "Lauter",
|
||||
"skipDuration": "Sprungdauer",
|
||||
"showSkipButtons": "Schaltflächen zum Überspringen anzeigen",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"minimumScrobblePercentage": "minimale Scrobble-Dauer (Prozentsatz)",
|
||||
"minimumScrobblePercentage": "Minimum Scrobble-Dauer (Prozentsatz)",
|
||||
"lyricFetch": "Songtexte aus dem Internet abrufen",
|
||||
"scrobble": "Scrobbeln",
|
||||
"scrobble": "scrobbel",
|
||||
"skipDuration_description": "Legt die zu überspringende Dauer fest, wenn die Überspringen-Schaltflächen in der Player-Leiste verwendet werden",
|
||||
"mpvExecutablePath_description": "Legt den Pfad zur ausführbaren MPV-Datei fest. Wenn leer gelassen, wird der Standard-Pfad verwendet",
|
||||
"mpvExecutablePath_description": "Legt den Pfad zur ausführbaren MPV-Datei fest. Wenn leer gelassen, wird der Standardpfad verwendet",
|
||||
"replayGainClipping_description": "Verhindern Sie durch {{ReplayGain}} verursachtes Clipping, indem Sie die Verstärkung automatisch verringern",
|
||||
"replayGainPreamp": "{{ReplayGain}} Vorverstärker (db)",
|
||||
"hotkey_favoriteCurrentSong": "Favorit $t(common.currentSong)",
|
||||
"sampleRate": "Abtastrate",
|
||||
"sidePlayQueueStyle_optionAttached": "angefügt",
|
||||
"sidebarConfiguration": "Seitenleistenkonfiguration",
|
||||
"sampleRate_description": "Wähle die auszugebende Abtastrate aus, wenn sich die ausgewählte Abtastfrequenz von der des aktuellen Mediums unterscheidet. Ein Wert unter 8000 wird die Standard-Frequenz verwenden",
|
||||
"sampleRate_description": "Wähle die auszugebende Abtastrate aus, wenn sich die ausgewählte Abtastfrequenz von der des aktuellen Mediums unterscheidet. Ein Wert unter 8000 wird die Standardfrequenz verwenden",
|
||||
"replayGainMode_optionNone": "$t(common.none)",
|
||||
"hotkey_zoomIn": "Hineinzoomen",
|
||||
"scrobble_description": "Scrobble wird auf Ihrem Medienserver abgespielt",
|
||||
@@ -613,51 +753,51 @@
|
||||
"remoteUsername_description": "Legt den Benutzernamen für den Fernsteuerungsserver fest. Wenn sowohl Benutzername als auch Passwort leer sind, wird die Authentifizierung deaktiviert",
|
||||
"hotkey_favoritePreviousSong": "Favorit $t(common.previousSong)",
|
||||
"replayGainMode_optionAlbum": "$t(entity.album_one)",
|
||||
"lyricOffset": "Liedtext-Versatz (ms)",
|
||||
"themeDark_description": "Legt das dunkle Design fest, das für die Anwendung verwendet werden soll",
|
||||
"lyricOffset": "Zeitversatz des Songtextes (ms)",
|
||||
"themeDark_description": "Legt das Erscheinungsbild für den dunklen Modus fest",
|
||||
"remotePassword": "Passwort des Fernsteuerungsservers",
|
||||
"lyricFetchProvider": "Anbieter, von denen Liedtexte abgerufen werden können",
|
||||
"lyricFetchProvider": "Anbieter, von denen Songtexte abgerufen werden können",
|
||||
"language_description": "Legt die Sprache für die Anwendung fest $t(common.restartRequired)",
|
||||
"playbackStyle_optionCrossFade": "Überblendung",
|
||||
"hotkey_rate3": "Bewertung 3 Sterne",
|
||||
"replayGainMode_optionTrack": "$t(entity.track_one)",
|
||||
"themeLight_description": "Legt das helle Thema fest, das für die Anwendung verwendet werden soll",
|
||||
"themeLight_description": "Legt das Erscheinungsbild für den hellen Modus fest",
|
||||
"hotkey_toggleFullScreenPlayer": "Vollbildmodus umschalten",
|
||||
"hotkey_localSearch": "Suche auf Seite",
|
||||
"hotkey_toggleQueue": "Warteschlange umschalten",
|
||||
"remotePassword_description": "Legt das Passwort für den Fernsteuerungsserver fest. Diese Anmeldeinformationen werden standardmäßig unsicher übertragen, daher sollten Sie ein eindeutiges Passwort verwenden, das Ihnen egal ist",
|
||||
"hotkey_toggleQueue": "Wiedergabeliste umschalten",
|
||||
"remotePassword_description": "Legt das Passwort für den Fernsteuerungsserver fest. Diese Anmeldeinformationen werden standardmäßig unsicher übertragen, daher sollten Sie ein Passwort verwenden, das Ihnen egal ist",
|
||||
"hotkey_rate5": "Bewertung 5 Sterne",
|
||||
"hotkey_playbackPrevious": "Vorheriger Track",
|
||||
"showSkipButtons_description": "Ein- oder Ausblenden der Überspringen-Schaltflächen in der Player-Leiste",
|
||||
"playbackStyle": "Wiedergabestil",
|
||||
"hotkey_toggleShuffle": "Zufallswiedergabe umschalten",
|
||||
"theme": "Thema",
|
||||
"theme": "Erscheinungsbild",
|
||||
"playbackStyle_description": "Wählen Sie den Wiedergabestil aus, der für den Audioplayer verwendet werden soll",
|
||||
"mpvExecutablePath": "Pfad der ausführbaren MPV-Datei",
|
||||
"hotkey_rate2": "Bewertung 2 Sterne",
|
||||
"playButtonBehavior_description": "Legt das Standardverhalten der Wiedergabeschaltfläche fest, wenn Songs zur Warteschlange hinzugefügt werden",
|
||||
"minimumScrobblePercentage_description": "Der Mindestprozentsatz des Songs, der gespielt werden muss, bevor er gescrobbelt wird",
|
||||
"playButtonBehavior_description": "legt das Standardverhalten des Wiedergabe-Buttons fest, wenn Lieder zur Wiedergabeliste hinzugefügt werden",
|
||||
"minimumScrobblePercentage_description": "die Mindestdauer in Prozent, welche das Lied gespielt werden muss, bevor dieses gescrobbelt wird",
|
||||
"hotkey_rate4": "Bewertung 4 Sterne",
|
||||
"showSkipButton_description": "Ein- oder Ausblenden der Überspringen-Schaltflächen in der Player-Leiste",
|
||||
"savePlayQueue": "Wiedergabe-Warteschlange speichern",
|
||||
"minimumScrobbleSeconds_description": "die Mindestdauer in Sekunden, die das Lied abspielen muss, bevor es gescrobbelt wird",
|
||||
"skipPlaylistPage_description": "Gehen Sie beim Navigieren zu einer Wiedergabeliste zur Titelseite der Wiedergabeliste und nicht zur Standardseite",
|
||||
"savePlayQueue": "Wiedergabeliste speichern",
|
||||
"minimumScrobbleSeconds_description": "die Mindestdauer in Sekunden, welche das Lied gespielt werden muss, bevor dieses gescrobbelt wird",
|
||||
"skipPlaylistPage_description": "Gehe beim Navigieren zu einer Wiedergabeliste zu deren Titelseite und nicht zur Standardseite",
|
||||
"fontType_description": "Die integrierte Schriftart wählt eine der von feishin bereitgestellten Schriftarten aus. Mit der Systemschriftart können Sie jede von Ihrem Betriebssystem bereitgestellte Schriftart auswählen. Benutzerdefiniert erlaubt es eine eigene Schriftart bereitzustellen",
|
||||
"playButtonBehavior": "Verhalten der Wiedergabetaste",
|
||||
"volumeWheelStep": "Lautstärkeregler Stufe",
|
||||
"volumeWheelStep": "Lautstärkeänderung mit Mausrad",
|
||||
"sidebarPlaylistList_description": "Ein- oder Ausblenden der Playlisten-Liste in der Seitenleiste",
|
||||
"sidePlayQueueStyle_description": "Legt den Stil der Wiedergabewarteliste in der Seitenleiste fest",
|
||||
"sidePlayQueueStyle_description": "legt den Stil der Wiedergabeliste in der Seitenleiste fest",
|
||||
"replayGainMode": "{{ReplayGain}} Modus",
|
||||
"playbackStyle_optionNormal": "Normal",
|
||||
"windowBarStyle": "Fensterleistenstil",
|
||||
"replayGainFallback_description": "Verstärkung in db, die angewendet werden soll, wenn die Datei keine {{ReplayGain}}-Tags hat",
|
||||
"replayGainPreamp_description": "Passen Sie die Vorverstärkerverstärkung an, die auf die {{ReplayGain}}-Werte angewendet wird",
|
||||
"hotkey_toggleRepeat": "Wiederholung umschalten",
|
||||
"lyricOffset_description": "Versetzen Sie den Liedtext um die angegebene Anzahl von Millisekunden",
|
||||
"lyricOffset_description": "Versetzen Sie den Songtext um die angegebene Anzahl von Millisekunden",
|
||||
"sidebarConfiguration_description": "Wählen Sie die Elemente und die Reihenfolge aus, in der sie in der Seitenleiste angezeigt werden",
|
||||
"remotePort": "Port des Fernsteuerungsserver",
|
||||
"hotkey_playbackNext": "Nächster Track",
|
||||
"useSystemTheme_description": "der systemdefinierten Hell- oder Dunkelpräferenz folgen",
|
||||
"useSystemTheme_description": "Folgt dem hellen oder dunklen Erscheinungsbild des Systems",
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"lyricFetch_description": "Songtexte aus verschiedenen Internetquellen abrufen",
|
||||
"lyricFetchProvider_description": "Wählen Sie die Anbieter aus, von denen Sie Liedtexte abrufen möchten. Die Reihenfolge der Anbieter ist die Reihenfolge, in der sie abgefragt werden",
|
||||
@@ -672,67 +812,220 @@
|
||||
"sidebarPlaylistList": "Seitenleiste Playlisten-Liste",
|
||||
"minimizeToTray": "Zur Taskleiste minimieren",
|
||||
"skipPlaylistPage": "Playlisten-Seite überspringen",
|
||||
"themeDark": "Thema (dunkel)",
|
||||
"themeDark": "Erscheinungsbild (dunkel)",
|
||||
"sidebarCollapsedNavigation": "Navigation in der Seitenleiste (komprimiert)",
|
||||
"gaplessAudio_optionWeak": "schwach (empfohlen)",
|
||||
"minimumScrobbleSeconds": "minimales Scrobble (Sekunden)",
|
||||
"minimumScrobbleSeconds": "Minimum Scrobble-Dauer (Sekunden)",
|
||||
"hotkey_playbackStop": "Stoppen",
|
||||
"savePlayQueue_description": "Speichert Wiedergabewarteschlange, wenn die Anwendung geschlossen wird, und stellt sie wieder her, wenn die Anwendung geöffnet wird",
|
||||
"useSystemTheme": "Systemdesign verwenden",
|
||||
"enableRemote_description": "aktiviere den eingebauten Webserver, um die Anwendung von anderen Geräten aus zu steuern",
|
||||
"savePlayQueue_description": "speichert die Wiedergabeliste beim Schließen der Anwendung, und stellt diese wieder her, wenn die Anwendung geöffnet wird",
|
||||
"useSystemTheme": "Nach Erscheinungsbild des Systems richten",
|
||||
"enableRemote_description": "Aktiviert den Server für die Fernsteuerung, damit andere Geräte die Anwendung steuern können",
|
||||
"fontType_optionSystem": "System Schriftart",
|
||||
"discordUpdateInterval": "{{discord}} rich presence Aktualisierungsintervall",
|
||||
"discordUpdateInterval": "{{discord}} Rich Presence Aktualisierungsintervall",
|
||||
"fontType_optionBuiltIn": "eingebaute Schriftart",
|
||||
"gaplessAudio": "unterbrechungsfreie Wiedergabe",
|
||||
"exitToTray_description": "die Anwendung beim Schließen in die Taskleiste minimieren",
|
||||
"followLyric_description": "der Songtext scrollt automatisch mir der Wiedergabe",
|
||||
"discordUpdateInterval_description": "zeit in Sekunden zwischen den Statusupdates (Minimum: 15s)",
|
||||
"followLyric_description": "der Songtext bewegt sich mit der Wiedergabeposition",
|
||||
"discordUpdateInterval_description": "Zeit in Sekunden zwischen Aktualisierungen (min. 15 Sekunden)",
|
||||
"fontType_optionCustom": "benutzerdefinierte Schriftart",
|
||||
"font": "Schriftart",
|
||||
"exitToTray": "In die Taskleiste minimieren",
|
||||
"enableRemote": "server für Fernzugriff aktivieren",
|
||||
"floatingQueueArea": "Beim Darüberfahren schwebende Warteschlange anzeigen",
|
||||
"enableRemote": "Server für Fernsteuerung aktivieren",
|
||||
"fontType": "schriftartenquelle",
|
||||
"followLyric": "songtext synchronisieren",
|
||||
"floatingQueueArea_description": "zeige ein Icon auf der rechten Seite, um beim Darüberfahren die Wartschlange anzuzeigen",
|
||||
"followLyric": "aktuellen songtext synchronisieren",
|
||||
"font_description": "wähle die Schriftart für die Anwendung",
|
||||
"themeLight": "Thema (hell)",
|
||||
"themeLight": "Erscheinungsbild (hell)",
|
||||
"sidePlayQueueStyle_optionDetached": "lösgelöst",
|
||||
"windowBarStyle_description": "Wähle den Stil der Windows-Leiste",
|
||||
"windowBarStyle_description": "Legt das Erscheinungsbild des Fensterrahmens fest",
|
||||
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) zu Favoriten hinzufügen",
|
||||
"clearQueryCache_description": "\"Weiches\" Zurücksetzen. Dies wird Playlisten, Musik-Metadaten und gespeicherte Liedtexte zurücksetzen, Zugangsinformationen und zwischengespeicherte Bilder werden behalten",
|
||||
"discordRichPresence_description": "zeige deinen Wiedergabe-Status in {{discord}} als rich presence an. Angezeigte Bilder sind: {{icon}}, {{playing}}, und {{paused}}",
|
||||
"discordRichPresence_description": "Aktiviert den Wiedergabestatus in {{discord}} Rich Presence. Angezeigte Bilder sind: {{icon}}, {{playing}}, und {{paused}}",
|
||||
"clearCache": "Browser-Zwischenspeicher löschen",
|
||||
"clearQueryCache": "feishins Zwischenspeicher leeren",
|
||||
"clearCache_description": "Hartes Zurücksetzen. Neben feishins Zwischenspeicher wird auch der des Browsers gelöscht (Bilder und andere Daten). Zugangsinformationen und Einstellungen werden behalten",
|
||||
"sidePlayQueueStyle": "Wiedergabelistenstil in der Seitenleiste",
|
||||
"sidePlayQueueStyle": "Stil der Wiedergabeliste in der Seitenleiste",
|
||||
"zoom_description": "Setzt den Zoom (in %) für das Programm",
|
||||
"zoom": "Zoom",
|
||||
"albumBackground": "Album Hintergrund",
|
||||
"customCss": "Benutzerdefiniert css",
|
||||
"customCss": "Benutzerdefiniertes CSS",
|
||||
"homeConfiguration": "Startseite Konfiguration",
|
||||
"lastfmApiKey": "{{lastfm}} API-Schlüssel",
|
||||
"lastfmApiKey_description": "Der API-Schlüssel für {{lastfm}}. wird für benötigt",
|
||||
"lastfmApiKey_description": "Der API-Schlüssel für {{lastfm}}. wird für Albumcover benötigt",
|
||||
"discordListening": "Status als hört zu anzeigen",
|
||||
"discordListening_description": "Status als hört zu statt als spielt anzeigen",
|
||||
"lastfm": "zeige last.fm links",
|
||||
"lastfm_description": "zeige links zu Last.fm auf dem Künstler/Album-Seiten",
|
||||
"musicbrainz": "Zeig MusicBrainz links",
|
||||
"customCssEnable": "aktiviere Benutzerdefinierte css",
|
||||
"customCssEnable": "benutzerdefiniertes CSS aktivieren",
|
||||
"albumBackground_description": "fügt ein Hintergrundbild für die Albumseiten hinzu, welche das Albumcover zeigen",
|
||||
"albumBackgroundBlur": "Größe der Album-Bildunschärfe",
|
||||
"albumBackgroundBlur_description": "passt die Stärke der Unschärfe an, welche auf das Hintergrundbild des Albums angewandt wird",
|
||||
"clearCacheSuccess": "Cache erfolgreich geleert",
|
||||
"contextMenu": "Kontextmenü-Einstellungen (Rechtsklick)",
|
||||
"customCssEnable_description": "ermöglicht das Schreiben benutzerdefinierten CSS",
|
||||
"doubleClickBehavior": "bei Doppelklick alle gesuchten Tracks zur Warteschlange hinzufügen",
|
||||
"customCssEnable_description": "erlaubt das Hinzufügen von benutzerdefiniertem CSS",
|
||||
"artistBackground": "Künstler Hintergrundbild",
|
||||
"artistBackground_description": "fügt ein Hintergrundbild für die Künstlerseite hinzu",
|
||||
"artistConfiguration": "künstler Albumseite Konfiguration",
|
||||
"buttonSize": "spielerleisten-Knopfgröße",
|
||||
"buttonSize_description": "die Größe der Spieler-Knöpfe",
|
||||
"hotkey_togglePreviousSongFavorite": "wähle $t(common.previousSong) als Favorit aus",
|
||||
"replayGainFallback": "{{ReplayGain}} Rückgriff",
|
||||
"replayGainClipping": "{{ReplayGain}} Clipping"
|
||||
"replayGainFallback": "{{ReplayGain}} Alternative",
|
||||
"replayGainClipping": "{{ReplayGain}} Clipping",
|
||||
"exportImportSettings_control_description": "Einstellungen mit JSON exportieren und importieren",
|
||||
"exportImportSettings_control_exportText": "Einstellungen exportieren",
|
||||
"exportImportSettings_control_importText": "Einstellungen importieren",
|
||||
"exportImportSettings_control_title": "Einstellungen importieren / exportieren",
|
||||
"exportImportSettings_importBtn": "Einstellungen importieren",
|
||||
"exportImportSettings_importModalTitle": "Feishin Einstellungen importieren",
|
||||
"exportImportSettings_importSuccess": "Einstellungen wurden erfolgreich importiert!",
|
||||
"exportImportSettings_notValidJSON": "Die Datei ist kein gültiges JSON",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" ist falsch - {{reason}}",
|
||||
"language": "Sprache",
|
||||
"imageAspectRatio": "Original Seitenverhältnis des Albumcovers verwenden",
|
||||
"analyticsDisable": "Keine nutzungsbasierte Analyse",
|
||||
"analyticsDisable_description": "Anonymisierte Nutzungsdaten werden an den Entwickler geschickt, um die Anwendung zu verbessern",
|
||||
"logLevel_optionDebug": "Debug",
|
||||
"logLevel_description": "legt die Protokollstufe fest. \"Debug\" zeigt alle Protokollierungen an. \"Fehler\" zeigt nur Fehler an",
|
||||
"logLevel": "Protokolllevel",
|
||||
"logLevel_optionError": "Fehler",
|
||||
"logLevel_optionInfo": "Info",
|
||||
"logLevel_optionWarn": "Warnung",
|
||||
"autoDJ_description": "füge automatisch ähnliche Lieder der Wiedergabeliste hinzu",
|
||||
"autoDJ": "Auto DJ",
|
||||
"autoDJ_itemCount": "Anzahl",
|
||||
"autoDJ_itemCount_description": "die Anzahl der Lieder, die bei aktiviertem Auto DJ zur Wiedergabeliste hinzugefügt werden sollen",
|
||||
"autoDJ_timing_description": "die Anzahl der Lieder, die sich noch in der Wiedergabeliste befinden, bevor Auto DJ ausgelöst wird",
|
||||
"autoDJ_timing": "Timing",
|
||||
"discordDisplayType": "{{discord}} Presence Darstellungsart",
|
||||
"discordLinkType_mbz_lastfm": "{{musicbrainz}} mit {{lastfm}} als Ersatz",
|
||||
"discordLinkType_none": "$t(common.none)",
|
||||
"discordLinkType": "{{discord}} Presence Links",
|
||||
"discordPausedStatus_description": "Wenn aktiviert, wird der Status auch angezeigt, falls die Wiedergabe pausiert",
|
||||
"discordPausedStatus": "Zeige Rich Presence bei Pause",
|
||||
"discordRichPresence": "{{discord}} Rich Presence",
|
||||
"discordServeImage": "Bilder für {{discord}} vom Server beziehen",
|
||||
"discordServeImage_description": "Bezieht die Coverbilder für {{discord}} Rich Presence vom Server selbst. Nur verfügbar für Jellyfin und Navidrome. Damit der Bot von {{discord}} die Coverbilder abrufen kann, muss der Server öffentlich erreichbar sein",
|
||||
"enableAutoTranslation": "Automatische Übersetzung aktivieren",
|
||||
"externalLinks": "Externe Links anzeigen",
|
||||
"externalLinks_description": "Aktiviert die Anzeige externer Links (Last.fm, MusicBrainz) auf Artist/Album Seiten",
|
||||
"musicbrainz_description": "Zeige Links zu MusicBrainz auf Artist/Album Seite, falls MusicBrainz ID vorhanden",
|
||||
"neteaseTranslation_description": "Wenn aktiviert, werden Songtextübersetzungen von NetEase abgerufen und angezeigt, sofern verfügbar",
|
||||
"neteaseTranslation": "NetEase Übersetzungen aktivieren",
|
||||
"notify": "Benachrichtigungen aktivieren",
|
||||
"notify_description": "Zeigt Benachrichtigungen beim Titelwechsel",
|
||||
"playerFilters": "Lieder der Wiedergabeliste filtern",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"volumeWidth_description": "Die Breite des Lautstärkereglers",
|
||||
"volumeWidth": "Lautstärkereglerbreite",
|
||||
"webAudio_description": "Web-Audio verwenden. Dies ermöglicht erweiterte Funktionen wie ReplayGain. Deaktiviere die Option, falls bei der Wiedergabe Probleme auftreten",
|
||||
"webAudio": "Web-Audio verwenden",
|
||||
"trayEnabled": "Info-Symbol anzeigen",
|
||||
"transcode": "Transkodierung aktivieren",
|
||||
"transcode_description": "Aktiviert die Umwandlung in verschiedene Formate",
|
||||
"transcodeBitrate_description": "Legt die Bitrate für die Umwandlung fest. Bei 0 wird die Wahl dem Server überlassen",
|
||||
"transcodeBitrate": "Bitrate für Umwandlung",
|
||||
"transcodeFormat_description": "Legt das Format für die Umwandlung fest. Leer lassen, um den Server entscheiden zu lassen",
|
||||
"transcodeFormat": "Format für Umwandlung",
|
||||
"startMinimized_description": "Startet die Anwendung im Info-Bereich",
|
||||
"startMinimized": "Im Info-Bereich starten",
|
||||
"mediaSession_description": "Aktiviert die Windows Media Session-Integration, zeigt Mediensteuerelemente und Metadaten im Systemlautstärke-Overlay und auf dem Sperrbildschirm an (nur Windows)",
|
||||
"mediaSession": "Media Session aktivieren",
|
||||
"artistBackgroundBlur": "Unschärfegrad für Künstlerhintergründe",
|
||||
"artistBackgroundBlur_description": "Legt den Grad der Unschärfe fest, der auf das Hintergrundbild des Künstlers angewendet wird",
|
||||
"artistConfiguration_description": "Legt fest, welche Elemente auf der Albumkünstlerseite angezeigt werden und in welcher Reihenfolge",
|
||||
"contextMenu_description": "Legt die Einträge fest, die im Rechtsklick-Menü angezeigt werden sollen. Abgewählte Einträge werden ausgeblendet",
|
||||
"crossfadeStyle": "Art der Überblende",
|
||||
"customCss_description": "Benutzerdefinierter CSS-Inhalt. Hinweis: Inhalte und Remote-URLs sind nicht zulässige Eigenschaften. Unten siehst du eine Vorschau deines Inhalts. Aufgrund von Bereinigung werden womöglich zusätzliche, nicht von dir definierte Felder angezeigt",
|
||||
"customCssNotice": "Warnung: Obwohl eine gewisse Bereinigung erfolgt (url() und content: sind nicht zulässig), kann die Verwendung von benutzerdefiniertem CSS dennoch Risiken mit sich bringen, da dadurch die Benutzeroberfläche verändert wird",
|
||||
"releaseChannel_optionBeta": "Beta",
|
||||
"releaseChannel_optionLatest": "Stabil",
|
||||
"releaseChannel": "Veröffentlichungskanal",
|
||||
"releaseChannel_description": "Zwischen stabilen und beta Veröffentlichungen für automatische Aktualisierungen wählen",
|
||||
"discordDisplayType_artistname": "Künstlername(n)",
|
||||
"discordDisplayType_description": "Ändert den aktuellen Titel im Zuhör-Status",
|
||||
"discordDisplayType_songname": "Songtitel",
|
||||
"discordLinkType_description": "Fügt externe Links zu {{lastfm}} oder {{musicbrainz}} zu Song- und Künstlerfeldern in {{discord}} Rich Presence hinzu. {{musicbrainz}} ist am genauesten, erfordert jedoch Tags und bietet keine Künstlerlinks, während {{lastfm}} immer einen Link bereitstellen sollte. Verursacht keine zusätzlichen Netzwerkabfragen",
|
||||
"enableAutoTranslation_description": "Automatische Übersetzung von Songtexten aktivieren",
|
||||
"exportImportSettings_destructiveWarning": "Das Importieren von Einstellungen ist irreversibel. Bitte lies die Hinweise oben sorgfältig durch, bevor du auf \"Importieren\" klickst!",
|
||||
"followCurrentSong": "aktuellem Titel folgen",
|
||||
"followCurrentSong_description": "die Wiedergabeliste scrollt automatisch zum aktuellen Titel",
|
||||
"playerFilters_description": "verhindert, dass Titel anhand der folgenden Kriterien zur Wiedergabeliste hinzugefügt werden",
|
||||
"preferLocalLyrics_description": "lokale Songtexte gegenüber externen Quellen bevorzugen (sofern verfügbar)",
|
||||
"preferLocalLyrics": "Priorisiere lokale Songtexte",
|
||||
"showLyricsInSidebar_description": "ein Bereich, in dem Songtexte angezeigt werden, wird der Wiedergabeliste hinzugefügt",
|
||||
"showLyricsInSidebar": "zeige Songtexte in der Player-Seitenleiste",
|
||||
"homeFeature_description": "steuert, ob das große Featured-Karussell auf der Startseite angezeigt wird",
|
||||
"homeFeature": "Feature-Karussell",
|
||||
"playerbarWaveformAlign_optionTop": "Oben",
|
||||
"playerbarWaveformAlign_optionCenter": "Mitte",
|
||||
"playerbarWaveformAlign_optionBottom": "Unten",
|
||||
"translationApiKey_description": "API-Schlüssel für Übersetzungen (nur globale Service-Endpunkte)",
|
||||
"translationApiKey": "API-Schlüssel für Übersetzungen",
|
||||
"translationApiProvider_description": "API-Anbieter für Übersetzungen",
|
||||
"translationApiProvider": "API-Anbieter für Übersetzungen",
|
||||
"hotkey_navigateHome": "zurück zur Startseite",
|
||||
"translationTargetLanguage_description": "die gewünschte Sprache der Übersetzung",
|
||||
"translationTargetLanguage": "Zielsprache der Übersetzung",
|
||||
"queryBuilderCustomFields": "benutzerdefiniertes Feld",
|
||||
"queryBuilderCustomFields_inputTag": "Tag"
|
||||
},
|
||||
"dragDropZone": {
|
||||
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
|
||||
"error_readingFile": "Beim Lesen der Datei trat ein Fehler auf: {{errorMessage}}",
|
||||
"mainText": "Datei hier ablegen"
|
||||
},
|
||||
"filterOperator": {
|
||||
"contains": "enthält",
|
||||
"endsWith": "endet mit",
|
||||
"inPlaylist": "ist in",
|
||||
"inTheLast": "ist in den letzten",
|
||||
"is": "ist",
|
||||
"isNot": "ist nicht",
|
||||
"isGreaterThan": "ist größer als",
|
||||
"isLessThan": "ist kleiner als",
|
||||
"notContains": "enthält nicht",
|
||||
"notInPlaylist": "ist nicht in",
|
||||
"notInTheLast": "ist nicht in den letzten",
|
||||
"startsWith": "beginnt mit",
|
||||
"after": "ist nach",
|
||||
"afterDate": "ist nach (Datum)",
|
||||
"before": "ist vor",
|
||||
"beforeDate": "ist vor (Datum)",
|
||||
"inTheRange": "ist im Bereich",
|
||||
"inTheRangeDate": "ist im Bereich (Datum)",
|
||||
"matchesRegex": "entspricht Regex"
|
||||
},
|
||||
"queryBuilder": {
|
||||
"standardTags": "Standardtags",
|
||||
"customTags": "benutzerdefinierte Tags"
|
||||
},
|
||||
"releaseType": {
|
||||
"primary": {
|
||||
"album": "$t(entity.album_one)",
|
||||
"broadcast": "Broadcast",
|
||||
"ep": "EP",
|
||||
"other": "andere",
|
||||
"single": "Single"
|
||||
},
|
||||
"secondary": {
|
||||
"audiobook": "Hörbuch",
|
||||
"audioDrama": "Hörspiel",
|
||||
"compilation": "Compilation",
|
||||
"djMix": "DJ Mix",
|
||||
"demo": "Demo",
|
||||
"fieldRecording": "Außenaufnahme",
|
||||
"interview": "Interview",
|
||||
"live": "Live",
|
||||
"mixtape": "Mixtape",
|
||||
"remix": "Remix",
|
||||
"soundtrack": "Soundtrack",
|
||||
"spokenWord": "Gesprochenes Wort"
|
||||
}
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "Min",
|
||||
"secondShort": "Sek",
|
||||
"hourShort": "Std",
|
||||
"dayShort": "Tag"
|
||||
}
|
||||
}
|
||||
|
||||
+392
-20
@@ -2,15 +2,29 @@
|
||||
"action": {
|
||||
"addToFavorites": "add to $t(entity.favorite_other)",
|
||||
"addToPlaylist": "add to $t(entity.playlist_one)",
|
||||
"addOrRemoveFromSelection": "add or remove from selection",
|
||||
"selectRangeOfItems": "select a range of items",
|
||||
"clearQueue": "clear queue",
|
||||
"createPlaylist": "create $t(entity.playlist_one)",
|
||||
"createRadioStation": "create $t(entity.radioStation_one)",
|
||||
"deletePlaylist": "delete $t(entity.playlist_one)",
|
||||
"deleteRadioStation": "delete $t(entity.radioStation_one)",
|
||||
"selectAll": "select all",
|
||||
"deselectAll": "deselect all",
|
||||
"downloadStarted": "started download of {{count}} items",
|
||||
"editPlaylist": "edit $t(entity.playlist_one)",
|
||||
"goToPage": "go to page",
|
||||
"moveToNext": "move to next",
|
||||
"moveToBottom": "move to bottom",
|
||||
"moveToTop": "move to top",
|
||||
"moveUp": "move up",
|
||||
"moveDown": "move down",
|
||||
"holdToMoveToTop": "hold to move to top",
|
||||
"holdToMoveToBottom": "hold to move to bottom",
|
||||
"moveItems": "move items",
|
||||
"shuffle": "shuffle",
|
||||
"shuffleAll": "shuffle all",
|
||||
"shuffleSelected": "shuffle selected",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromFavorites": "remove from $t(entity.favorite_other)",
|
||||
"removeFromPlaylist": "remove from $t(entity.playlist_one)",
|
||||
@@ -18,12 +32,15 @@
|
||||
"setRating": "set rating",
|
||||
"toggleSmartPlaylistEditor": "toggle $t(entity.smartPlaylist) editor",
|
||||
"viewPlaylists": "view $t(entity.playlist_other)",
|
||||
"viewMore": "view more",
|
||||
"openApplicationDirectory": "open application directory",
|
||||
"openIn": {
|
||||
"lastfm": "Open in Last.fm",
|
||||
"musicbrainz": "Open in MusicBrainz"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"countSelected": "{{count}} selected",
|
||||
"explicitStatus": "explicit status",
|
||||
"action_one": "action",
|
||||
"action_other": "actions",
|
||||
@@ -60,10 +77,14 @@
|
||||
"disable": "disable",
|
||||
"disc": "disc",
|
||||
"dismiss": "dismiss",
|
||||
"doNotShowAgain": "do not show this again",
|
||||
"duration": "duration",
|
||||
"view": "view",
|
||||
"edit": "edit",
|
||||
"enable": "enable",
|
||||
"expand": "expand",
|
||||
"externalLinks": "external links",
|
||||
"faster": "faster",
|
||||
"favorite": "favorite",
|
||||
"filter_one": "filter",
|
||||
"filter_other": "filters",
|
||||
@@ -85,6 +106,7 @@
|
||||
"no": "no",
|
||||
"none": "none",
|
||||
"noResultsFromQuery": "the query returned no results",
|
||||
"noFilters": "no filters configured",
|
||||
"note": "note",
|
||||
"ok": "ok",
|
||||
"owner": "owner",
|
||||
@@ -97,6 +119,7 @@
|
||||
"quit": "quit",
|
||||
"random": "random",
|
||||
"rating": "rating",
|
||||
"retry": "retry",
|
||||
"recordLabel": "record label",
|
||||
"releaseType": "release type",
|
||||
"refresh": "refresh",
|
||||
@@ -113,8 +136,10 @@
|
||||
"setting": "setting",
|
||||
"setting_one": "setting",
|
||||
"setting_other": "settings",
|
||||
"slower": "slower",
|
||||
"share": "share",
|
||||
"size": "size",
|
||||
"sort": "sort",
|
||||
"sortOrder": "order",
|
||||
"tags": "tags",
|
||||
"title": "title",
|
||||
@@ -127,7 +152,10 @@
|
||||
"year": "year",
|
||||
"yes": "yes",
|
||||
"explicit": "explicit",
|
||||
"clean": "clean"
|
||||
"clean": "clean",
|
||||
"gridRows": "grid rows",
|
||||
"tableColumns": "table columns",
|
||||
"itemsMore": "{{count}} more"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "album",
|
||||
@@ -138,6 +166,10 @@
|
||||
"albumArtistCount_other": "{{count}} album artists",
|
||||
"albumWithCount_one": "{{count}} album",
|
||||
"albumWithCount_other": "{{count}} albums",
|
||||
"radioStation_one": "radio station",
|
||||
"radioStation_other": "radio stations",
|
||||
"radioStationWithCount_one": "{{count}} radio station",
|
||||
"radioStationWithCount_other": "{{count}} radio stations",
|
||||
"artist_one": "artist",
|
||||
"artist_other": "artists",
|
||||
"artistWithCount_one": "{{count}} artist",
|
||||
@@ -179,7 +211,10 @@
|
||||
"localFontAccessDenied": "access denied to local fonts",
|
||||
"loginRateError": "too many login attempts, please try again in a few seconds",
|
||||
"mpvRequired": "MPV required",
|
||||
"multipleServerSaveQueueError": "the play queue has one or more songs which are not from the current server. this is not supported",
|
||||
"networkError": "a network error occurred",
|
||||
"noNetwork": "server unavailable",
|
||||
"noNetworkDescription": "couldn't connect to this server",
|
||||
"notificationDenied": "permissions for notifications were denied. this setting has no effect",
|
||||
"openError": "could not open file",
|
||||
"playbackError": "an error occurred when trying to play the media",
|
||||
@@ -187,10 +222,12 @@
|
||||
"remoteEnableError": "an error occurred when trying to $t(common.enable) the remote server",
|
||||
"remotePortError": "an error occurred when trying to set the remote server port",
|
||||
"remotePortWarning": "restart the server to apply the new port",
|
||||
"saveQueueFailed": "failed to save queue",
|
||||
"serverNotSelectedError": "no server selected",
|
||||
"serverRequired": "server required",
|
||||
"sessionExpiredError": "your session has expired",
|
||||
"systemFontError": "an error occurred when trying to get system fonts"
|
||||
"systemFontError": "an error occurred when trying to get system fonts",
|
||||
"settingsSyncError": "discrepancies were found between the settings in the renderer and the main process. restart the application to apply the changes"
|
||||
},
|
||||
"filter": {
|
||||
"album": "$t(entity.album_one)",
|
||||
@@ -237,6 +274,33 @@
|
||||
"trackNumber": "track",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "m",
|
||||
"secondShort": "s",
|
||||
"hourShort": "h",
|
||||
"dayShort": "d"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "is after",
|
||||
"afterDate": "is after (date)",
|
||||
"before": "is before",
|
||||
"beforeDate": "is before (date)",
|
||||
"contains": "contains",
|
||||
"endsWith": "ends with",
|
||||
"inPlaylist": "is in",
|
||||
"inTheLast": "is in the last",
|
||||
"inTheRange": "is in the range",
|
||||
"inTheRangeDate": "is in the range (date)",
|
||||
"is": "is",
|
||||
"isNot": "is not",
|
||||
"isGreaterThan": "is greater than",
|
||||
"isLessThan": "is less than",
|
||||
"matchesRegex": "matches regex",
|
||||
"notContains": "does not contain",
|
||||
"notInPlaylist": "is not in",
|
||||
"notInTheLast": "is not in the last",
|
||||
"startsWith": "starts with"
|
||||
},
|
||||
"form": {
|
||||
"addServer": {
|
||||
"error_savePassword": "an error occurred when trying to save the password",
|
||||
@@ -253,6 +317,10 @@
|
||||
"success": "server added successfully",
|
||||
"title": "add server"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "add items to the queue",
|
||||
"description": "This action will add all items in the current filtered view"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"create": "create $t(entity.playlist_one) {{playlist}}",
|
||||
"input_playlists": "$t(entity.playlist_other)",
|
||||
@@ -269,6 +337,13 @@
|
||||
"success": "$t(entity.playlist_one) created successfully",
|
||||
"title": "create $t(entity.playlist_one)"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "radio station created successfully",
|
||||
"title": "create radio station",
|
||||
"input_homepageUrl": "homepage url",
|
||||
"input_name": "name",
|
||||
"input_streamUrl": "stream url"
|
||||
},
|
||||
"deletePlaylist": {
|
||||
"input_confirm": "type the name of the $t(entity.playlist_one) to confirm",
|
||||
"success": "$t(entity.playlist_one) deleted successfully",
|
||||
@@ -276,9 +351,15 @@
|
||||
},
|
||||
"editPlaylist": {
|
||||
"publicJellyfinNote": "Jellyfin for some reason does not expose whether a playlist is public or not. If you wish for this to remain public, please have the following input selected",
|
||||
"editNote": "manual edits are not recommended for large playlists. are you sure you accept the risk of data loss incurred by overwriting the existing playlist?",
|
||||
"success": "$t(entity.playlist_one) updated successfully",
|
||||
"title": "edit $t(entity.playlist_one)"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "export lyrics",
|
||||
"input_synced": "export synced lyrics",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_artist": "$t(entity.artist_one)",
|
||||
"input_name": "$t(common.name)",
|
||||
@@ -287,7 +368,14 @@
|
||||
"queryEditor": {
|
||||
"title": "query editor",
|
||||
"input_optionMatchAll": "match all",
|
||||
"input_optionMatchAny": "match any"
|
||||
"input_optionMatchAny": "match any",
|
||||
"addRuleGroup": "add rule group",
|
||||
"removeRuleGroup": "remove rule group",
|
||||
"resetToDefault": "reset to default",
|
||||
"clearFilters": "clear filters"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "saved play queue to server"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "allow downloading",
|
||||
@@ -297,6 +385,17 @@
|
||||
"expireInvalid": "expiration must be in the future",
|
||||
"createFailed": "failed to create share (is sharing enabled?)"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "play random",
|
||||
"input_genre": "$t(entity.genre_one)",
|
||||
"input_limit": "how many songs?",
|
||||
"input_minYear": "from year",
|
||||
"input_maxYear": "to year",
|
||||
"input_played": "play filter",
|
||||
"input_played_optionAll": "all tracks",
|
||||
"input_played_optionUnplayed": "only unplayed tracks",
|
||||
"input_played_optionPlayed": "only played tracks"
|
||||
},
|
||||
"updateServer": {
|
||||
"success": "server updated successfully",
|
||||
"title": "update server"
|
||||
@@ -311,6 +410,8 @@
|
||||
"albumArtistDetail": {
|
||||
"about": "About {{artist}}",
|
||||
"appearsOn": "appears on",
|
||||
"groupingTypeAll": "all release types",
|
||||
"groupingTypePrimary": "primary release types",
|
||||
"recentReleases": "recent releases",
|
||||
"viewDiscography": "view discography",
|
||||
"relatedArtists": "related $t(entity.artist_other)",
|
||||
@@ -332,8 +433,15 @@
|
||||
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
|
||||
"title": "$t(entity.album_other)"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "radio stations"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite_other)"
|
||||
},
|
||||
"appMenu": {
|
||||
"collapseSidebar": "collapse sidebar",
|
||||
"commandPalette": "open command palette",
|
||||
"expandSidebar": "expand sidebar",
|
||||
"goBack": "go back",
|
||||
"goForward": "go forward",
|
||||
@@ -343,6 +451,9 @@
|
||||
"openBrowserDevtools": "open browser devtools",
|
||||
"quit": "$t(common.quit)",
|
||||
"selectServer": "select server",
|
||||
"selectMusicFolder": "select music folder",
|
||||
"noMusicFolder": "no music folder selected",
|
||||
"multipleMusicFolders": "{{count}} music folders selected",
|
||||
"settings": "$t(common.setting_other)",
|
||||
"version": "version {{version}}"
|
||||
},
|
||||
@@ -364,6 +475,7 @@
|
||||
"deletePlaylist": "$t(action.deletePlaylist)",
|
||||
"deselectAll": "$t(action.deselectAll)",
|
||||
"download": "download",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"moveToBottom": "$t(action.moveToBottom)",
|
||||
"moveToTop": "$t(action.moveToTop)",
|
||||
@@ -376,6 +488,7 @@
|
||||
"setRating": "$t(action.setRating)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "share item",
|
||||
"goTo": "go to",
|
||||
"goToAlbum": "go to $t(entity.album_one)",
|
||||
"goToAlbumArtist": "go to $t(entity.albumArtist_one)",
|
||||
"showDetails": "get info"
|
||||
@@ -408,6 +521,9 @@
|
||||
"showTracks": "show $t(entity.genre_one) $t(entity.track_other)",
|
||||
"title": "$t(entity.genre_other)"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
"goToPage": "go to page",
|
||||
@@ -418,6 +534,7 @@
|
||||
},
|
||||
"home": {
|
||||
"explore": "explore from your library",
|
||||
"genres": "$t(entity.genre_other)",
|
||||
"mostPlayed": "most played",
|
||||
"newlyAdded": "newly added releases",
|
||||
"recentlyPlayed": "recently played",
|
||||
@@ -437,18 +554,38 @@
|
||||
},
|
||||
"setting": {
|
||||
"advanced": "advanced",
|
||||
"analytics": "analytics",
|
||||
"generalTab": "general",
|
||||
"hotkeysTab": "hotkeys",
|
||||
"playbackTab": "playback",
|
||||
"windowTab": "window"
|
||||
"windowTab": "window",
|
||||
"updates": "update",
|
||||
"cache": "cache",
|
||||
"application": "application",
|
||||
"queryBuilder": "query builder",
|
||||
"theme": "theme",
|
||||
"controls": "controls",
|
||||
"sidebar": "sidebar",
|
||||
"remote": "remote",
|
||||
"exportImport": "import/export",
|
||||
"scrobble": "scrobble",
|
||||
"audio": "audio",
|
||||
"lyrics": "lyrics",
|
||||
"lyricsDisplay": "lyrics display",
|
||||
"transcoding": "transcoding",
|
||||
"discord": "discord",
|
||||
"logger": "logger",
|
||||
"playerFilters": "player filters"
|
||||
},
|
||||
"sidebar": {
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"albums": "$t(entity.album_other)",
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"folders": "$t(entity.folder_other)",
|
||||
"genres": "$t(entity.genre_other)",
|
||||
"home": "$t(common.home)",
|
||||
"radio": "$t(entity.radioStation_other)",
|
||||
"myLibrary": "my library",
|
||||
"nowPlaying": "now playing",
|
||||
"playlists": "$t(entity.playlist_other)",
|
||||
@@ -464,9 +601,14 @@
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"addLast": "add last",
|
||||
"addNext": "add next",
|
||||
"addLast": "last",
|
||||
"addNext": "next",
|
||||
"addLastShuffled": "last (shuffled)",
|
||||
"addNextShuffled": "next (shuffled)",
|
||||
"artistRadio": "artist radio",
|
||||
"holdToShuffle": "hold to shuffle",
|
||||
"favorite": "favorite",
|
||||
"lyrics": "lyrics",
|
||||
"mute": "mute",
|
||||
"muted": "muted",
|
||||
"next": "next",
|
||||
@@ -482,22 +624,32 @@
|
||||
"queue_moveToBottom": "move selected to top",
|
||||
"queue_moveToTop": "move selected to bottom",
|
||||
"queue_remove": "remove selected",
|
||||
"queueType": "queue type",
|
||||
"queueType_default": "default",
|
||||
"queueType_priority": "priority",
|
||||
"repeat": "repeat",
|
||||
"repeat_all": "repeat all",
|
||||
"repeat_off": "repeat disabled",
|
||||
"repeat_one": "repeat one",
|
||||
"repeat_other": "",
|
||||
"shuffle": "play shuffled",
|
||||
"restoreQueueFromServer": "restore queue from server",
|
||||
"saveQueueToServer": "save queue to server",
|
||||
"shuffle": "play (shuffled)",
|
||||
"shuffle_off": "shuffle disabled",
|
||||
"skip": "skip",
|
||||
"skip_back": "skip backwards",
|
||||
"skip_forward": "skip forwards",
|
||||
"stop": "stop",
|
||||
"toggleFullscreenPlayer": "toggle fullscreen player",
|
||||
"trackRadio": "track radio",
|
||||
"unfavorite": "unfavorite",
|
||||
"pause": "pause",
|
||||
"viewQueue": "view queue"
|
||||
},
|
||||
"queryBuilder": {
|
||||
"standardTags": "standard tags",
|
||||
"customTags": "custom tags"
|
||||
},
|
||||
"releaseType": {
|
||||
"primary": {
|
||||
"album": "$t(entity.album_one)",
|
||||
@@ -522,12 +674,22 @@
|
||||
}
|
||||
},
|
||||
"setting": {
|
||||
"autoDJ": "auto DJ",
|
||||
"autoDJ_description": "automatically add similar songs to the queue",
|
||||
"autoDJ_itemCount": "item count",
|
||||
"autoDJ_itemCount_description": "the number of items attempted to be added to the queue when auto DJ is enabled",
|
||||
"autoDJ_timing": "timing",
|
||||
"autoDJ_timing_description": "the number of songs remaining in the queue before auto DJ is triggered",
|
||||
"accentColor_description": "sets the accent color for the application",
|
||||
"accentColor": "accent color",
|
||||
"useThemeAccentColor": "use theme accent color",
|
||||
"useThemeAccentColor_description": "use the primary color defined in the selected theme instead of the custom accent color",
|
||||
"albumBackground_description": "adds a background image for album pages containing the album art",
|
||||
"albumBackground": "album background image",
|
||||
"albumBackgroundBlur_description": "adjusts the amount of blur applied to the album background image",
|
||||
"albumBackgroundBlur": "album background image blur size",
|
||||
"analyticsDisable": "Opt-out of usage based analytics",
|
||||
"analyticsDisable_description": "Anonymized usage data is sent to the developer to help improve the application",
|
||||
"applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)",
|
||||
"applicationHotkeys": "application hotkeys",
|
||||
"artistBackground": "artist background image",
|
||||
@@ -590,8 +752,6 @@
|
||||
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for Jellyfin and Navidrome. {{discord}} uses a bot to fetch images, so your server must be reachable from the public internet",
|
||||
"discordUpdateInterval": "{{discord}} rich presence update interval",
|
||||
"discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)",
|
||||
"doubleClickBehavior_description": "if true, all matching tracks in a track search will be queued. otherwise, only the clicked one will be queued",
|
||||
"doubleClickBehavior": "queue all searched tracks when double clicking",
|
||||
"enableAutoTranslation_description": "enable translation automatically when lyrics are loaded",
|
||||
"enableAutoTranslation": "enable auto translation",
|
||||
"enableRemote_description": "enables the remote control server to allow other devices to control the application",
|
||||
@@ -610,8 +770,8 @@
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" is incorrect - {{reason}}",
|
||||
"externalLinks_description": "enables showing external links (Last.fm, MusicBrainz) on artist/album pages",
|
||||
"externalLinks": "show external links",
|
||||
"floatingQueueArea_description": "display a hover icon on the right side of the screen to view the play queue",
|
||||
"floatingQueueArea": "show floating queue hover area",
|
||||
"followCurrentSong_description": "automatically scroll the play queue to the current playing song",
|
||||
"followCurrentSong": "follow current song",
|
||||
"followLyric_description": "scroll the lyric to the current playing position",
|
||||
"followLyric": "follow current lyric",
|
||||
"font_description": "sets the font to use for the application",
|
||||
@@ -624,8 +784,6 @@
|
||||
"gaplessAudio_description": "sets the gapless audio setting for mpv",
|
||||
"gaplessAudio_optionWeak": "weak (recommended)",
|
||||
"gaplessAudio": "gapless audio",
|
||||
"genreBehavior_description": "determines whether clicking on a genre opens by default in track or album list",
|
||||
"genreBehavior": "genre page default behavior",
|
||||
"globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback",
|
||||
"globalMediaHotkeys": "global media hotkeys",
|
||||
"homeConfiguration_description": "configure what items are shown, and in what order, on the home page",
|
||||
@@ -680,6 +838,12 @@
|
||||
"lyricFetchProvider": "providers to fetch lyrics from",
|
||||
"lyricOffset_description": "offset the lyric by the specified amount of milliseconds",
|
||||
"lyricOffset": "lyric offset (ms)",
|
||||
"logLevel": "log level",
|
||||
"logLevel_description": "sets the minimum log level to display. debug shows all logs, error only shows errors",
|
||||
"logLevel_optionDebug": "debug",
|
||||
"logLevel_optionError": "error",
|
||||
"logLevel_optionInfo": "info",
|
||||
"logLevel_optionWarn": "warn",
|
||||
"minimizeToTray_description": "minimize the application to the system tray",
|
||||
"minimizeToTray": "minimize to tray",
|
||||
"minimumScrobblePercentage_description": "the minimum percentage of the song that must be played before it is scrobbled",
|
||||
@@ -697,6 +861,8 @@
|
||||
"notify_description": "show notifications when changing the current song",
|
||||
"passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords",
|
||||
"passwordStore": "passwords/secret store",
|
||||
"playerFilters": "Filter songs from the queue",
|
||||
"playerFilters_description": "omit songs from being added to the queue based on the following criteria",
|
||||
"playbackStyle_description": "select the playback style to use for the audio player",
|
||||
"playbackStyle_optionCrossFade": "crossfade",
|
||||
"playbackStyle_optionNormal": "normal",
|
||||
@@ -707,14 +873,42 @@
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playButtonBehavior": "play button behavior",
|
||||
"playerAlbumArtResolution_description": "the resolution for the large player's album art preview. larger makes it look more crisp, but may slow loading down. defaults to 0, meaning auto",
|
||||
"playerAlbumArtResolution": "player album art resolution",
|
||||
"artistRadioCount_description": "sets the number of songs to fetch for artist radio and track radio",
|
||||
"artistRadioCount": "artist/track radio count",
|
||||
"imageResolution": "image resolution",
|
||||
"imageResolution_description": "the resolution for the images used around the app. using a value of 0 will default to the native image resolution",
|
||||
"imageResolution_optionTable": "table",
|
||||
"imageResolution_optionItemCard": "item card",
|
||||
"imageResolution_optionSidebar": "sidebar",
|
||||
"imageResolution_optionHeader": "header",
|
||||
"imageResolution_optionFullScreenPlayer": "fullscreen player",
|
||||
"playerbarOpenDrawer_description": "allows clicking of the playerbar to open the full screen player",
|
||||
"playerbarOpenDrawer": "playerbar fullscreen toggle",
|
||||
"playerbarSlider": "playerbar slider",
|
||||
"playerbarSlider_description": "the waveform is not recommended if on a slow or metered internet connection",
|
||||
"playerbarSliderType_optionSlider": "slider",
|
||||
"playerbarSliderType_optionWaveform": "waveform",
|
||||
"playerbarWaveformAlign": "waveform align",
|
||||
"playerbarWaveformAlign_optionTop": "top",
|
||||
"playerbarWaveformAlign_optionCenter": "center",
|
||||
"playerbarWaveformAlign_optionBottom": "bottom",
|
||||
"playerbarWaveformBarWidth": "waveform bar width",
|
||||
"playerbarWaveformGap": "waveform gap",
|
||||
"playerbarWaveformRadius": "waveform radius",
|
||||
"preferLocalLyrics_description": "prefer local lyrics over remote lyrics when available",
|
||||
"preferLocalLyrics": "prefer local lyrics",
|
||||
"showLyricsInSidebar_description": "a panel will be added to the attached play queue that displays the lyrics",
|
||||
"showLyricsInSidebar": "show lyrics in player sidebar",
|
||||
"showRatings_description": "controls if the star ratings feature shows up in the interface",
|
||||
"showRatings": "show star ratings",
|
||||
"showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer",
|
||||
"showVisualizerInSidebar": "show visualizer in player sidebar",
|
||||
"combinedLyricsAndVisualizer_description": "combine lyrics and visualizer into the same panel",
|
||||
"combinedLyricsAndVisualizer": "combine lyrics and visualizer in player sidebar",
|
||||
"preservePitch_description": "preserves pitch when modifying playback speed",
|
||||
"preservePitch": "preserve pitch",
|
||||
"audioFadeOnStatusChange": "audio fade on status change",
|
||||
"audioFadeOnStatusChange_description": "enables fade out and fade in when play/pause status changes",
|
||||
"preventSleepOnPlayback_description": "prevent the display from sleeping while music is playing",
|
||||
"preventSleepOnPlayback": "prevent sleep on playback",
|
||||
"remotePassword_description": "sets the password for the remote control server. These credentials are by default transferred insecurely, so you should use a unique password that you do not care about",
|
||||
@@ -793,7 +987,12 @@
|
||||
"windowBarStyle_description": "select the style of the window bar",
|
||||
"windowBarStyle": "window bar style",
|
||||
"zoom_description": "sets the zoom percentage for the application",
|
||||
"zoom": "zoom percentage"
|
||||
"zoom": "zoom percentage",
|
||||
"queryBuilder": "query builder",
|
||||
"queryBuilderCustomFields_inputLabel": "label",
|
||||
"queryBuilderCustomFields_inputTag": "tag",
|
||||
"queryBuilderCustomFields": "custom fields",
|
||||
"queryBuilderCustomFields_description": "add custom fields to use in query builders"
|
||||
},
|
||||
"table": {
|
||||
"column": {
|
||||
@@ -802,6 +1001,7 @@
|
||||
"albumCount": "$t(entity.album_other)",
|
||||
"artist": "$t(entity.artist_one)",
|
||||
"biography": "biography",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"bitrate": "bitrate",
|
||||
"bpm": "bpm",
|
||||
"channels": "$t(common.channel_other)",
|
||||
@@ -817,28 +1017,53 @@
|
||||
"rating": "rating",
|
||||
"releaseDate": "release date",
|
||||
"releaseYear": "year",
|
||||
"sampleRate": "$t(common.sampleRate)",
|
||||
"size": "$t(common.size)",
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"title": "title",
|
||||
"trackNumber": "track"
|
||||
"trackNumber": "track",
|
||||
"owner": "owner"
|
||||
},
|
||||
"config": {
|
||||
"general": {
|
||||
"advancedSettings": "advanced settings",
|
||||
"autoFitColumns": "auto fit columns",
|
||||
"autosize": "autosize",
|
||||
"moveUp": "move up",
|
||||
"moveDown": "move down",
|
||||
"pinToLeft": "pin to left",
|
||||
"pinToRight": "pin to right",
|
||||
"alignLeft": "align left",
|
||||
"alignCenter": "align center",
|
||||
"alignRight": "align right",
|
||||
"followCurrentSong": "follow current song",
|
||||
"displayType": "display type",
|
||||
"gap": "$t(common.gap)",
|
||||
"itemGap": "item gap (px)",
|
||||
"itemSize": "item size (px)",
|
||||
"itemsPerRow": "items per row",
|
||||
"size": "$t(common.size)",
|
||||
"tableColumns": "table columns"
|
||||
"size_default": "default",
|
||||
"size_compact": "compact",
|
||||
"size_large": "large",
|
||||
"tableColumns": "table columns",
|
||||
"pagination": "pagination",
|
||||
"pagination_itemsPerPage": "items per page",
|
||||
"pagination_infinite": "infinite",
|
||||
"pagination_paginate": "paginated",
|
||||
"alternateRowColors": "alternate row colors",
|
||||
"horizontalBorders": "row borders",
|
||||
"rowHoverHighlight": "row hover highlight",
|
||||
"verticalBorders": "column borders"
|
||||
},
|
||||
"label": {
|
||||
"actions": "$t(common.action_other)",
|
||||
"album": "$t(entity.album_one)",
|
||||
"albumCount": "$t(entity.album_other)",
|
||||
"albumArtist": "$t(entity.albumArtist_one)",
|
||||
"artist": "$t(entity.artist_one)",
|
||||
"biography": "$t(common.biography)",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"bitrate": "$t(common.bitrate)",
|
||||
"bpm": "$t(common.bpm)",
|
||||
"channels": "$t(common.channel_other)",
|
||||
@@ -848,6 +1073,8 @@
|
||||
"duration": "$t(common.duration)",
|
||||
"favorite": "$t(common.favorite)",
|
||||
"genre": "$t(entity.genre_one)",
|
||||
"genreBadge": "$t(entity.genre_one) (badges)",
|
||||
"image": "image",
|
||||
"lastPlayed": "last played",
|
||||
"note": "$t(common.note)",
|
||||
"owner": "$t(common.owner)",
|
||||
@@ -856,6 +1083,7 @@
|
||||
"rating": "$t(common.rating)",
|
||||
"releaseDate": "release date",
|
||||
"rowIndex": "row index",
|
||||
"sampleRate": "$t(common.sampleRate)",
|
||||
"size": "$t(common.size)",
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"title": "$t(common.title)",
|
||||
@@ -864,10 +1092,8 @@
|
||||
"year": "$t(common.year)"
|
||||
},
|
||||
"view": {
|
||||
"card": "card",
|
||||
"grid": "grid",
|
||||
"list": "list",
|
||||
"poster": "poster",
|
||||
"table": "table"
|
||||
}
|
||||
}
|
||||
@@ -876,5 +1102,151 @@
|
||||
"error_oneFileOnly": "Please only select 1 file",
|
||||
"error_readingFile": "there has been an issue reading the file: {{errorMessage}}",
|
||||
"mainText": "drop a file here"
|
||||
},
|
||||
"visualizer": {
|
||||
"visualizerType": "Visualizer Type",
|
||||
"cyclePresets": "Cycle Presets",
|
||||
"cycleTime": "Cycle Time (seconds)",
|
||||
"includeAllPresets": "Include All Presets",
|
||||
"ignoredPresets": "Ignored Presets",
|
||||
"selectedPresets": "Selected Presets",
|
||||
"randomizeNextPreset": "Randomize Next Preset",
|
||||
"blendTime": "Blend Time",
|
||||
"presets": "Presets",
|
||||
"selectPreset": "Select Preset",
|
||||
"applyPreset": "Apply Preset",
|
||||
"saveAsPreset": "Save as Preset",
|
||||
"updatePreset": "Update Preset",
|
||||
"copyConfiguration": "Copy Configuration",
|
||||
"pasteConfiguration": "Paste Configuration",
|
||||
"pasteConfigurationPlaceholder": "Paste JSON configuration here...",
|
||||
"pasteFromClipboard": "Paste from Clipboard",
|
||||
"applyConfiguration": "Apply Configuration",
|
||||
"configCopied": "Configuration copied to clipboard",
|
||||
"configCopyFailed": "Failed to copy configuration",
|
||||
"configPasted": "Configuration applied successfully",
|
||||
"configPasteFailed": "Failed to apply configuration. Please check the format.",
|
||||
"configPasteReadFailed": "Failed to read from clipboard",
|
||||
"presetName": "Preset Name",
|
||||
"presetNamePlaceholder": "Enter preset name",
|
||||
"general": "General",
|
||||
"mode": "Mode",
|
||||
"mode1To8": "Mode 1 - 8",
|
||||
"mode10": "Mode 10",
|
||||
"barSpace": "Bar Space",
|
||||
"lineWidth": "Line Width",
|
||||
"fillAlpha": "Fill Alpha",
|
||||
"channelLayout": "Channel Layout",
|
||||
"maxFPS": "Max FPS",
|
||||
"opacity": "Opacity",
|
||||
"customGradients": "Custom Gradients",
|
||||
"addCustomGradient": "Add Custom Gradient",
|
||||
"gradientName": "Gradient Name",
|
||||
"gradientNamePlaceholder": "Gradient Name",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"colorStops": "Color Stops",
|
||||
"addColor": "Add Color",
|
||||
"position": "Position",
|
||||
"level": "Level",
|
||||
"remove": "Remove",
|
||||
"custom": "Custom",
|
||||
"builtIn": "Built-in",
|
||||
"colors": "Colors",
|
||||
"colorMode": "Color Mode",
|
||||
"gradient": "Gradient",
|
||||
"gradientLeft": "Gradient Left",
|
||||
"gradientRight": "Gradient Right",
|
||||
"fft": "FFT",
|
||||
"fftSize": "FFT Size",
|
||||
"smoothing": "Smoothing",
|
||||
"frequencyRangeAndScaling": "Frequency range and scaling",
|
||||
"minimumFrequency": "Minimum Frequency",
|
||||
"maximumFrequency": "Maximum Frequency",
|
||||
"frequencyScale": "Frequency Scale",
|
||||
"sensitivity": "Sensitivity",
|
||||
"weightingFilter": "Weighting Filter",
|
||||
"minimumDecibels": "Minimum Decibels",
|
||||
"maximumDecibels": "Maximum Decibels",
|
||||
"linearAmplitude": "Linear Amplitude",
|
||||
"linearBoost": "Linear Boost",
|
||||
"peakBehavior": "Peak Behavior",
|
||||
"showPeaks": "Show Peaks",
|
||||
"fadePeaks": "Fade Peaks",
|
||||
"peakLine": "Peak Line",
|
||||
"gravity": "Gravity",
|
||||
"peakFadeTime": "Peak Fade Time (ms)",
|
||||
"peakHoldTime": "Peak Hold Time (ms)",
|
||||
"radialSpectrum": "Radial Spectrum",
|
||||
"radial": "Radial",
|
||||
"radialInvert": "Radial Invert",
|
||||
"spinSpeed": "Spin Speed",
|
||||
"radius": "Radius",
|
||||
"reflexMirror": "Reflex Mirror",
|
||||
"reflexFit": "Reflex Fit",
|
||||
"reflexRatio": "Reflex Ratio",
|
||||
"reflexAlpha": "Reflex Alpha",
|
||||
"reflexBrightness": "Reflex Brightness",
|
||||
"mirror": "Mirror",
|
||||
"miscellaneousSettings": "Miscellaneous Settings",
|
||||
"alphaBars": "Alpha Bars",
|
||||
"ansiBands": "ANSI Bands",
|
||||
"ledBars": "LED Bars",
|
||||
"trueLeds": "True LEDs",
|
||||
"lumiBars": "Lumi Bars",
|
||||
"outlineBars": "Outline Bars",
|
||||
"roundBars": "Round Bars",
|
||||
"lowResolution": "Low Resolution",
|
||||
"splitGradient": "Split Gradient",
|
||||
"showFPS": "Show FPS",
|
||||
"showScaleX": "Show Scale X",
|
||||
"noteLabels": "Note Labels",
|
||||
"showScaleY": "Show Scale Y",
|
||||
"options": {
|
||||
"mode": {
|
||||
"bars": "[0] Bars",
|
||||
"circle": "[1] Circle",
|
||||
"wave": "[2] Wave",
|
||||
"rainbow": "[3] Rainbow",
|
||||
"rings": "[4] Rings",
|
||||
"mirror": "[5] Mirror",
|
||||
"line": "[6] Line",
|
||||
"particles": "[7] Particles",
|
||||
"fullOctave": "[8] Full octave / 10 bands",
|
||||
"outlineBars": "[10] Outline bars"
|
||||
},
|
||||
"colorMode": {
|
||||
"gradient": "Gradient",
|
||||
"barIndex": "Bar-Index",
|
||||
"barLevel": "Bar-Level"
|
||||
},
|
||||
"gradient": {
|
||||
"classic": "Classic",
|
||||
"prism": "Prism",
|
||||
"rainbow": "Rainbow",
|
||||
"steelblue": "Steelblue",
|
||||
"orangered": "Orangered"
|
||||
},
|
||||
"channelLayout": {
|
||||
"single": "Single",
|
||||
"dualCombined": "Dual-Combined",
|
||||
"dualHorizontal": "Dual-Horizontal",
|
||||
"dualVertical": "Dual-Vertical"
|
||||
},
|
||||
"frequencyScale": {
|
||||
"bark": "Bark",
|
||||
"linear": "Linear",
|
||||
"log": "Log",
|
||||
"mel": "Mel"
|
||||
},
|
||||
"weightingFilter": {
|
||||
"none": "None",
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "C",
|
||||
"d": "D",
|
||||
"z": "Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+246
-29
@@ -11,10 +11,10 @@
|
||||
"skip_back": "retroceder",
|
||||
"favorite": "favorito",
|
||||
"next": "siguiente",
|
||||
"shuffle": "Reproducir aleatoriamente",
|
||||
"shuffle": "Reproducir (mezclado)",
|
||||
"playbackFetchNoResults": "ninguna canción encontrada",
|
||||
"playbackFetchInProgress": "cargando canciones…",
|
||||
"addNext": "añadir siguiente",
|
||||
"addNext": "Siguiente",
|
||||
"playbackSpeed": "velocidad de reproducción",
|
||||
"playbackFetchCancel": "esto está tomando un tiempo... cierra la notificación para cancelar",
|
||||
"play": "reproducir",
|
||||
@@ -25,12 +25,23 @@
|
||||
"queue_moveToTop": "mover seleccionado al final",
|
||||
"queue_moveToBottom": "mover seleccionado al principio",
|
||||
"shuffle_off": "mezclar desactivado",
|
||||
"addLast": "añadir último",
|
||||
"addLast": "Al final",
|
||||
"mute": "silencio",
|
||||
"skip_forward": "saltar hacia delante",
|
||||
"pause": "pausa",
|
||||
"playSimilarSongs": "Reproducir canciones similares",
|
||||
"viewQueue": "ver cola"
|
||||
"viewQueue": "ver cola",
|
||||
"addLastShuffled": "Al final (mezclado)",
|
||||
"addNextShuffled": "Al siguiente (mezclado)",
|
||||
"queueType": "Tipo de cola",
|
||||
"queueType_default": "Predeterminado",
|
||||
"queueType_priority": "Prioridad",
|
||||
"holdToShuffle": "Mantener para mezclar",
|
||||
"lyrics": "Letras",
|
||||
"restoreQueueFromServer": "Restaurar cola del servidor",
|
||||
"saveQueueToServer": "Guardar cola en el servidor",
|
||||
"artistRadio": "Radio de artista",
|
||||
"trackRadio": "Radio de pista"
|
||||
},
|
||||
"setting": {
|
||||
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
|
||||
@@ -135,7 +146,6 @@
|
||||
"sidePlayQueueStyle_description": "establece el estilo de la cola de reproducción lateral",
|
||||
"replayGainMode": "modo de {{ReplayGain}}",
|
||||
"playbackStyle_optionNormal": "normal",
|
||||
"floatingQueueArea": "mostrar área flotante de cola",
|
||||
"replayGainFallback_description": "ganancia en db a aplicar si el archivo no tiene etiquetas de {{ReplayGain}}",
|
||||
"replayGainPreamp_description": "ajusta la ganancia del preamplificador aplicada a los valores de {{ReplayGain}}",
|
||||
"hotkey_toggleRepeat": "alterna repetir",
|
||||
@@ -161,7 +171,6 @@
|
||||
"hotkey_rate0": "Limpiar calificación",
|
||||
"discordApplicationId": "id de aplicación {{discord}}",
|
||||
"applicationHotkeys_description": "configura las teclas de acceso rápido de la aplicación. marca la casilla para establecerlas como teclas de acceso rápido globales (solo escritorio)",
|
||||
"floatingQueueArea_description": "muestra un icono flotante en el lado derecho de la pantalla para ver la cola de reproducción",
|
||||
"hotkey_volumeMute": "silenciar volumen",
|
||||
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) cambia a favorita",
|
||||
"remoteUsername": "nombre de usuario del control remoto del servidor",
|
||||
@@ -199,13 +208,9 @@
|
||||
"startMinimized_description": "inicia la aplicación en la bandeja del sistema",
|
||||
"startMinimized": "iniciar minimizado",
|
||||
"passwordStore": "contraseñas/almacenamiento secreto",
|
||||
"playerAlbumArtResolution_description": "la resolución para la vista previa de la carátula del álbum del reproductor grande. más grande hace que parezca más nítido, pero puede ralentizar la carga. El valor predeterminado es 0, lo que significa automático",
|
||||
"playerAlbumArtResolution": "resolución de la carátula del álbum del reproductor",
|
||||
"homeConfiguration": "Configuración de la página de inicio",
|
||||
"mpvExtraParameters_help": "Uno por línea",
|
||||
"genreBehavior": "Comportamiento predeterminado de la página de géneros",
|
||||
"externalLinks_description": "Permite mostrar enlaces externos (Last.fm, MusicBrainz) en las páginas del artista/álbum",
|
||||
"genreBehavior_description": "Determina si al hacer clic en un género se abre por defecto la lista de pistas o de álbumes",
|
||||
"homeConfiguration_description": "Configura qué elementos son mostrados y en qué orden en la página de inicio",
|
||||
"clearCacheSuccess": "Caché limpiada correctamente",
|
||||
"externalLinks": "Mostrar enlaces externos",
|
||||
@@ -213,8 +218,6 @@
|
||||
"homeFeature_description": "Controla si se muestra el gran carrusel destacado en la página de inicio",
|
||||
"imageAspectRatio_description": "Si está habilitado, la portada será mostrada usando su relación de aspecto nativa. Para arte que no es 1:1, el espacio restante estará vacío",
|
||||
"imageAspectRatio": "Usar relación de aspecto nativa de portada",
|
||||
"doubleClickBehavior": "poner en cola todas las pistas buscadas al hacer doble clic",
|
||||
"doubleClickBehavior_description": "si está activado, se pondrán en cola todas las pistas que coincidan en una búsqueda de pistas. De lo contrario, solo se pondrán en cola las pistas seleccionadas",
|
||||
"volumeWidth": "Ancho del deslizador de volumen",
|
||||
"volumeWidth_description": "La anchura del deslizador de volumen",
|
||||
"discordListening_description": "muestra el estado como Escuchando en lugar de Jugando a",
|
||||
@@ -304,7 +307,53 @@
|
||||
"language": "Idioma",
|
||||
"notify": "Activar notificaciones de canciones",
|
||||
"notify_description": "Muestra notificaciones cuando se cambia la canción actual",
|
||||
"transcode": "Activar transcodificación"
|
||||
"transcode": "Activar transcodificación",
|
||||
"analyticsDisable": "Exclusión voluntaria de analíticas basadas en el uso",
|
||||
"analyticsDisable_description": "Se envía el uso de datos anónimos al desarrollador para ayudar a mejorar la aplicación",
|
||||
"playerbarSlider": "Barra de reproducción deslizante",
|
||||
"playerbarSliderType_optionWaveform": "Forma de onda",
|
||||
"playerbarWaveformAlign": "Alineación de la forma de onda",
|
||||
"playerbarSliderType_optionSlider": "Deslizador",
|
||||
"playerbarWaveformAlign_optionTop": "Superior",
|
||||
"playerbarWaveformAlign_optionCenter": "Centrado",
|
||||
"playerbarWaveformAlign_optionBottom": "Inferior",
|
||||
"playerbarWaveformBarWidth": "Ancho de barra de la forma de onda",
|
||||
"playerbarWaveformGap": "Brecha de la forma de onda",
|
||||
"playerbarWaveformRadius": "Radio de la forma de onda",
|
||||
"showLyricsInSidebar_description": "Se añadirá un panel a la cola de reproducción acoplada que muestra las letras",
|
||||
"showLyricsInSidebar": "Mostrar letras en la barra lateral del reproductor",
|
||||
"showVisualizerInSidebar_description": "Se añadirá un panel a la barra lateral de reproducción que muestra el visualizador",
|
||||
"showVisualizerInSidebar": "Mostrar visualizador en la barra lateral de reproducción",
|
||||
"queryBuilder": "Generador de consultas",
|
||||
"queryBuilderCustomFields_inputTag": "Etiqueta",
|
||||
"queryBuilderCustomFields": "Campos personalizados",
|
||||
"queryBuilderCustomFields_description": "Añade campos personalizados a usar en los generadores de consultas",
|
||||
"queryBuilderCustomFields_inputLabel": "Rótulo",
|
||||
"audioFadeOnStatusChange": "Fundido del audio al cambiar de estado",
|
||||
"audioFadeOnStatusChange_description": "Activa el fundido de salida y el de entrada cuando cambia el estado al reproducir/pausar",
|
||||
"followCurrentSong_description": "Desplaza automáticamente la cola de reproducción a la canción en reproducción actual",
|
||||
"followCurrentSong": "Seguir la canción actual",
|
||||
"playerFilters": "Filtrar las canciones de la cola",
|
||||
"playerFilters_description": "Omite la adición de canciones a la cola basado en los siguientes criterios",
|
||||
"playerbarSlider_description": "La forma de onda no es recomendable en una conexión a Internet lenta o medida",
|
||||
"autoDJ": "DJ automático",
|
||||
"autoDJ_description": "Añade canciones similares a las de la cola automáticamente",
|
||||
"autoDJ_itemCount": "Recuento de elementos",
|
||||
"autoDJ_itemCount_description": "El número de elementos que se ha intentado añadir a la cola cuando DJ automático está activado",
|
||||
"autoDJ_timing_description": "El número de canciones restantes en la cola antes de que DJ automático se dispare",
|
||||
"autoDJ_timing": "Tiempo",
|
||||
"logLevel": "Nivel de registro",
|
||||
"logLevel_description": "Establece el mínimo nivel de registro a mostrar. Depuración muestra todos los registros, error solo muestra errores",
|
||||
"logLevel_optionDebug": "Depuración",
|
||||
"logLevel_optionError": "Error",
|
||||
"logLevel_optionInfo": "Información",
|
||||
"logLevel_optionWarn": "Advertencia",
|
||||
"useThemeAccentColor": "Usar color de acentuación de tema",
|
||||
"useThemeAccentColor_description": "Usa el color principal definido en el tema seleccionado en lugar del color de acentuación personalizado",
|
||||
"artistRadioCount_description": "Establece el número de canciones a buscar para la radio de artista y de pista",
|
||||
"artistRadioCount": "Recuento de radio de artista/pista",
|
||||
"imageResolution": "Resolución de imagen",
|
||||
"imageResolution_description": "La resolución de las imágenes usadas en la aplicación. Usar un valor de 0 lo dejará de forma predeterminada a la resolución nativa de la imagen"
|
||||
},
|
||||
"action": {
|
||||
"editPlaylist": "editar $t(entity.playlist_one)",
|
||||
@@ -328,7 +377,23 @@
|
||||
"lastfm": "Abrir en Last.fm",
|
||||
"musicbrainz": "Abrir en MusicBrainz"
|
||||
},
|
||||
"moveToNext": "pasar al siguiente"
|
||||
"moveToNext": "pasar al siguiente",
|
||||
"downloadStarted": "Iniciada descarga de {{count}} elementos",
|
||||
"moveItems": "Mover elementos",
|
||||
"shuffle": "Mezclar",
|
||||
"shuffleAll": "Mezclar todo",
|
||||
"shuffleSelected": "Mezclar seleccionados",
|
||||
"viewMore": "Ver más",
|
||||
"holdToMoveToBottom": "Mantener pulsado para desplazar hacia abajo",
|
||||
"holdToMoveToTop": "Mantener pulsado para desplazar hacia arriba",
|
||||
"moveUp": "Desplazar hacia arriba",
|
||||
"moveDown": "Desplazar hacia abajo",
|
||||
"createRadioStation": "Crear $t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "Borrar $t(entity.radioStation_one)",
|
||||
"openApplicationDirectory": "Abrir directorio de la aplicación",
|
||||
"addOrRemoveFromSelection": "Añadir o quitar de la selección",
|
||||
"selectRangeOfItems": "Seleccionar un intervalo de elementos",
|
||||
"selectAll": "Seleccionar todo"
|
||||
},
|
||||
"common": {
|
||||
"backward": "hacia atrás",
|
||||
@@ -435,7 +500,19 @@
|
||||
"private": "Privado",
|
||||
"public": "Público",
|
||||
"recordLabel": "Sello discográfico",
|
||||
"releaseType": "Tipo de lanzamiento"
|
||||
"releaseType": "Tipo de lanzamiento",
|
||||
"doNotShowAgain": "No mostrar esto de nuevo",
|
||||
"externalLinks": "Enlaces externos",
|
||||
"faster": "Más rápido",
|
||||
"slower": "Más lento",
|
||||
"sort": "Ordenar",
|
||||
"gridRows": "Filas de la cuadrícula",
|
||||
"tableColumns": "Columnas de la tabla",
|
||||
"itemsMore": "{{count}} más",
|
||||
"noFilters": "Ningún filtro configurado",
|
||||
"view": "Vista",
|
||||
"countSelected": "{{count}} seleccionados",
|
||||
"retry": "Reintentar"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
|
||||
@@ -461,7 +538,12 @@
|
||||
"networkError": "Ocurrió un error de red",
|
||||
"openError": "No se pudo abrir el archivo",
|
||||
"badValue": "Opción inválida \"{{value}}\". Este valor ya no existe",
|
||||
"notificationDenied": "Se denegaron los permisos para notificaciones. Esta configuración no tiene efecto"
|
||||
"notificationDenied": "Se denegaron los permisos para notificaciones. Esta configuración no tiene efecto",
|
||||
"saveQueueFailed": "Error al guardar la cola",
|
||||
"multipleServerSaveQueueError": "La cola de reproducción tiene una o más canciones que no son del servidor actual. Esto no está soportado",
|
||||
"settingsSyncError": "Se encontraron discrepancias entre las opciones del renderizador y el proceso principal. Reinicia la aplicación para aplicar los cambios",
|
||||
"noNetwork": "Servidor no disponible",
|
||||
"noNetworkDescription": "No se pudo conectar a este servidor"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "más reproducido",
|
||||
@@ -522,7 +604,9 @@
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "compartido $t(entity.playlist_other)",
|
||||
"myLibrary": "Mi biblioteca"
|
||||
"myLibrary": "Mi biblioteca",
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"appMenu": {
|
||||
"selectServer": "seleccionar servidor",
|
||||
@@ -536,7 +620,11 @@
|
||||
"goBack": "retroceder",
|
||||
"goForward": "avanzar",
|
||||
"privateModeOff": "Desactivar modo privado",
|
||||
"privateModeOn": "Activar modo privado"
|
||||
"privateModeOn": "Activar modo privado",
|
||||
"selectMusicFolder": "Seleccionar carpeta de música",
|
||||
"noMusicFolder": "Ninguna carpeta de música seleccionada",
|
||||
"multipleMusicFolders": "{{count}} carpetas de música seleccionadas",
|
||||
"commandPalette": "Abrir paleta de comandos"
|
||||
},
|
||||
"contextMenu": {
|
||||
"addToPlaylist": "$t(action.addToPlaylist)",
|
||||
@@ -562,7 +650,9 @@
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"goToAlbum": "Ir a $t(entity.album_one)",
|
||||
"goToAlbumArtist": "Ir a $t(entity.albumArtist_one)"
|
||||
"goToAlbumArtist": "Ir a $t(entity.albumArtist_one)",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"goTo": "Ir a"
|
||||
},
|
||||
"home": {
|
||||
"mostPlayed": "más reproducidos",
|
||||
@@ -570,7 +660,8 @@
|
||||
"title": "$t(common.home)",
|
||||
"explore": "explora desde tu biblioteca",
|
||||
"recentlyPlayed": "reproducidos recientemente",
|
||||
"recentlyReleased": "Lanzado recientemente"
|
||||
"recentlyReleased": "Lanzado recientemente",
|
||||
"genres": "$t(entity.genre_other)"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"upNext": "siguiente",
|
||||
@@ -605,7 +696,25 @@
|
||||
"generalTab": "general",
|
||||
"hotkeysTab": "teclas de acceso rápido",
|
||||
"windowTab": "ventana",
|
||||
"advanced": "Avanzado"
|
||||
"advanced": "Avanzado",
|
||||
"analytics": "Analíticas",
|
||||
"updates": "Actualización",
|
||||
"cache": "Caché",
|
||||
"application": "Aplicación",
|
||||
"queryBuilder": "Generador de consultas",
|
||||
"theme": "Tema",
|
||||
"controls": "Controles",
|
||||
"remote": "Remoto",
|
||||
"exportImport": "Importar/Exportar",
|
||||
"scrobble": "Scrobble",
|
||||
"audio": "Audio",
|
||||
"lyrics": "Letras",
|
||||
"transcoding": "Transcodificación",
|
||||
"discord": "Discord",
|
||||
"sidebar": "Barra lateral",
|
||||
"playerFilters": "Filtros del reproductor",
|
||||
"logger": "Registrador",
|
||||
"lyricsDisplay": "Mostrar letras"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
@@ -662,6 +771,15 @@
|
||||
"username": "nombre de usuario",
|
||||
"editServerDetailsTooltip": "editar detalles del servidor",
|
||||
"url": "URL"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite_other)"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "Estaciones de radio"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -713,12 +831,17 @@
|
||||
"editPlaylist": {
|
||||
"title": "editar $t(entity.playlist_one)",
|
||||
"success": "$t(entity.playlist_one) actualizada correctamente",
|
||||
"publicJellyfinNote": "Jellyfin por alguna razón no expone si una lista de reproducción es pública o no. Si deseas que ésta siga siendo pública, por favor ten seleccionada la siguiente entrada"
|
||||
"publicJellyfinNote": "Jellyfin por alguna razón no expone si una lista de reproducción es pública o no. Si deseas que ésta siga siendo pública, por favor ten seleccionada la siguiente entrada",
|
||||
"editNote": "No se recomiendan las ediciones manuales para grandes listas de reproducción. ¿Seguro que aceptas el riesgo de pérdida de información incurrido por sobrescribir la lista de reproducción existente?"
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "coincidir todos",
|
||||
"input_optionMatchAny": "coincidir cualquiera",
|
||||
"title": "Editor de consultas"
|
||||
"title": "Editor de consultas",
|
||||
"addRuleGroup": "Añadir regla de grupo",
|
||||
"removeRuleGroup": "Eliminar regla de grupo",
|
||||
"resetToDefault": "Restablecer al valor predeterminado",
|
||||
"clearFilters": "Limpiar filtros"
|
||||
},
|
||||
"shareItem": {
|
||||
"createFailed": "No se pudo crear el recurso compartido (¿está habilitado el uso compartido?)",
|
||||
@@ -732,6 +855,36 @@
|
||||
"enabled": "Modo privado activado, el estado de reproducción ahora está oculto de integraciones externas",
|
||||
"disabled": "Modo privado desactivado, el estado de reproducción ahora es visible a las integraciones externas habilitadas",
|
||||
"title": "Modo privado"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "Añadir elementos a la cola",
|
||||
"description": "Esta acción agregará todos los elementos en la vista filtrada actual"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "Reproducir aleatorio",
|
||||
"input_genre": "$t(entity.genre_one)",
|
||||
"input_limit": "¿Cuántas canciones?",
|
||||
"input_minYear": "Del año",
|
||||
"input_maxYear": "Hasta el año",
|
||||
"input_played": "Reproducir filtro",
|
||||
"input_played_optionAll": "Todas las pistas",
|
||||
"input_played_optionUnplayed": "Solo las pistas sin reproducir",
|
||||
"input_played_optionPlayed": "Solo las pistas reproducidas"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "Cola de reproducción guardada en el servidor"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "Estación de radio creada con éxito",
|
||||
"title": "Crear estación de radio",
|
||||
"input_homepageUrl": "URL de la página de inicio",
|
||||
"input_name": "Nombre",
|
||||
"input_streamUrl": "URL de la transmisión"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "Exportar letras",
|
||||
"input_synced": "Exportar letras sincronizadas",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -759,7 +912,10 @@
|
||||
"discNumber": "disco",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"size": "$t(common.size)",
|
||||
"codec": "$t(common.codec)"
|
||||
"codec": "$t(common.codec)",
|
||||
"owner": "Propietario",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
},
|
||||
"config": {
|
||||
"label": {
|
||||
@@ -790,7 +946,12 @@
|
||||
"favorite": "$t(common.favorite)",
|
||||
"year": "$t(common.year)",
|
||||
"codec": "$t(common.codec)",
|
||||
"songCount": "$t(entity.track_other)"
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"albumCount": "$t(entity.album_other)",
|
||||
"genreBadge": "$t(entity.genre_one) (insignias)",
|
||||
"image": "Imagen",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
},
|
||||
"general": {
|
||||
"gap": "$t(common.gap)",
|
||||
@@ -800,12 +961,31 @@
|
||||
"displayType": "tipo de visualización",
|
||||
"itemGap": "espacio entre elementos (px)",
|
||||
"itemSize": "tamaño del elemento (px)",
|
||||
"followCurrentSong": "seguir la canción actual"
|
||||
"followCurrentSong": "seguir la canción actual",
|
||||
"advancedSettings": "Opciones avanzadas",
|
||||
"autosize": "Autodimensionar",
|
||||
"moveUp": "Ascender",
|
||||
"moveDown": "Descender",
|
||||
"pinToLeft": "Anclar a la izquierda",
|
||||
"pinToRight": "Anclar a la derecha",
|
||||
"alignLeft": "Alinear a la izquierda",
|
||||
"alignCenter": "Alinear al centro",
|
||||
"alignRight": "Alinear a la derecha",
|
||||
"itemsPerRow": "Elementos por fila",
|
||||
"size_default": "Predeterminado",
|
||||
"size_compact": "Compacto",
|
||||
"size_large": "Grande",
|
||||
"pagination": "Paginación",
|
||||
"pagination_itemsPerPage": "Elementos por página",
|
||||
"pagination_infinite": "Infinita",
|
||||
"pagination_paginate": "Paginada",
|
||||
"alternateRowColors": "Colores de fila alternativos",
|
||||
"horizontalBorders": "Bordes de fila",
|
||||
"verticalBorders": "Bordes de columna",
|
||||
"rowHoverHighlight": "Resaltar al pasar el cursor por la fila"
|
||||
},
|
||||
"view": {
|
||||
"card": "tarjeta",
|
||||
"table": "tabla",
|
||||
"poster": "cartel",
|
||||
"list": "Lista",
|
||||
"grid": "Cuadrícula"
|
||||
}
|
||||
@@ -863,7 +1043,13 @@
|
||||
"play_other": "{{count}} reproducciones",
|
||||
"song_one": "canción",
|
||||
"song_many": "canciones",
|
||||
"song_other": "canciones"
|
||||
"song_other": "canciones",
|
||||
"radioStation_one": "Estación de radio",
|
||||
"radioStation_many": "Estaciones de radio",
|
||||
"radioStation_other": "Estaciones de radio",
|
||||
"radioStationWithCount_one": "{{count}} estación de radio",
|
||||
"radioStationWithCount_many": "{{count}} estaciones de radio",
|
||||
"radioStationWithCount_other": "{{count}} estaciones de radio"
|
||||
},
|
||||
"dragDropZone": {
|
||||
"error_oneFileOnly": "Por favor selecciona un solo archivo",
|
||||
@@ -892,5 +1078,36 @@
|
||||
"spokenWord": "Palabra hablada",
|
||||
"demo": "Maqueta"
|
||||
}
|
||||
},
|
||||
"queryBuilder": {
|
||||
"standardTags": "Etiquetas estándar",
|
||||
"customTags": "Etiquetas personalizadas"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "es después",
|
||||
"afterDate": "es después (fecha)",
|
||||
"before": "es antes",
|
||||
"beforeDate": "es antes (fecha)",
|
||||
"contains": "contiene",
|
||||
"endsWith": "termina con",
|
||||
"inPlaylist": "está en",
|
||||
"inTheLast": "está en el último",
|
||||
"inTheRange": "está en el rango",
|
||||
"inTheRangeDate": "está en el rango (fecha)",
|
||||
"is": "es",
|
||||
"isNot": "no es",
|
||||
"isGreaterThan": "es mayor que",
|
||||
"isLessThan": "es menor que",
|
||||
"notContains": "no contiene",
|
||||
"notInPlaylist": "no está en",
|
||||
"notInTheLast": "no está en el último",
|
||||
"startsWith": "empieza con",
|
||||
"matchesRegex": "coincide con expresión regular"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "min",
|
||||
"secondShort": "seg",
|
||||
"hourShort": "h",
|
||||
"dayShort": "día"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,9 +164,7 @@
|
||||
"view": {
|
||||
"table": "taula",
|
||||
"list": "zerrenda",
|
||||
"card": "txartela",
|
||||
"grid": "sareta",
|
||||
"poster": "kartela"
|
||||
"grid": "sareta"
|
||||
},
|
||||
"general": {
|
||||
"gap": "$t(common.gap)",
|
||||
@@ -448,7 +446,6 @@
|
||||
"discordDisplayType_description": "zure egoeran entzuten ari zarena aldatzen du",
|
||||
"discordLinkType": "{{discord}} egoera estekak",
|
||||
"fontType_description": "barneko letra-tipoa feishinek eskaintzen dituen letra-tipoetako bat aukeratzen du. sistemaren letra-tipoa zure sistema eragileak eskaintzen duen edozein letra-tipo hautatzeko aukera ematen dizu. pertsonalizatua zure letra-tipoa eskaintzeko aukera ematen dizu",
|
||||
"genreBehavior": "genero orriaren portaera lehenetsia",
|
||||
"homeConfiguration_description": "konfiguratu zein elementu erakusten diren hasierako orrian eta zein ordenatan",
|
||||
"homeFeature": "etxeko karrusela nabarmendua",
|
||||
"homeFeature_description": "hasierako orrian karrusel nabarmen handia erakutsi behar den ala ez kontrolatzen du",
|
||||
@@ -496,7 +493,6 @@
|
||||
"accentColor": "azentu-kolorea",
|
||||
"clearCache_description": "feishinen 'garbiketa gogorra'. feishinen katxea garbitzeaz gain, hustu nabigatzailearen katxea (gordetako irudiak eta bestelako aktiboak). zerbitzari-kredentzialak eta ezarpenak gorde egiten dira",
|
||||
"clearQueryCache_description": "feishinen 'garbiketa ahula'. honek erreprodukzio-zerrendak eta pisten metadatuak freskatuko ditu eta gordetako letrak berrezarriko ditu. ezarpenak, zerbitzari-kredentzialak eta katxetutako irudiak gorde egiten dira",
|
||||
"doubleClickBehavior": "jarri ilaran bilatutako pista guztiak klik bikoitza egitean",
|
||||
"exitToTray_description": "irten aplikaziotik sistemaren erretilura",
|
||||
"followLyric_description": "mugitu letra uneko erreprodukzio-posiziora",
|
||||
"preferLocalLyrics": "nahiago izan letra lokalak",
|
||||
@@ -523,10 +519,8 @@
|
||||
"playbackStyle_description": "aukeratu audio erreproduzitzailearentzat erabiliko den erreprodukzio estiloa",
|
||||
"playButtonBehavior": "erreprodukzio botoiaren portaera",
|
||||
"playButtonBehavior_description": "ezartzen du erreprodukzio botoiaren portaera lehenetsia abestiak ilaran gehitzean",
|
||||
"playerAlbumArtResolution": "erreproduzitzailearen albumaren arte-azalaren erresoluzioa",
|
||||
"gaplessAudio": "hutsune gabeko audioa",
|
||||
"gaplessAudio_description": "ezartzen du hutsunik gabeko audio ezarpena mpv-rako",
|
||||
"genreBehavior_description": "genero batean klik egiteak abestien edo albumen zerrendan lehenespenez irekitzen den zehazten du",
|
||||
"passwordStore": "pasahitzak/biltegi sekretua",
|
||||
"playerbarOpenDrawer": "txandakatu erreproduzitzailearen barra pantaila osora",
|
||||
"playerbarOpenDrawer_description": "aukera ematen du erreproduzitzailearen barran klik egiteak pantaila osoko erreproduzitzailea irekitzeko",
|
||||
@@ -534,7 +528,6 @@
|
||||
"customCss_description": "css eduki pertsonalizatua. Oharra: edukia eta urruneko URLak debekatutako propietateak dira. Zure edukiaren aurrebista erakusten da behean. Ezarri ez dituzun eremu gehigarriak daude garbiketa dela eta",
|
||||
"enableRemote": "gaitu urruneko kontrol zerbitzaria",
|
||||
"enableRemote_description": "urruneko kontrol zerbitzariari beste gailu batzuei aplikazioa kontrolatzeko aukera ematen dio",
|
||||
"doubleClickBehavior_description": "egia bada, bilaketa batean bat datozen pista guztiak ilaran jarriko dira. bestela, klikatutakoak bakarrik jarriko dira ilaran",
|
||||
"imageAspectRatio_description": "gaituta badago, azaleko artea jatorrizko aspektu-erlazioa erabiliz erakutsiko da. 1:1 ez den arterako, gainerako espazioa hutsik egongo da",
|
||||
"crossfadeStyle": "crossfade estiloa",
|
||||
"discordRichPresence": "{{discord}}-en jarduera-egoera",
|
||||
|
||||
@@ -608,9 +608,6 @@
|
||||
"gap": "$t(common.gap)",
|
||||
"itemGap": "فاصلهی آیتم (px)"
|
||||
},
|
||||
"view": {
|
||||
"card": "کارت"
|
||||
},
|
||||
"label": {
|
||||
"playCount": "تعداد پخش",
|
||||
"dateAdded": "تاریخ افزوده شدن",
|
||||
|
||||
@@ -341,7 +341,6 @@
|
||||
"homeConfiguration": "koti sivun muokkaus",
|
||||
"homeConfiguration_description": "määritä mitä osioita näkyy, ja missä järjestyksessä, koti sivulla",
|
||||
"gaplessAudio_optionWeak": "heikko (suositus)",
|
||||
"genreBehavior_description": "määrittää avautuuko generä painettaessa oletuksena ääniraita vaiko albumi listassa",
|
||||
"hotkey_browserBack": "selain takaisin",
|
||||
"hotkey_playbackPlay": "toista",
|
||||
"hotkey_playbackPlayPause": "toista / tauko",
|
||||
@@ -378,14 +377,12 @@
|
||||
"disableAutomaticUpdates": "poista automaattiset päivitykset käytöstä",
|
||||
"discordIdleStatus": "näytä rich presencen käyttämätön tila",
|
||||
"discordIdleStatus_description": "kun käytössä, päivitä tila kun soitin on käyttämättömänä",
|
||||
"doubleClickBehavior": "lisää kaikki haetut kappaleet soittojonoon tuplaklikkauksella",
|
||||
"discordUpdateInterval_description": "päivitysväli sekunnteina (vähintään 15 sekunttia)",
|
||||
"discordRichPresence_description": "ota toiston tila käyttöön {{discord}}n rich presence-toiminnossa. Kuvakkeiden avaimet ovat {{icon}}, {{playing}} ja {{paused}}",
|
||||
"discordUpdateInterval": "{{discord}} rich presencen päivitysväli",
|
||||
"enableRemote": "aktivoi etäohjauspalvelin",
|
||||
"externalLinks_description": "ottaa ulkoiset linkit (Last.fm, MusicBrainz) artistien/albumien sivuilla",
|
||||
"exitToTray": "sulje tehtäväpalkkiin",
|
||||
"doubleClickBehavior_description": "jos päällä, kaikki hakutuloksissa olevat kappaleet lisätään soittojonoon. muuten vain napsautettu kappale lisätään jonoon",
|
||||
"discordApplicationId_description": "{{discord}}n ohjelma-ID rich presenceä varten (oletuksena {{defaultId}})",
|
||||
"enableRemote_description": "aktivoi etäohjauspalvelimen, jolla muut laitteet voivat ohjata sovellusta",
|
||||
"externalLinks": "näytä ulkoiset linkit",
|
||||
@@ -396,7 +393,6 @@
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"lastfmApiKey_description": "API-avain {{lastfm}}:lle. tarvitaan kansikuvia varten",
|
||||
"passwordStore_description": "mitä salasanojen/avaimien tallennusta käytetään. muuta tätä, jos sinulla on ongelmia salasanojen tallennuksessa",
|
||||
"floatingQueueArea_description": "näyttää ikonin ikkunan oikealla reunalla jonon katselua varten",
|
||||
"homeFeature_description": "ohjaa näytetäänkö suuri esittelykaruselli kotisivulla",
|
||||
"hotkey_rate0": "arvostelun tyhjennys",
|
||||
"hotkey_togglePreviousSongFavorite": "vaihda $t(common.previousSong) suosikkiasetus",
|
||||
@@ -409,7 +405,6 @@
|
||||
"mpvExecutablePath_description": "asettaa mpv:n suoritettavan tiedoston polun. ollessa tyhjä, käytetään oletuspolkua",
|
||||
"mpvExtraParameters_help": "yksi per rivi",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"genreBehavior": "genre-sivun oletustoiminta",
|
||||
"globalMediaHotkeys": "globaalit median pikanäppäimet",
|
||||
"globalMediaHotkeys_description": "ota käyttöön tai poista käytöstä järjestelmän median pikanäppäinten käyttö toiston hallintaa",
|
||||
"hotkey_toggleCurrentSongFavorite": "vaihda $t(common.currentSong) suosikkiasetus",
|
||||
@@ -436,7 +431,6 @@
|
||||
"minimizeToTray_description": "pienennä sovellus ilmaisinalueelle",
|
||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||
"hotkey_zoomOut": "loitonna",
|
||||
"floatingQueueArea": "näytä kelluvan jonon avausalue",
|
||||
"homeFeature": "kodin esittelykaruselli",
|
||||
"hotkey_toggleFullScreenPlayer": "vaihda kokonäytön toistin",
|
||||
"hotkey_toggleRepeat": "vaihda kertaus",
|
||||
@@ -480,7 +474,6 @@
|
||||
"replayGainClipping": "{{ReplayGain}} leikkaus",
|
||||
"replayGainClipping_description": "Estää {{ReplayGain}}n aiheuttaman leikkauksen laskemalla vahvistusta automaatisesti",
|
||||
"replayGainFallback": "{{ReplayGain}} palautus",
|
||||
"playerAlbumArtResolution_description": "suurien kansikuvien resoluutio soittimen esikatselussa. suurempi tekee niistä terävempiä, mutta voi hidastaa latausta. oletuksena on 0, joka tarkoittaa automaattista",
|
||||
"replayGainMode_optionAlbum": "$t(entity.album_one)",
|
||||
"replayGainPreamp": "{{ReplayGain}} esivahvistus (dB)",
|
||||
"scrobble_description": "skrobblaa toistot mediapalvelimellesi",
|
||||
@@ -496,7 +489,6 @@
|
||||
"sidebarConfiguration": "sivupalkin asetukset",
|
||||
"sidebarConfiguration_description": "valitse kohteet ja niiden järjestys sivupalkissa",
|
||||
"volumeWidth_description": "äänenvoimakkuuden säätimen leveys",
|
||||
"playerAlbumArtResolution": "soittimen kansikuvien resoluutio",
|
||||
"playerbarOpenDrawer": "toistipalkin kokoruudun kytkin",
|
||||
"playerbarOpenDrawer_description": "sallii toistopalkin klikkaamisen avaamaan kokonäytön soittimen",
|
||||
"replayGainFallback_description": "asetettava vahvistus desibelinä (dB), jos tiedostolla ei ole {{ReplayGain}} tageja",
|
||||
@@ -783,8 +775,6 @@
|
||||
},
|
||||
"view": {
|
||||
"table": "taulukko",
|
||||
"card": "kortti",
|
||||
"poster": "juliste",
|
||||
"grid": "ruudukko",
|
||||
"list": "lista"
|
||||
}
|
||||
|
||||
+223
-32
@@ -11,10 +11,10 @@
|
||||
"skip_back": "reculer",
|
||||
"favorite": "favori",
|
||||
"next": "suivant",
|
||||
"shuffle": "lecture aléatoire",
|
||||
"shuffle": "lecture (mélangé)",
|
||||
"playbackFetchNoResults": "aucun titre trouvé",
|
||||
"playbackFetchInProgress": "chargement des titres…",
|
||||
"addNext": "ajouter ensuite",
|
||||
"addNext": "prochain",
|
||||
"playbackSpeed": "vitesse de lecture",
|
||||
"playbackFetchCancel": "cela prend du temps… fermez la notification pour annuler",
|
||||
"play": "lecture",
|
||||
@@ -24,13 +24,22 @@
|
||||
"queue_moveToTop": "déplacer la sélection vers le bas",
|
||||
"queue_moveToBottom": "déplacer la sélection vers le haut",
|
||||
"shuffle_off": "aléatoire désactivée",
|
||||
"addLast": "ajouter en dernier",
|
||||
"addLast": "dernier",
|
||||
"mute": "muet",
|
||||
"skip_forward": "avancer",
|
||||
"pause": "pause",
|
||||
"unfavorite": "retirer des favoris",
|
||||
"playSimilarSongs": "jouer des titres similaires",
|
||||
"viewQueue": "voir la file d'attente"
|
||||
"viewQueue": "voir la file d'attente",
|
||||
"addLastShuffled": "dernier (mélangé)",
|
||||
"addNextShuffled": "prochain (mélangé)",
|
||||
"queueType": "type de file d'attente",
|
||||
"queueType_default": "défaut",
|
||||
"queueType_priority": "priorité",
|
||||
"holdToShuffle": "maintenir pour mélanger",
|
||||
"lyrics": "paroles",
|
||||
"restoreQueueFromServer": "restaurer la file d'attente depuis le serveur",
|
||||
"saveQueueToServer": "enregistrer la file d'attente sur le serveur"
|
||||
},
|
||||
"action": {
|
||||
"editPlaylist": "éditer $t(entity.playlist_one)",
|
||||
@@ -54,7 +63,19 @@
|
||||
"lastfm": "Ouvrir dans Last.fm",
|
||||
"musicbrainz": "Ouvrir dans MusicBrainz"
|
||||
},
|
||||
"moveToNext": "passer au suivant"
|
||||
"moveToNext": "passer au suivant",
|
||||
"downloadStarted": "téléchargement de {{count}} éléments en cours",
|
||||
"moveItems": "déplacer les entrées",
|
||||
"shuffle": "mélanger",
|
||||
"shuffleAll": "mélanger tout",
|
||||
"shuffleSelected": "mélanger la sélection",
|
||||
"viewMore": "voir plus",
|
||||
"moveUp": "monter",
|
||||
"moveDown": "descendre",
|
||||
"holdToMoveToTop": "Maintenir pour déplacer en haut",
|
||||
"holdToMoveToBottom": "Maintenir pour déplacer en bas",
|
||||
"createRadioStation": "créer $t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "supprimer $t(entity.radioStation_one)"
|
||||
},
|
||||
"common": {
|
||||
"backward": "en arrière",
|
||||
@@ -164,7 +185,17 @@
|
||||
"private": "privé",
|
||||
"public": "publique",
|
||||
"recordLabel": "label de discographie",
|
||||
"releaseType": "type de sortie"
|
||||
"releaseType": "type de sortie",
|
||||
"doNotShowAgain": "ne plus afficher",
|
||||
"externalLinks": "liens externe",
|
||||
"faster": "plus rapide",
|
||||
"slower": "ralentir",
|
||||
"sort": "trier",
|
||||
"gridRows": "lignes de la grille",
|
||||
"tableColumns": "colonnes du tableau",
|
||||
"itemsMore": "plus {{count}}",
|
||||
"view": "vue",
|
||||
"noFilters": "aucun filtre configuré"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
|
||||
@@ -190,7 +221,10 @@
|
||||
"networkError": "une erreur de réseau est survenue",
|
||||
"badAlbum": "vous voyez cette page parce que cette chanson ne fait pas parti d'un album. vous rencontrez probablement cette erreur si vous avez une chanson qui n'est pas dans votre répertoire de musique. Jellyfin gère les chansons uniquement si elles sont dans un sous-dossier, qui est lui-même dans un dossier \"Musique(s)\"",
|
||||
"badValue": "option {{value}} invalide. Cette valeur n'existe plus",
|
||||
"notificationDenied": "les autorisations pour les notifications ont été refusées. ce paramètre n'a aucun effet"
|
||||
"notificationDenied": "les autorisations pour les notifications ont été refusées. ce paramètre n'a aucun effet",
|
||||
"multipleServerSaveQueueError": "la file d'attente de lecture contient un ou plusieurs morceaux qui ne proviennent pas du serveur actuel. Ceci n'est pas prise en charge",
|
||||
"saveQueueFailed": "échec de l'enregistrement de la file d'attente",
|
||||
"settingsSyncError": "des incohérences ont été détectées entre les paramètres du moteur de rendu et ceux du processus principal. redémarrez l'application pour appliquer les modifications"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "plus joués",
|
||||
@@ -251,7 +285,9 @@
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "partagé $t(entity.playlist_other)",
|
||||
"myLibrary": "Bibliothèque"
|
||||
"myLibrary": "Bibliothèque",
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
@@ -288,7 +324,11 @@
|
||||
"settings": "$t(common.setting_other)",
|
||||
"quit": "$t(common.quit)",
|
||||
"privateModeOff": "désactiver le mode privé",
|
||||
"privateModeOn": "activer le mode privé"
|
||||
"privateModeOn": "activer le mode privé",
|
||||
"commandPalette": "ouvrir la palette de commandes",
|
||||
"selectMusicFolder": "sélectionner le dossier musique",
|
||||
"noMusicFolder": "aucun dossier musique de sélectionner",
|
||||
"multipleMusicFolders": "{{count}} dossiers musique sélectionner"
|
||||
},
|
||||
"home": {
|
||||
"mostPlayed": "Les plus joués",
|
||||
@@ -296,7 +336,8 @@
|
||||
"explore": "Explorer depuis la bibliothèque",
|
||||
"recentlyPlayed": "Joués récemment",
|
||||
"title": "$t(common.home)",
|
||||
"recentlyReleased": "Sortis récemment"
|
||||
"recentlyReleased": "Sortis récemment",
|
||||
"genres": "$t(entity.genre_other)"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "plus de $t(entity.artist_one)",
|
||||
@@ -308,7 +349,24 @@
|
||||
"hotkeysTab": "raccourcis",
|
||||
"windowTab": "fenêtre",
|
||||
"playbackTab": "lecteur",
|
||||
"advanced": "avancé"
|
||||
"advanced": "avancé",
|
||||
"analytics": "analytique",
|
||||
"updates": "mise à jour",
|
||||
"cache": "cache",
|
||||
"application": "application",
|
||||
"queryBuilder": "constructeur de requêtes",
|
||||
"theme": "thème",
|
||||
"controls": "contrôles",
|
||||
"sidebar": "barre latérale",
|
||||
"remote": "distant",
|
||||
"exportImport": "importer/exporter",
|
||||
"scrobble": "scrobble",
|
||||
"audio": "audio",
|
||||
"lyrics": "paroles",
|
||||
"transcoding": "transcodage",
|
||||
"discord": "discord",
|
||||
"logger": "logger",
|
||||
"playerFilters": "filtres du lecteur"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
@@ -342,7 +400,9 @@
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"goToAlbumArtist": "aller à l'$t(entity.albumArtist_one)",
|
||||
"goToAlbum": "aller à l'$t(entity.album_one)"
|
||||
"goToAlbum": "aller à l'$t(entity.album_one)",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"goTo": "aller à"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
@@ -391,6 +451,15 @@
|
||||
"title": "gérer les serveurs",
|
||||
"username": "nom d'utilisateur",
|
||||
"editServerDetailsTooltip": "modifier les détails du serveur"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite_other)"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "stations radio"
|
||||
}
|
||||
},
|
||||
"setting": {
|
||||
@@ -479,7 +548,6 @@
|
||||
"fontType_description": "La police intégrée vous permet de sélectionner une des polices fourni par feishin. La police système vous permet de sélectionner une des polices fourni par votre système d'exploitation. L'option personnalisée vous permet d'importer votre propre police",
|
||||
"playButtonBehavior": "comportement du bouton play",
|
||||
"playbackStyle_optionNormal": "normale",
|
||||
"floatingQueueArea": "afficher le zone de file d'attente flottante",
|
||||
"hotkey_toggleRepeat": "basculer la répétition",
|
||||
"lyricOffset_description": "décale les paroles par le nombre de millisecondes spécifiées",
|
||||
"fontType": "type de police",
|
||||
@@ -535,7 +603,6 @@
|
||||
"discordApplicationId_description": "l'identifiant de l'application pour le statut d'activité {{discord}} (par défaut à {{defaultId}})",
|
||||
"audioExclusiveMode": "mode de sortie audio exclusif",
|
||||
"discordApplicationId": "identifiant d'application {{discord}}",
|
||||
"floatingQueueArea_description": "afficher une icon flottante sur le côté droit de l'écran pour afficher la liste d'attente",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"replayGainMode_optionNone": "$t(common.none)",
|
||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||
@@ -558,7 +625,6 @@
|
||||
"buttonSize": "taille des boutons du lecteur",
|
||||
"clearCacheSuccess": "le cache a été vidé",
|
||||
"externalLinks_description": "activer l'affichage de liens externes (Last.fm, MusicBrainz) sur la page de l'artiste/album",
|
||||
"genreBehavior": "comportement par défaut de la page des genres",
|
||||
"startMinimized_description": "démarrer l'application dans la barre des tâches",
|
||||
"externalLinks": "afficher les liens externes",
|
||||
"homeConfiguration": "configuration de la page d'accueil",
|
||||
@@ -568,12 +634,9 @@
|
||||
"imageAspectRatio_description": "si cette option est activée, les pochettes d'album seront affichées en utilisant leur rapport hauteur/largeur natif. pour les pochettes qui n'ont pas un rapport 1:1 (carré), l'espace restant sera vide",
|
||||
"mpvExtraParameters_help": "un par ligne",
|
||||
"passwordStore_description": "quel mot de passe utiliser. changez cela si vous rencontrez des problèmes pour stocker les mots de passe",
|
||||
"playerAlbumArtResolution": "résolution de la pochette d'album du lecteur",
|
||||
"passwordStore": "mots de passe",
|
||||
"playerAlbumArtResolution_description": "résolution pour l'aperçu de la pochette d'album agrandie du lecteur. plus grand le rend plus net, mais peut ralentir le chargement. la valeur par défaut est 0 (automatique)",
|
||||
"homeConfiguration_description": "configurer quels éléments sont affichés sur la page d'accueil, et dans quel ordre",
|
||||
"startMinimized": "démarrer l'application en mode réduit",
|
||||
"genreBehavior_description": "détermine si cliquer sur un genre ouvre par défaut la liste des pistes ou des albums",
|
||||
"transcode_description": "permet le transcodage vers différents formats",
|
||||
"transcodeBitrate_description": "sélectionne le débit du transcodage. 0 signifie que le serveur choisit",
|
||||
"transcodeFormat_description": "sélectionne le format du transcodage. laisser vide pour laisser le serveur décider",
|
||||
@@ -589,7 +652,6 @@
|
||||
"webAudio_description": "utiliser l'audio web. cela permet d'utiliser des fonctions avancées comme le replaygain. désactivez si vous rencontrez d'autres problèmes",
|
||||
"artistConfiguration": "page de configuration de l'artiste de l'album",
|
||||
"artistConfiguration_description": "configurer les éléments et l'ordre à afficher, sur la page de l'artiste de l'album",
|
||||
"doubleClickBehavior": "mettre en file d'attente toutes les pistes recherchées lors d'un double clic",
|
||||
"contextMenu": "configuration du menu contextuel (clic droit)",
|
||||
"contextMenu_description": "permet de masquer les éléments qui s'affichent dans le menu lorsque vous cliquez avec le bouton droit de la souris sur un élément. les éléments qui ne sont pas cochés seront masqués",
|
||||
"albumBackground": "image d'arrière-plan de l'album",
|
||||
@@ -609,7 +671,6 @@
|
||||
"translationApiKey": "clé api de traduction",
|
||||
"translationTargetLanguage_description": "langue cible pour la traduction des paroles",
|
||||
"trayEnabled_description": "afficher ou masquer l'icône et le menu de la barre d'état système. si désactivé, désactive également la réduction et la sortie vers la barre d'état système",
|
||||
"doubleClickBehavior_description": "si vrai, toutes les pistes correspondantes dans une recherche de piste seront mises en file d'attente. sinon, seule celle sur laquelle vous avez cliqué sera mise en file d'attente",
|
||||
"albumBackgroundBlur": "intensité du flou de l'image d'arrière-plan de l'album",
|
||||
"lastfmApiKey": "clé API {{lastfm}}",
|
||||
"lastfmApiKey_description": "la clé API pour {{lastfm}}. requise pour la pochette d'album",
|
||||
@@ -658,14 +719,56 @@
|
||||
"exportImportSettings_importBtn": "paramètres d'importation",
|
||||
"exportImportSettings_importSuccess": "les paramètres ont été importés avec succès !",
|
||||
"exportImportSettings_notValidJSON": "le fichier transmis n'est pas un JSON valide",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" est incorrecte - {{reason}}",
|
||||
"exportImportSettings_offendingKeyError": "la clé \"{{offendingKey}}\" est incorrecte - {{reason}}",
|
||||
"exportImportSettings_importModalTitle": "paramètres d'importation feishin",
|
||||
"crossfadeStyle": "style de fondu enchaîné",
|
||||
"discordRichPresence": "{{discord}} Rich Presence",
|
||||
"language": "langage",
|
||||
"notify_description": "affiche une notification lorsque la chanson en cours change",
|
||||
"transcode": "activer le transcodage",
|
||||
"notify": "activer les notifications de chansons"
|
||||
"notify": "activer les notifications de chansons",
|
||||
"analyticsDisable": "Désactiver l'analytique basée sur l'utilisation",
|
||||
"analyticsDisable_description": "les données d'utilisation anonymisées sont envoyées au développeur afin de contribuer à l'amélioration de l'application",
|
||||
"playerbarSlider": "curseur de la barre de lecture",
|
||||
"playerbarSliderType_optionSlider": "curseur",
|
||||
"playerbarSliderType_optionWaveform": "forme d'onde",
|
||||
"playerbarWaveformAlign": "forme d'onde alignée",
|
||||
"playerbarWaveformAlign_optionTop": "haut",
|
||||
"playerbarWaveformAlign_optionCenter": "centre",
|
||||
"playerbarWaveformAlign_optionBottom": "bas",
|
||||
"playerbarWaveformBarWidth": "largeur de la barre en forme d'onde",
|
||||
"playerbarWaveformGap": "écart de la forme d'onde",
|
||||
"playerbarWaveformRadius": "rayon de la forme d'onde",
|
||||
"showLyricsInSidebar_description": "un panneau sera attaché à la file d'attente de lecture, qui affichera les paroles",
|
||||
"showLyricsInSidebar": "afficher les paroles dans la barre de lecture latérale",
|
||||
"showVisualizerInSidebar_description": "un panneau sera ajouté à la barre de lecture latérale qui affiche le visualiseur",
|
||||
"showVisualizerInSidebar": "afficher le visualiseur dans la barre de lecture latérale",
|
||||
"audioFadeOnStatusChange": "diminution du volume sonore lors du changement d'état",
|
||||
"audioFadeOnStatusChange_description": "permet le fondu enchaîné et le fondu au noir quand la lecture/pause change d'états",
|
||||
"queryBuilder": "constructeur de requêtes",
|
||||
"queryBuilderCustomFields_inputLabel": "label",
|
||||
"queryBuilderCustomFields_inputTag": "tag",
|
||||
"queryBuilderCustomFields": "champs personnalisé",
|
||||
"queryBuilderCustomFields_description": "ajouter un champ personnalisé à utiliser dans les constructeurs de requêtes",
|
||||
"autoDJ": "DJ auto",
|
||||
"autoDJ_description": "ajouter automatiquement des titres similaire à la file d'attente",
|
||||
"autoDJ_itemCount": "nombre d'entrée",
|
||||
"autoDJ_itemCount_description": "le nombre d'entrées tentées d'être ajoutées à la file d'attente lorsque le DJ auto est activé",
|
||||
"autoDJ_timing": "timing",
|
||||
"autoDJ_timing_description": "le nombre de titres restant dans la file d'attente avant le déclenchement du DJ auto",
|
||||
"followCurrentSong_description": "défiler automatiquement jusqu'au titre en cours de lecture dans la file d'attente",
|
||||
"followCurrentSong": "suivre le titre en cours",
|
||||
"logLevel": "niveau de log",
|
||||
"logLevel_description": "définis le niveau minimum de log à afficher. débogage affiche tous les logs, erreur affiche seulement les erreurs",
|
||||
"logLevel_optionDebug": "débogage",
|
||||
"logLevel_optionError": "erreur",
|
||||
"logLevel_optionInfo": "info",
|
||||
"logLevel_optionWarn": "avertissement",
|
||||
"playerFilters": "filtrer les tires de la file d'attente",
|
||||
"playerFilters_description": "exclure les titres de la file d'attente selon les critères suivants",
|
||||
"playerbarSlider_description": "la forme d'onde n'est pas recommandée sur une connexion lente ou limitée",
|
||||
"useThemeAccentColor": "utiliser la couleur d'accent du thème",
|
||||
"useThemeAccentColor_description": "utiliser la couleur principale définie dans le thème sélectionné au lieu de la couleur d'accent personnalisée"
|
||||
},
|
||||
"form": {
|
||||
"deletePlaylist": {
|
||||
@@ -711,12 +814,17 @@
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "correspondre à tous",
|
||||
"input_optionMatchAny": "correspondre à n'importe quel",
|
||||
"title": "éditeur de requête"
|
||||
"title": "éditeur de requête",
|
||||
"addRuleGroup": "ajouter un groupe de règles",
|
||||
"removeRuleGroup": "supprimer un groupe de règles",
|
||||
"resetToDefault": "réinitialiser par défaut",
|
||||
"clearFilters": "réinitialiser les filtres"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"title": "modifier $t(entity.playlist_one)",
|
||||
"publicJellyfinNote": "Jellyfin n'indique pas si une liste de lecture est publique ou non. Si vous souhaitez que cette liste de lecture reste publique, veuillez sélectionner l'entrée suivante",
|
||||
"success": "$t(entity.playlist_one) mis à jour avec succès"
|
||||
"success": "$t(entity.playlist_one) mis à jour avec succès",
|
||||
"editNote": "les modifications manuelles ne sont pas recommandées pour les listes de lecture volumineuses. êtes-vous sûre d'accepter le risque d'une perte de données en écrasant la liste de lecture existante ?"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"title": "recherche de paroles",
|
||||
@@ -735,6 +843,31 @@
|
||||
"enabled": "le mode privé est activé, le statut de lecture est maintenant caché des intégrations externes",
|
||||
"disabled": "le mode privé est désactivé, le statut de lecture est maintenant visible des intégrations externes",
|
||||
"title": "mode privé"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "ajouter des entrées à la file d'attente",
|
||||
"description": "Cette action ajoutera tous les éléments dans la vue filtrée actuelle"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "jouer aléatoirement",
|
||||
"input_genre": "$t(entity.genre_one)",
|
||||
"input_limit": "combien de titres ?",
|
||||
"input_minYear": "à partir de l'année",
|
||||
"input_maxYear": "à l'année",
|
||||
"input_played": "filtre de lecture",
|
||||
"input_played_optionAll": "toutes les pistes",
|
||||
"input_played_optionUnplayed": "seulement les pistes non jouées",
|
||||
"input_played_optionPlayed": "seulement les pistes jouées"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "station radio créée avec succès",
|
||||
"title": "créer une station radio",
|
||||
"input_homepageUrl": "lien de la page d'accueil",
|
||||
"input_name": "nom",
|
||||
"input_streamUrl": "lien du flux en direct"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "file d'attente de lecture enregistrée sur le serveur"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -789,7 +922,13 @@
|
||||
"play_other": "{{count}} écoutes",
|
||||
"song_one": "titre",
|
||||
"song_many": "titres",
|
||||
"song_other": "titres"
|
||||
"song_other": "titres",
|
||||
"radioStation_one": "station radio",
|
||||
"radioStation_many": "stations radio",
|
||||
"radioStation_other": "stations radio",
|
||||
"radioStationWithCount_one": "{{count}} station radio",
|
||||
"radioStationWithCount_many": "{{count}} stations radio",
|
||||
"radioStationWithCount_other": "{{count}} stations radio"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
@@ -801,12 +940,31 @@
|
||||
"size": "$t(common.size)",
|
||||
"itemGap": "écart entre les éléments (en pixel)",
|
||||
"itemSize": "taille des élements (en pixel)",
|
||||
"followCurrentSong": "suivre la chanson actuelle"
|
||||
"followCurrentSong": "suivre la chanson actuelle",
|
||||
"advancedSettings": "paramètres avancés",
|
||||
"autosize": "taille automatique",
|
||||
"moveUp": "monter",
|
||||
"moveDown": "descendre",
|
||||
"pinToLeft": "épingler à gauche",
|
||||
"pinToRight": "épingler à droite",
|
||||
"alignLeft": "aligner à gauche",
|
||||
"alignCenter": "centrer",
|
||||
"alignRight": "aligner à droite",
|
||||
"itemsPerRow": "entrées par ligne",
|
||||
"size_default": "défaut",
|
||||
"size_compact": "compacte",
|
||||
"size_large": "large",
|
||||
"pagination": "pagination",
|
||||
"pagination_itemsPerPage": "entrées par page",
|
||||
"pagination_infinite": "infini",
|
||||
"pagination_paginate": "paginé",
|
||||
"alternateRowColors": "alterner les couleurs des lignes",
|
||||
"horizontalBorders": "bordures de ligne",
|
||||
"rowHoverHighlight": "surligner les lignes au survol",
|
||||
"verticalBorders": "bordure de colonne"
|
||||
},
|
||||
"view": {
|
||||
"table": "liste",
|
||||
"poster": "affiche",
|
||||
"card": "Carte",
|
||||
"grid": "grille",
|
||||
"list": "liste"
|
||||
},
|
||||
@@ -838,7 +996,12 @@
|
||||
"genre": "$t(entity.genre_one)",
|
||||
"year": "$t(common.year)",
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"codec": "$t(common.codec)"
|
||||
"codec": "$t(common.codec)",
|
||||
"albumCount": "$t(entity.album_other)",
|
||||
"genreBadge": "$t(entity.genre_one) (badges)",
|
||||
"image": "image",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
}
|
||||
},
|
||||
"column": {
|
||||
@@ -865,7 +1028,10 @@
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"size": "$t(common.size)",
|
||||
"codec": "$t(common.codec)"
|
||||
"codec": "$t(common.codec)",
|
||||
"owner": "propriétaire",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
}
|
||||
},
|
||||
"dragDropZone": {
|
||||
@@ -885,15 +1051,40 @@
|
||||
"audiobook": "livre audio",
|
||||
"audioDrama": "dramatique radio",
|
||||
"compilation": "compilation",
|
||||
"djMix": "dj mix",
|
||||
"djMix": "mix dj",
|
||||
"demo": "démo",
|
||||
"fieldRecording": "prise de son en extérieur",
|
||||
"interview": "interview",
|
||||
"live": "directe",
|
||||
"live": "live",
|
||||
"mixtape": "mixtape",
|
||||
"remix": "remix",
|
||||
"soundtrack": "bande son",
|
||||
"spokenWord": "spoken word"
|
||||
}
|
||||
},
|
||||
"queryBuilder": {
|
||||
"standardTags": "tags standard",
|
||||
"customTags": "tags personnalisées"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "est après",
|
||||
"afterDate": "est après (date)",
|
||||
"before": "est avant",
|
||||
"beforeDate": "est avant (date)",
|
||||
"contains": "contient",
|
||||
"endsWith": "se termine par",
|
||||
"inPlaylist": "est dans",
|
||||
"inTheLast": "est dans le dernier",
|
||||
"inTheRange": "est dans la plage",
|
||||
"inTheRangeDate": "est dans la plage (date)",
|
||||
"is": "est",
|
||||
"isNot": "n'est pas",
|
||||
"isGreaterThan": "est plus grand que",
|
||||
"isLessThan": "est plus petit que",
|
||||
"matchesRegex": "correspond à l'expression régulière",
|
||||
"notContains": "ne contient pas",
|
||||
"notInPlaylist": "n'est pas dans",
|
||||
"notInTheLast": "n'est pas dans le dernier",
|
||||
"startsWith": "commence par"
|
||||
}
|
||||
}
|
||||
|
||||
+258
-55
@@ -3,7 +3,7 @@
|
||||
"moveToNext": "ugrás a következőre",
|
||||
"deletePlaylist": "$t(entity.playlist_one) törlése",
|
||||
"removeFromFavorites": "eltávolítás innen $t(entity.favorite_other)",
|
||||
"setRating": "értékelés beállítása",
|
||||
"setRating": "értékelés",
|
||||
"viewPlaylists": "$t(entity.playlist_other) megtekintése",
|
||||
"openIn": {
|
||||
"lastfm": "Megnyitás Last.fm-ben",
|
||||
@@ -21,7 +21,23 @@
|
||||
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) szerkesztője",
|
||||
"addToFavorites": "$t(entity.favorite_other) kedvelése",
|
||||
"addToPlaylist": "hozzáadás lejátszási listához: $t(entity.playlist_one)",
|
||||
"refresh": "$t(common.refresh)"
|
||||
"refresh": "$t(common.refresh)",
|
||||
"downloadStarted": "megkezdődött {{count}} elem letöltése",
|
||||
"moveItems": "elemek mozgatása",
|
||||
"shuffle": "keverés",
|
||||
"shuffleAll": "összes keverése",
|
||||
"shuffleSelected": "kiválasztottak keverése",
|
||||
"viewMore": "további információ",
|
||||
"moveUp": "ugrás fel",
|
||||
"moveDown": "ugrás le",
|
||||
"holdToMoveToTop": "hosszan nyomva felülre mozgat",
|
||||
"holdToMoveToBottom": "hosszan nyomva lejjebb mozgat",
|
||||
"selectAll": "összes kijelölése",
|
||||
"deleteRadioStation": "$t(entity.radioStation_one) törlése",
|
||||
"createRadioStation": "$t(entity.radioStation_one) létrehozása",
|
||||
"openApplicationDirectory": "app könyvtár megnyitása",
|
||||
"addOrRemoveFromSelection": "hozzáadás vagy eltávolítás a kiválasztásból",
|
||||
"selectRangeOfItems": "válaszd ki a tartományt"
|
||||
},
|
||||
"common": {
|
||||
"collapse": "összecsukás",
|
||||
@@ -68,7 +84,7 @@
|
||||
"filter_other": "szűrők",
|
||||
"filters": "szűrők",
|
||||
"forward": "előre",
|
||||
"gap": "gap",
|
||||
"gap": "hézag",
|
||||
"increase": "megnövelés",
|
||||
"left": "bal",
|
||||
"limit": "korlát",
|
||||
@@ -125,11 +141,23 @@
|
||||
"sampleRate": "mintavételi frekvencia",
|
||||
"releaseType": "kiadás típusa",
|
||||
"explicitStatus": "nyílt státusz",
|
||||
"tags": "címkék"
|
||||
"tags": "címkék",
|
||||
"doNotShowAgain": "ne mutasd többet",
|
||||
"externalLinks": "külső linkek",
|
||||
"faster": "gyorsabban",
|
||||
"slower": "lassabban",
|
||||
"sort": "rendezés",
|
||||
"gridRows": "rács sorok",
|
||||
"tableColumns": "táblázat oszlopok",
|
||||
"itemsMore": "{{count}} még több",
|
||||
"view": "nézet",
|
||||
"noFilters": "nincs konfigurált szűrő",
|
||||
"countSelected": "{{count}} kiválasztott",
|
||||
"retry": "újra"
|
||||
},
|
||||
"entity": {
|
||||
"albumArtist_one": "album szerzője",
|
||||
"albumArtist_other": "album szerzői",
|
||||
"albumArtist_one": "Zenész",
|
||||
"albumArtist_other": "Zenészek",
|
||||
"albumArtistCount_one": "{{count}} album szerző",
|
||||
"albumArtistCount_other": "{{count}} album szerzők",
|
||||
"albumWithCount_one": "{{count}} album",
|
||||
@@ -162,7 +190,11 @@
|
||||
"play_one": "{{count}} lejátszás",
|
||||
"play_other": "{{count}} lejátszások",
|
||||
"trackWithCount_one": "{{count}} sáv",
|
||||
"trackWithCount_other": "{{count}} sávok"
|
||||
"trackWithCount_other": "{{count}} sávok",
|
||||
"radioStation_one": "rádió állomás",
|
||||
"radioStation_other": "rádió állomások",
|
||||
"radioStationWithCount_one": "{{count}} rádióállomás",
|
||||
"radioStationWithCount_other": "{{count}} rádióállomások"
|
||||
},
|
||||
"error": {
|
||||
"apiRouteError": "a kérést nem sikerült célba juttatni",
|
||||
@@ -188,7 +220,12 @@
|
||||
"serverRequired": "szerver szükséges",
|
||||
"serverNotSelectedError": "nincs szerver kiválasztva",
|
||||
"notificationDenied": "Az értesítések engedélyezése megtagadva. Ez a beállítás hatástalan",
|
||||
"badValue": "érvénytelen opció \"{{value}}\". ez az érték már nem létezik"
|
||||
"badValue": "érvénytelen opció \"{{value}}\". ez az érték már nem létezik",
|
||||
"noNetwork": "Szerver nem elérhető",
|
||||
"noNetworkDescription": "Nem tudok csatlakozni a szerverhez",
|
||||
"saveQueueFailed": "műsorlista mentése sikertelen",
|
||||
"settingsSyncError": "Eltéréseket találtam a leképző és a fő folyamat beállításai között. Indítsd újra az alkalmazást",
|
||||
"multipleServerSaveQueueError": "a műsorlistában egy vagy több olyan dal található, amely nem az aktuális szerverről származik. Ez nem támogatott"
|
||||
},
|
||||
"filter": {
|
||||
"albumCount": "$t(entity.album_other) darab",
|
||||
@@ -275,7 +312,8 @@
|
||||
"editPlaylist": {
|
||||
"success": "$t(entity.playlist_one) sikeresen módosítva",
|
||||
"publicJellyfinNote": "A Jellyfin valamiért nem teszi közzé, hogy egy lejátszási lista publikus-e vagy sem. Amennyiben azt szeretnéd, hogy publikus maradjon, válaszd ki az alábbi beviteli mezőt",
|
||||
"title": "szerkesztés $t(entity.playlist_one)"
|
||||
"title": "szerkesztés $t(entity.playlist_one)",
|
||||
"editNote": "A kézi szerkesztés nem ajánlott nagy lejátszási listák esetén. Biztosan vállalod a meglévő lejátszási lista felülírásával járó adatvesztés kockázatát?"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_artist": "$t(entity.artist_one)",
|
||||
@@ -285,7 +323,11 @@
|
||||
"queryEditor": {
|
||||
"title": "lekérdezés szerkesztő",
|
||||
"input_optionMatchAll": "összes egyezés",
|
||||
"input_optionMatchAny": "bármelyik egyező"
|
||||
"input_optionMatchAny": "bármelyik egyező",
|
||||
"addRuleGroup": "szabálycsoport hozzáadás",
|
||||
"removeRuleGroup": "szabálycsoport eltávolítás",
|
||||
"resetToDefault": "alapértelmezettre visszaállítás",
|
||||
"clearFilters": "szűrők törlése"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "letöltés engedélyezése",
|
||||
@@ -303,6 +345,31 @@
|
||||
"enabled": "privát mód engedélyezve, a lejátszási állapot mostantól rejtve marad a külső integrációk elől",
|
||||
"disabled": "A privát mód le van tiltva, a lejátszási állapot mostantól látható az engedélyezett külső integrációk számára",
|
||||
"title": "Privát mód"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "műsorlistához ad",
|
||||
"description": "Ez a művelet hozzáadja az összes elemet az aktuális szűrt nézetben"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "véletlenszerű lejátszás",
|
||||
"input_genre": "$t(entity.genre_one)",
|
||||
"input_limit": "Hány dal?",
|
||||
"input_minYear": "ettől az évtől",
|
||||
"input_maxYear": "eddig az évig",
|
||||
"input_played_optionAll": "összes sáv",
|
||||
"input_played": "csak szűrt zenék",
|
||||
"input_played_optionUnplayed": "Csak a még nem lejátszottak",
|
||||
"input_played_optionPlayed": "Csak a játszottak számok"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "rádió állomás sikeresen létrehozva",
|
||||
"title": "rádió állomás létrehozása",
|
||||
"input_homepageUrl": "oldal url",
|
||||
"input_name": "név",
|
||||
"input_streamUrl": "stream url"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "mentett lejátszási műsorlista a szerverre"
|
||||
}
|
||||
},
|
||||
"dragDropZone": {
|
||||
@@ -336,18 +403,22 @@
|
||||
"title": "$t(entity.album_other)"
|
||||
},
|
||||
"appMenu": {
|
||||
"collapseSidebar": "összecsukni az oldalsávot",
|
||||
"expandSidebar": "kibővíteni az oldalsávot",
|
||||
"goBack": "menj vissza",
|
||||
"goForward": "menj előre",
|
||||
"manageServers": "szerverek kezelése",
|
||||
"privateModeOff": "Privát mód kikapcsolása",
|
||||
"privateModeOn": "Privát mód bekapcsolása",
|
||||
"openBrowserDevtools": "böngésző fejlesztői eszközeinek megnyitása",
|
||||
"collapseSidebar": "oldalsáv",
|
||||
"expandSidebar": "oldalsáv",
|
||||
"goBack": "vissza",
|
||||
"goForward": "előre",
|
||||
"manageServers": "szerverek",
|
||||
"privateModeOff": "Privát mód",
|
||||
"privateModeOn": "Privát mód",
|
||||
"openBrowserDevtools": "Fejlesztői eszközök",
|
||||
"quit": "$t(common.quit)",
|
||||
"selectServer": "Szerver választása",
|
||||
"settings": "$t(common.setting_other)",
|
||||
"version": "verzió {{version}}"
|
||||
"version": "verzió {{version}}",
|
||||
"selectMusicFolder": "zene mappa kiválasztása",
|
||||
"noMusicFolder": "nincs zene mappa kiválasztva",
|
||||
"multipleMusicFolders": "{{count}} kiválasztott zene mappák",
|
||||
"commandPalette": "Parancspaletta"
|
||||
},
|
||||
"manageServers": {
|
||||
"title": "Szerverek kezelés",
|
||||
@@ -378,10 +449,12 @@
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"setRating": "$t(action.setRating)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "elem megosztása",
|
||||
"goToAlbum": "menj a $t(entity.album_one)",
|
||||
"shareItem": "Megosztás",
|
||||
"goToAlbum": "menj az $t(entity.album_one)",
|
||||
"goToAlbumArtist": "menj a $t(entity.albumArtist_one)",
|
||||
"showDetails": "információkérés"
|
||||
"showDetails": "info",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"goTo": "menj"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
@@ -425,7 +498,8 @@
|
||||
"newlyAdded": "újonnan hozzáadott megjelenések",
|
||||
"recentlyPlayed": "nemrég játszott",
|
||||
"recentlyReleased": "nemrég megjelent",
|
||||
"title": "$t(common.home)"
|
||||
"title": "$t(common.home)",
|
||||
"genres": "$t(entity.genre_other)"
|
||||
},
|
||||
"itemDetail": {
|
||||
"copyPath": "másolja az útvonalat a vágólapra",
|
||||
@@ -443,7 +517,24 @@
|
||||
"generalTab": "általános",
|
||||
"hotkeysTab": "gyorsbillentyűk",
|
||||
"windowTab": "ablak",
|
||||
"playbackTab": "visszajátszás"
|
||||
"playbackTab": "visszajátszás",
|
||||
"analytics": "elemzés",
|
||||
"updates": "frissítés",
|
||||
"cache": "gyorsítótár",
|
||||
"application": "applikáció",
|
||||
"theme": "téma",
|
||||
"controls": "irányítás",
|
||||
"sidebar": "oldalsáv",
|
||||
"remote": "távoli",
|
||||
"exportImport": "import/export",
|
||||
"scrobble": "scrobble",
|
||||
"audio": "audio",
|
||||
"lyrics": "dalszöveg",
|
||||
"transcoding": "átkódolás",
|
||||
"discord": "discord",
|
||||
"queryBuilder": "lekérdezés-építő",
|
||||
"playerFilters": "lejátszó szűrők",
|
||||
"logger": "naplózó"
|
||||
},
|
||||
"sidebar": {
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
@@ -458,17 +549,28 @@
|
||||
"search": "$t(common.search)",
|
||||
"settings": "$t(common.setting_other)",
|
||||
"shared": "megosztott $t(entity.playlist_other)",
|
||||
"tracks": "$t(entity.track_other)"
|
||||
"tracks": "$t(entity.track_other)",
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"trackList": {
|
||||
"artistTracks": "sávok tőle {{artist}}",
|
||||
"artistTracks": "dalok tőle {{artist}}",
|
||||
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
||||
"title": "$t(entity.track_other)"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite_other)"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "rádió állomások"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"addLast": "utoljára hozzáadva",
|
||||
"addNext": "következő hozzáadása",
|
||||
"addLast": "utolsónak",
|
||||
"addNext": "következő",
|
||||
"favorite": "kedvenc",
|
||||
"mute": "némítás",
|
||||
"muted": "némítva",
|
||||
@@ -488,7 +590,7 @@
|
||||
"repeat": "ismétlés",
|
||||
"repeat_all": "összes ismétlése",
|
||||
"repeat_off": "ismétlés kikapcsolva",
|
||||
"shuffle": "kevert lejátszás",
|
||||
"shuffle": "kevert (lejátszás)",
|
||||
"skip": "ugrás",
|
||||
"skip_back": "visszaugrás",
|
||||
"skip_forward": "előre ugrás",
|
||||
@@ -497,7 +599,16 @@
|
||||
"unfavorite": "kedvencekből eltávolítás",
|
||||
"pause": "szünet",
|
||||
"viewQueue": "műsorlista megtekintése",
|
||||
"shuffle_off": "kevert lejátszás ki"
|
||||
"shuffle_off": "kevert lejátszás ki",
|
||||
"addLastShuffled": "végére (keverve)",
|
||||
"addNextShuffled": "következő (keverve)",
|
||||
"queueType": "lekérdezés típus",
|
||||
"queueType_default": "alapértelmezett",
|
||||
"queueType_priority": "prioritás",
|
||||
"holdToShuffle": "tartsd lenyomva a keveréshez",
|
||||
"lyrics": "dalszöveg",
|
||||
"saveQueueToServer": "műsorlista mentése a szerverre",
|
||||
"restoreQueueFromServer": "műsorlista visszaállítása a szerverről"
|
||||
},
|
||||
"releaseType": {
|
||||
"primary": {
|
||||
@@ -563,10 +674,10 @@
|
||||
"customCssNotice": "Figyelem: bár van némi tisztítás (az url() és a content: használata nem engedélyezett), az egyéni css használata továbbra is kockázatot jelenthet, mivel megváltoztatja a felületet",
|
||||
"customFontPath_description": "beállítja az alkalmazáshoz használandó egyéni betűtípus elérési útját",
|
||||
"contextMenu": "kontextusmenü (jobb klikk) beállítás",
|
||||
"crossfadeDuration_description": "beállítja a crossfade effekt időtartamát",
|
||||
"crossfadeDuration": "crossfade időtartam",
|
||||
"crossfadeStyle": "crossfade stílus",
|
||||
"crossfadeStyle_description": "válaszd ki az audiolejátszóhoz használni kívánt crossfade stílust",
|
||||
"crossfadeDuration_description": "beállítja áthúzás effekt időtartamát",
|
||||
"crossfadeDuration": "áthúzás időtartam",
|
||||
"crossfadeStyle": "áthúzás stílus",
|
||||
"crossfadeStyle_description": "válaszd ki az audiolejátszóhoz használni kívánt áthúzás stílust",
|
||||
"releaseChannel_description": "válassz a stabil kiadás vagy a béta kiadás közül az automatikus frissítésekhez",
|
||||
"disableLibraryUpdateOnStartup": "új verziók ellenőrzését indításkor letiltása",
|
||||
"discordDisplayType_artistname": "előadó név",
|
||||
@@ -591,18 +702,16 @@
|
||||
"discordServeImage_description": "megosztja a {{discord}} borítóképet a szerverről (csak Jellyfin és Navidrome esetén elérhető). A {{discord}} botot használ a képek letöltéséhez, ezért a szervernek elérhetőnek kell lennie a nyilvános interneten",
|
||||
"discordUpdateInterval": "{{discord}} rich presence frissítési intervallum",
|
||||
"discordUpdateInterval_description": "az egyes frissítések közötti idő másodpercben (minimum 15 másodperc)",
|
||||
"doubleClickBehavior_description": "Ha igaz, akkor a keresés során talált összes megfelelő szám a műsorlistára kerül. Ellenkező esetben csak a rákattintott szám kerül a műsorlistára",
|
||||
"doubleClickBehavior": "dupla kattintással az összes keresett zeneszámot műsorlistára teszi",
|
||||
"enableAutoTranslation_description": "a dalszöveg betöltésekor automatikusan engedélyezze a fordítást",
|
||||
"enableAutoTranslation": "automatikus fordítás engedélyezése",
|
||||
"enableRemote_description": "lehetővé teszi egy távoli vezérlő szerver számára, hogy más eszközök vezéreljék az alkalmazást",
|
||||
"enableRemote": "távoli vezérlő szerver engedélyezése",
|
||||
"exitToTray_description": "kilépés az alkalmazásból a tálcára",
|
||||
"exitToTray": "kilépés a tálcára",
|
||||
"exportImportSettings_control_description": "beállítások exportálása és importálása JSON-on keresztül",
|
||||
"exportImportSettings_control_description": "Beállítások exportálása és importálása JSON-on keresztül",
|
||||
"exportImportSettings_control_exportText": "beállítások exportálása",
|
||||
"exportImportSettings_control_importText": "beállítások importálása",
|
||||
"exportImportSettings_control_title": "beállítások exportálása és importálása",
|
||||
"exportImportSettings_control_title": "Beállítások exportálása és importálása",
|
||||
"exportImportSettings_destructiveWarning": "A beállítások importálása végleges, ezért kérlek, olvasd el a fenti információkat, mielőtt rákattintasz az alábbi „Importálás” gombra!",
|
||||
"exportImportSettings_importBtn": "beállítások importálása",
|
||||
"exportImportSettings_importModalTitle": "Feishin beállítások importálása",
|
||||
@@ -611,8 +720,6 @@
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" helytelen - {{reason}}",
|
||||
"externalLinks_description": "lehetővé teszi külső linkek (Last.fm, MusicBrainz) megjelenítését az előadó/album oldalakon",
|
||||
"externalLinks": "külső linkek megjelenítése",
|
||||
"floatingQueueArea_description": "Lebegő ikon megjelenítése a képernyő jobb oldalán a műsorlista megnyitásához",
|
||||
"floatingQueueArea": "Lebegő műsorlista területének megjelenítése",
|
||||
"followLyric_description": "görgess a dalszöveghez az aktuális lejátszási pozícióig",
|
||||
"followLyric": "kövesd az aktuális dalszöveget",
|
||||
"font_description": "beállítja az alkalmazáshoz használandó betűtípust",
|
||||
@@ -622,11 +729,9 @@
|
||||
"fontType_optionCustom": "egyedi betűtípus",
|
||||
"fontType_optionSystem": "rendszer betűtípus",
|
||||
"fontType": "Font típusa",
|
||||
"gaplessAudio_description": "Beállítja az MPV résmentes (gapless) lejátszását",
|
||||
"gaplessAudio_description": "Beállítja az MPV résmentes (hézagmentes) lejátszását",
|
||||
"gaplessAudio_optionWeak": "gyenge (ajánlott)",
|
||||
"gaplessAudio": "hézagmentes hang",
|
||||
"genreBehavior_description": "meghatározza, hogy egy műfajra kattintva alapértelmezés szerint a zeneszámok vagy az albumok listája nyílik-e meg",
|
||||
"genreBehavior": "műfaj oldal alapértelmezett viselkedése",
|
||||
"globalMediaHotkeys_description": "engedélyezheted vagy letilthatod a rendszer média gyorsbillentyűinek használatát a lejátszás vezérléséhez",
|
||||
"globalMediaHotkeys": "globális média gyorsbillentyűk",
|
||||
"homeConfiguration_description": "beállíthatod, hogy mely elemek jelenjenek meg, és milyen sorrendben a kezdőlapon",
|
||||
@@ -692,7 +797,6 @@
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playButtonBehavior": "lejátszás gomb viselkedése",
|
||||
"playerAlbumArtResolution_description": "A nagy lejátszó albumborító-előnézetének felbontása. A nagyobb érték élesebb képet ad, de lassíthatja a betöltést. Alapértelmezés: 0, ami az automatikus módot jelenti",
|
||||
"minimumScrobblePercentage_description": "a szám lejátszásának minimális százaléka, amelynek el kell hangzania, mielőtt Scrobble-nak számít",
|
||||
"minimumScrobblePercentage": "Minimális Scrobble arány (százalék)",
|
||||
"minimumScrobbleSeconds": "Minimum Scrobble arány (mp)",
|
||||
@@ -706,7 +810,6 @@
|
||||
"notify": "bekapcsolja a dal értesítéseket",
|
||||
"notify_description": "értesítések megjelenítése az aktuális dal megváltoztatásakor",
|
||||
"playbackStyle_description": "válaszd ki az lejátszóhoz használni kívánt lejátszási stílust",
|
||||
"playerAlbumArtResolution": "lejátszó albumborító felbontás",
|
||||
"playerbarOpenDrawer_description": "lehetővé teszi a lejátszósávra kattintással a teljes képernyős lejátszó megnyitását",
|
||||
"playerbarOpenDrawer": "lejátszósáv teljes képernyőre váltás",
|
||||
"preferLocalLyrics_description": "ha elérhető, akkor a távoli dalszövegek helyett a helyi dalszövegeket részesítse előnyben",
|
||||
@@ -723,7 +826,7 @@
|
||||
"remoteUsername": "távoli vezérlő szerver felhasználónév",
|
||||
"passwordStore_description": "jelszó/titkos tároló kiválasztása. Módosítsd, ha problémát tapasztalsz a jelszavak tárolásánál",
|
||||
"passwordStore": "jelszó/titkos tároló",
|
||||
"playbackStyle_optionCrossFade": "crossfade",
|
||||
"playbackStyle_optionCrossFade": "áthúzás",
|
||||
"replayGainMode_optionAlbum": "$t(entity.album_one)",
|
||||
"replayGainMode_optionNone": "$t(common.none)",
|
||||
"replayGainMode_optionTrack": "$t(entity.track_one)",
|
||||
@@ -745,7 +848,7 @@
|
||||
"scrobble_description": "a lejátszás Scrobble-elése a médiaszerveredre",
|
||||
"showSkipButtons_description": "a lejátszó sávon megjelenő átugrás gombok megjelenítése vagy elrejtése",
|
||||
"showSkipButtons": "mutasd az átugrás gombot",
|
||||
"sidebarConfiguration_description": "Válaszd ki az oldalsávban megjelenő elemeket és azok sorrendjét",
|
||||
"sidebarConfiguration_description": "válaszd ki az oldalsávban megjelenő elemeket és azok sorrendjét",
|
||||
"sidebarCollapsedNavigation_description": "Navigáció megjelenítése vagy elrejtése az összecsukott oldalsávban",
|
||||
"sidebarCollapsedNavigation": "Összecsukott oldalsáv navigációja",
|
||||
"sidebarConfiguration": "oldalsáv konfigurációja",
|
||||
@@ -755,7 +858,7 @@
|
||||
"sidePlayQueueStyle_description": "beállítja az oldalsó műsorlista stílusát",
|
||||
"mediaSession_description": "lehetővé teszi a Windows Media Session integrációját, a médiavezérlők és metaadatok megjelenítését a rendszer hangerő-átfedésben és a zárolási képernyőn (csak Windows)",
|
||||
"mediaSession": "média munkamenet engedélyezése",
|
||||
"sidePlayQueueStyle": "oldalsó műsorlista stílusa",
|
||||
"sidePlayQueueStyle": "oldalsó műsorlista stílus",
|
||||
"skipDuration": "átugrás hossza",
|
||||
"skipPlaylistPage_description": "lejátszási listához való navigáláskor az alapértelmezett oldal helyett a lejátszási lista dalainak listájára mutató oldalra lépjen",
|
||||
"skipPlaylistPage": "lejátszási lista oldalának átugrása",
|
||||
@@ -788,13 +891,55 @@
|
||||
"zoom": "nagyítási arány",
|
||||
"webAudio": "web audio használata",
|
||||
"windowBarStyle_description": "válaszd ki az címsor stílusát",
|
||||
"windowBarStyle": "címsor stílusa",
|
||||
"windowBarStyle": "címsor",
|
||||
"zoom_description": "beállítja az alkalmazás nagyítási arányát",
|
||||
"volumeWheelStep_description": "A hangerő változásának mértéke az egérgörgő használatakor a hangerő sávon",
|
||||
"volumeWheelStep": "hangerőgörgő lépés",
|
||||
"volumeWidth": "hangerő sáv szélessége",
|
||||
"webAudio_description": "Web Audio használata. Ez lehetővé teszi a fejlettebb funkciókat, például a ReplayGain-t. (Kapcsold ki, ha problémát tapasztalsz)",
|
||||
"volumeWidth_description": "hangerő sáv szélessége"
|
||||
"volumeWidth_description": "hangerő sáv szélessége",
|
||||
"analyticsDisable_description": "Anonim használati adatok kerülnek elküldésre a fejlesztőnek, hogy segítsék az alkalmazás fejlesztését",
|
||||
"analyticsDisable": "Használati alapú adatok küldésének kikapcsolása",
|
||||
"playerbarSlider": "lejátszósáv csúszka",
|
||||
"playerbarSliderType_optionSlider": "csűszka",
|
||||
"playerbarSliderType_optionWaveform": "hullámforma",
|
||||
"playerbarWaveformAlign": "hullámforma igazítás",
|
||||
"playerbarWaveformAlign_optionTop": "felső",
|
||||
"playerbarWaveformAlign_optionCenter": "középső",
|
||||
"playerbarWaveformAlign_optionBottom": "alsó",
|
||||
"playerbarWaveformBarWidth": "hullámforma oszlopszélesség",
|
||||
"playerbarWaveformGap": "hullámforma oszlopköz",
|
||||
"playerbarWaveformRadius": "hullámforma sugara",
|
||||
"showLyricsInSidebar_description": "a csatolt műsorlistához egy panel kerül hozzáadásra, amelyen a dalszövegek jelennek meg",
|
||||
"showLyricsInSidebar": "dalszövegek megjelenítése a lejátszó oldalsávban",
|
||||
"showVisualizerInSidebar_description": "a lejátszó oldalsávjához egy panel kerül hozzáadásra, amely megjeleníti a vizualizáció",
|
||||
"showVisualizerInSidebar": "vizualizáció megjelenítése a lejátszó oldalsávban",
|
||||
"queryBuilder": "lekérdezés-építő",
|
||||
"queryBuilderCustomFields_inputLabel": "címke",
|
||||
"queryBuilderCustomFields_inputTag": "jelölés",
|
||||
"queryBuilderCustomFields": "egyéni mezők",
|
||||
"queryBuilderCustomFields_description": "egyéni mezők hozzáadása a lekérdezés-építőhöz",
|
||||
"autoDJ": "auto DJ",
|
||||
"autoDJ_timing": "időzítés",
|
||||
"autoDJ_description": "hasonló dalokat automatikusan hozzáad a műsorlistához",
|
||||
"autoDJ_itemCount": "elem szám",
|
||||
"autoDJ_itemCount_description": "az auto DJ engedélyezésekor a műsorsorba felvenni kívánt elemek száma",
|
||||
"autoDJ_timing_description": "az auto DJ elindulása előtt a műsorlistában maradt dalok száma",
|
||||
"followCurrentSong_description": "automatikusan görgesse a műsorlistát az aktuálisan lejátszott dalra",
|
||||
"followCurrentSong": "kövesd az aktuális dalt",
|
||||
"logLevel": "naplózási szint",
|
||||
"logLevel_description": "beállítja a megjelenítendő minimális naplószintet. A debug minden naplót megjeleníti, az error csak a hibákat",
|
||||
"logLevel_optionDebug": "debug",
|
||||
"logLevel_optionError": "error",
|
||||
"logLevel_optionInfo": "info",
|
||||
"logLevel_optionWarn": "figyelmeztetés",
|
||||
"playerFilters": "Szűrje a dalokat a műsorlistából",
|
||||
"playerFilters_description": "a következő kritériumok alapján kihagyja a dalokat a műsorlistából",
|
||||
"playerbarSlider_description": "a hullámforma nem ajánlott lassú vagy korlátozott internetkapcsolat esetén",
|
||||
"audioFadeOnStatusChange": "audio behúzás állapotváltozáskor",
|
||||
"audioFadeOnStatusChange_description": "lehetővé teszi a lehúzást és a behúzást, amikor a lejátszás/szünet állapot megváltozik",
|
||||
"useThemeAccentColor": "használd a téma kiemelő színét",
|
||||
"useThemeAccentColor_description": "a kiválasztott témában meghatározott alapszínt használja az egyéni kiemelő szín helyett"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
@@ -826,13 +971,16 @@
|
||||
"duration": "$t(common.duration)",
|
||||
"favorite": "$t(common.favorite)",
|
||||
"actions": "$t(common.action_other)",
|
||||
"album": "$t(entity.album_one)"
|
||||
"album": "$t(entity.album_one)",
|
||||
"albumCount": "$t(entity.album_other)",
|
||||
"genreBadge": "$t(entity.genre_one) (jelvények)",
|
||||
"image": "kép",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
},
|
||||
"view": {
|
||||
"card": "kártya",
|
||||
"grid": "rács",
|
||||
"list": "lista",
|
||||
"poster": "poszter",
|
||||
"table": "táblázat"
|
||||
},
|
||||
"general": {
|
||||
@@ -843,7 +991,28 @@
|
||||
"size": "$t(common.size)",
|
||||
"tableColumns": "táblázat oszlopai",
|
||||
"itemGap": "elemek közötti távolság (px)",
|
||||
"itemSize": "elem mérete (px)"
|
||||
"itemSize": "elem mérete (px)",
|
||||
"advancedSettings": "speciális beállítások",
|
||||
"autosize": "automatikus méret",
|
||||
"moveUp": "felfelé",
|
||||
"moveDown": "lefelé",
|
||||
"pinToLeft": "balra tűz",
|
||||
"pinToRight": "jobbra tűz",
|
||||
"alignLeft": "igazítás balra",
|
||||
"alignCenter": "igazítás középre",
|
||||
"alignRight": "igazítás jobbra",
|
||||
"itemsPerRow": "elemek soronként",
|
||||
"size_default": "alapértelmezett",
|
||||
"size_compact": "kompakt",
|
||||
"size_large": "nagy",
|
||||
"pagination": "oldalszámozás",
|
||||
"pagination_itemsPerPage": "elemek oldalanként",
|
||||
"pagination_infinite": "végtelen",
|
||||
"pagination_paginate": "oldal számozva",
|
||||
"alternateRowColors": "alternatív sor színek",
|
||||
"horizontalBorders": "sorhatárok",
|
||||
"rowHoverHighlight": "sor kiemelése egérrel",
|
||||
"verticalBorders": "oszlophatárok"
|
||||
}
|
||||
},
|
||||
"column": {
|
||||
@@ -870,7 +1039,41 @@
|
||||
"title": "cím",
|
||||
"trackNumber": "sáv",
|
||||
"album": "album",
|
||||
"albumArtist": "album előadó"
|
||||
"albumArtist": "album előadó",
|
||||
"owner": "tulajdonos",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
}
|
||||
},
|
||||
"queryBuilder": {
|
||||
"standardTags": "általános címkék",
|
||||
"customTags": "egyedi címkék"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "után",
|
||||
"afterDate": "(dátum) után",
|
||||
"before": "előtt",
|
||||
"beforeDate": "(dátum) előtt",
|
||||
"contains": "tartalmaz",
|
||||
"inPlaylist": "benne",
|
||||
"endsWith": "végződik",
|
||||
"inTheLast": "elmúlt",
|
||||
"inTheRange": "tartományban",
|
||||
"inTheRangeDate": "(dátum) tartományban",
|
||||
"isGreaterThan": "nagyobb mint",
|
||||
"isLessThan": "kisebb mint",
|
||||
"notContains": "nem tartalmazza",
|
||||
"notInPlaylist": "nincs benne",
|
||||
"startsWith": "kezdődik",
|
||||
"notInTheLast": "nem az elmúlt",
|
||||
"matchesRegex": "illeszkedik a regexre",
|
||||
"is": "van",
|
||||
"isNot": "nincs"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "perc",
|
||||
"secondShort": "mp",
|
||||
"hourShort": "óra",
|
||||
"dayShort": "nap"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,16 +492,12 @@
|
||||
"discordRichPresence_description": "aktifkan status pemutaran di status aktivitas {{discord}}. Gambar tombol adalah: {{icon}}, {{playing}}, dan {{paused}}",
|
||||
"discordUpdateInterval": "interval pembaruan status aktivitas {{discord}}",
|
||||
"discordUpdateInterval_description": "waktu dalam detik antara setiap pembaruan (minimal 15 detik)",
|
||||
"doubleClickBehavior": "masukkan semua lagu yang dicari saat mengklik dua kali",
|
||||
"doubleClickBehavior_description": "jika true, semua lagu yang cocok dalam pencarian lagu akan dimasukkan ke dalam antrean. Jika tidak, hanya lagu yang dipilih yang akan dimasukkan ke dalam antrean",
|
||||
"enableRemote": "aktifkan kontrol jarak jauh server",
|
||||
"enableRemote_description": "aktifkan kontrol jarak jauh server untuk memungkinkan perangkat lain mengontrol aplikasi",
|
||||
"externalLinks": "Tampilkan tautan eksternal",
|
||||
"externalLinks_description": "Izinkan untuk menampilkan tautan eksternal (Last.fm, MusicBrainz) di halaman artis/album",
|
||||
"exitToTray": "keluar ke baki",
|
||||
"exitToTray_description": "keluar dari aplikasi ke baki sistem",
|
||||
"floatingQueueArea": "tampilkan area antrean mengambang",
|
||||
"floatingQueueArea_description": "menampilkan ikon mengambang di sisi kanan layar untuk melihat antrean pemutaran",
|
||||
"followLyric": "ikuti lirik saat ini",
|
||||
"followLyric_description": "gulir lirik ke posisi pemutaran saat ini",
|
||||
"font": "font",
|
||||
@@ -514,8 +510,6 @@
|
||||
"gaplessAudio": "audio tanpa jeda",
|
||||
"gaplessAudio_description": "tentukan pengaturan audio tanpa jeda untuk mpv",
|
||||
"gaplessAudio_optionWeak": "lemah (disarankan)",
|
||||
"genreBehavior": "Perilaku default halaman genre",
|
||||
"genreBehavior_description": "Menentukan apakah klik pada genre akan membuka daftar lagu atau album secara default",
|
||||
"globalMediaHotkeys": "tombol pintasan media global",
|
||||
"globalMediaHotkeys_description": "aktifkan atau nonaktifkan penggunaan tombol pintasan sistem media untuk mengontrol pemutaran",
|
||||
"homeConfiguration": "Pengaturan halaman beranda",
|
||||
@@ -585,8 +579,6 @@
|
||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playerAlbumArtResolution": "resolusi sampul album pemutar",
|
||||
"playerAlbumArtResolution_description": "resolusi untuk pratinjau sampul album pemutar besar. semakin besar akan membuatnya lebih tajam, tetapi dapat memperlambat pemuatan. Nilai default adalah 0, yang berarti otomatis",
|
||||
"playerbarOpenDrawer": "Buka pemutar ke layar penuh",
|
||||
"playerbarOpenDrawer_description": "Izinkan mengklik bilah pemutar untuk membuka pemutar di layar penuh",
|
||||
"remotePassword": "kata sandi kontrol jarak jauh server",
|
||||
@@ -732,8 +724,6 @@
|
||||
"year": "$t(common.year)"
|
||||
},
|
||||
"view": {
|
||||
"card": "kartu",
|
||||
"poster": "poster",
|
||||
"table": "tabel"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,6 @@
|
||||
"accentColor_description": "imposta colore d'accento per l'applicazione",
|
||||
"playbackStyle_optionNormal": "normale",
|
||||
"windowBarStyle": "stile barra della finestra",
|
||||
"floatingQueueArea": "mostra l'area di passaggio della coda fluttante",
|
||||
"hotkey_toggleRepeat": "attiva/disattiva ripeti",
|
||||
"lyricOffset_description": "aumenta/dimuisce l'offset del testo di una specifica quantità di millisecondi",
|
||||
"sidebarConfiguration_description": "seleziona gli elementi e l'ordine in cui appaiono nella barra laterale",
|
||||
@@ -271,7 +270,6 @@
|
||||
"hotkey_rate0": "rimuovi voto",
|
||||
"discordApplicationId": "application id {{discord}}",
|
||||
"applicationHotkeys_description": "configura tasti a scelta rapida dell'applicazione. attiva/disattiva la casella per impostare un tasto a scelta rapida globale (solo desktop)",
|
||||
"floatingQueueArea_description": "visualizza l'icona di passaggio sul lato destro dello schermo per mostrare la coda di riproduzione",
|
||||
"hotkey_volumeMute": "silenzia volume",
|
||||
"remoteUsername": "username server di controllo remoto",
|
||||
"sidebarPlaylistList": "lista playlist nella barra laterale",
|
||||
@@ -335,14 +333,10 @@
|
||||
"discordListening_description": "mostra lo stato come in ascolto invece che in riproduzione",
|
||||
"discordServeImage": "recupera le immagini di {{discord}} dal server",
|
||||
"discordServeImage_description": "condividi la copertina per lo stato attività di {{discord}} direttamente dal server, disponibile solo per Jellyfin e Navidrome",
|
||||
"doubleClickBehavior": "aggiungi alla coda tutte le tracce cercate, con un doppio clic",
|
||||
"doubleClickBehavior_description": "se attivato, tutte le tracce corrispondenti alla ricerca verranno aggiunte alla coda. altrimenti, verrà aggiunta alla coda solo la traccia selezionata",
|
||||
"externalLinks": "mostra link esterni",
|
||||
"externalLinks_description": "consente di visualizzare link esterni (Last.fm, MusicBrainz) sulle pagine di artista/album",
|
||||
"preferLocalLyrics": "utilizza i testi locali",
|
||||
"preferLocalLyrics_description": "usa i testi locali anziché quelli online, quando disponibili",
|
||||
"genreBehavior": "comportamento predefinito della pagina genere",
|
||||
"genreBehavior_description": "determina se cliccando su un genere si apre di default la lista dei brani o degli album",
|
||||
"homeConfiguration": "configurazione della home page",
|
||||
"homeConfiguration_description": "configura quali elementi vengono mostrati e in quale ordine nella home page",
|
||||
"homeFeature": "carosello in evidenza nella home page",
|
||||
@@ -361,8 +355,6 @@
|
||||
"passwordStore": "Archivio di password/segreti",
|
||||
"passwordStore_description": "specifica quale archivio di password e segreti utilizzare. modificalo in caso di problemi nel salvataggio delle credenziali",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playerAlbumArtResolution": "risoluzione della copertina nel lettore",
|
||||
"playerAlbumArtResolution_description": "la risoluzione dell’anteprima della copertina nel lettore in formato grande. valori più alti la rendono più nitida, ma possono rallentare il caricamento. Il valore predefinito è 0, che indica la modalità automatica",
|
||||
"sidePlayQueueStyle_optionAttached": "fissata",
|
||||
"sidePlayQueueStyle_optionDetached": "sganciata",
|
||||
"startMinimized": "avvia minimizzato",
|
||||
@@ -710,10 +702,8 @@
|
||||
},
|
||||
"view": {
|
||||
"table": "tabella",
|
||||
"card": "Scheda",
|
||||
"grid": "griglia",
|
||||
"list": "lista",
|
||||
"poster": "poster"
|
||||
"list": "lista"
|
||||
},
|
||||
"label": {
|
||||
"releaseDate": "data rilascio",
|
||||
|
||||
+19
-15
@@ -144,7 +144,6 @@
|
||||
"replayGainMode": "{{ReplayGain}} モード",
|
||||
"playbackStyle_optionNormal": "通常",
|
||||
"windowBarStyle": "ウィンドウバースタイル",
|
||||
"floatingQueueArea": "フローティング再生キューエリアの表示",
|
||||
"replayGainFallback_description": "ファイルに {{ReplayGain}} タグがない場合に適用するゲイン (dB 単位)",
|
||||
"replayGainPreamp_description": "{{ReplayGain}} の値に適用されるプリアンプゲインを調整します",
|
||||
"hotkey_toggleRepeat": "リピートの切り替え",
|
||||
@@ -170,7 +169,6 @@
|
||||
"hotkey_rate0": "評価をクリア",
|
||||
"discordApplicationId": "{{discord}} アプリケーション ID",
|
||||
"applicationHotkeys_description": "アプリケーションのホットキーを設定します。チェックボックスを切り替えて、グローバルホットキーとして設定します (デスクトップのみ)",
|
||||
"floatingQueueArea_description": "画面右側に、再生キューをフローティング表示するためのホバーアイコンが表示されます",
|
||||
"hotkey_volumeMute": "音量をミュート",
|
||||
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) をお気に入り登録/解除",
|
||||
"remoteUsername": "リモートコントロールサーバーのユーザー名",
|
||||
@@ -205,7 +203,6 @@
|
||||
"volumeWidth_description": "音量スライダーの幅",
|
||||
"volumeWidth": "音量スライダーの幅",
|
||||
"webAudio_description": "Web Audio を使用します。これにより、リプレイゲインなどの高度な機能が有効になります。それ以外の場合は無効にしてください",
|
||||
"playerAlbumArtResolution_description": "大画面プレーヤーのアルバムアートプレビューの解像度。解像度が高いほど鮮明になりますが、読み込みが遅くなる可能性があります。デフォルトは 0 (自動設定) です",
|
||||
"mpvExtraParameters_help": "1 行に 1 つずつ",
|
||||
"musicbrainz_description": "MusicBrainz ID が存在するアーティスト/アルバムページに MusicBrainz へのリンクを表示します",
|
||||
"musicbrainz": "MusicBrainz リンクを表示する",
|
||||
@@ -213,7 +210,6 @@
|
||||
"neteaseTranslation": "NetEase 翻訳歌詞を有効にする",
|
||||
"passwordStore_description": "使用するパスワード/シークレットストア。パスワードの保存に問題がある場合はこれを変更してください",
|
||||
"passwordStore": "パスワード/シークレットストア",
|
||||
"playerAlbumArtResolution": "プレーヤーのアルバムアートの解像度",
|
||||
"playerbarOpenDrawer_description": "プレーヤーバーをクリックすると全画面プレーヤーが開きます",
|
||||
"preferLocalLyrics_description": "利用可能な場合は、リモート歌詞よりもローカル歌詞を優先します",
|
||||
"preferLocalLyrics": "ローカル歌詞を優先する",
|
||||
@@ -225,18 +221,15 @@
|
||||
"imageAspectRatio": "ネイティブのカバーアートの縦横比を使用する",
|
||||
"language": "言語",
|
||||
"imageAspectRatio_description": "有効にすると、カバーアートはネイティブの縦横比で表示されます。縦横比が 1:1 でない場合、残りのスペースは空白になります",
|
||||
"lastfm_description": "アーティスト/アルバムページに Last.fm へのリンクを表示する",
|
||||
"lastfm_description": "アーティスト/アルバムページに Last.fm へのリンクを表示します",
|
||||
"lastfm": "Last.fm リンクを表示する",
|
||||
"lastfmApiKey": "{{lastfm}} API キー",
|
||||
"homeConfiguration_description": "ホーム画面に表示する項目と表示順序を設定します",
|
||||
"homeConfiguration": "ホーム画面の設定",
|
||||
"externalLinks": "外部リンクを表示する",
|
||||
"externalLinks_description": "アーティスト/アルバムページで外部リンク (Last.fm、MusicBrainz) を表示できるようにします",
|
||||
"doubleClickBehavior_description": "true の場合、トラック検索で一致するすべてのトラックがキューに入ります。そうでない場合は、クリックされたトラックのみがキューに入ります",
|
||||
"enableAutoTranslation": "自動翻訳を有効にする",
|
||||
"enableAutoTranslation_description": "歌詞が読み込まれたときに自動的に翻訳を有効にします",
|
||||
"genreBehavior_description": "ジャンルをクリックした際に、デフォルトでトラックリストまたはアルバムリストのどちらを開くか決定します",
|
||||
"genreBehavior": "ジャンルページのデフォルトの動作",
|
||||
"albumBackground_description": "アルバムアートを含むアルバムページに背景画像を追加します",
|
||||
"albumBackground": "アルバムの背景画像",
|
||||
"albumBackgroundBlur": "アルバムの背景画像のぼかしサイズ",
|
||||
@@ -262,7 +255,7 @@
|
||||
"customCssEnable_description": "カスタム CSS の記述を許可します",
|
||||
"customCssEnable": "カスタム CSS を有効にする",
|
||||
"customCssNotice": "警告: ある程度のサニタイズ (url() と content: の禁止) はありますが、カスタム CSS を使用するとインターフェースの変更によりリスクが生じる可能性があります",
|
||||
"releaseChannel_optionBeta": "beta",
|
||||
"releaseChannel_optionBeta": "ベータ",
|
||||
"releaseChannel_optionLatest": "最新",
|
||||
"releaseChannel": "リリースチャンネル",
|
||||
"releaseChannel_description": "自動更新のために安定版リリースまたはベータ版リリースを選択してください",
|
||||
@@ -297,7 +290,6 @@
|
||||
"discordPausedStatus_description": "有効にすると、プレーヤーが一時停止されているときにもステータスを表示します",
|
||||
"discordDisplayType_description": "ステータスで聴いている内容を変更します",
|
||||
"discordLinkType": "{{discord}} Presence リンク",
|
||||
"doubleClickBehavior": "ダブルクリック時に検索した全トラックをキューに追加する",
|
||||
"discordLinkType_mbz_lastfm": "{{musicbrainz}} と {{lastfm}} のフォールバック",
|
||||
"homeFeature": "ホーム画面の注目カルーセル",
|
||||
"homeFeature_description": "ホーム画面に大きな注目カルーセルを表示するかどうかを制御します",
|
||||
@@ -328,7 +320,13 @@
|
||||
"lastfm": "Last.fm で開く",
|
||||
"musicbrainz": "MusicBrainz で開く"
|
||||
},
|
||||
"moveToNext": "次"
|
||||
"moveToNext": "次",
|
||||
"downloadStarted": "{{count}} 曲のダウンロードを開始しました",
|
||||
"moveItems": "曲を移動",
|
||||
"shuffle": "シャッフル",
|
||||
"shuffleAll": "すべてシャッフル",
|
||||
"shuffleSelected": "選択した曲をシャッフル",
|
||||
"viewMore": "さらに表示"
|
||||
},
|
||||
"common": {
|
||||
"backward": "戻る",
|
||||
@@ -427,14 +425,16 @@
|
||||
"explicit": "明示的",
|
||||
"albumGain": "アルバムゲイン",
|
||||
"albumPeak": "アルバムピーク",
|
||||
"releaseType": "リリースタイプ"
|
||||
"releaseType": "リリースタイプ",
|
||||
"doNotShowAgain": "再度表示しない",
|
||||
"externalLinks": "外部リンク",
|
||||
"sort": "分類",
|
||||
"gridRows": "グリッド行"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
"view": {
|
||||
"card": "カード",
|
||||
"table": "テーブル",
|
||||
"poster": "ポスター",
|
||||
"grid": "グリッド",
|
||||
"list": "リスト"
|
||||
},
|
||||
@@ -666,7 +666,7 @@
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "$t(entity.artist_one) の他の項目",
|
||||
"moreFromGeneric": "{{item}} の他の項目",
|
||||
"moreFromGeneric": "{{item}} の他の作品",
|
||||
"released": "リリース"
|
||||
},
|
||||
"setting": {
|
||||
@@ -801,6 +801,10 @@
|
||||
"enabled": "プライベートモードが有効になりました。再生ステータスは外部連携から非表示になっています",
|
||||
"disabled": "プライベートモードが無効になりました。再生ステータスは有効になっている外部連携に表示されています",
|
||||
"title": "プライベートモード"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "キューにアイテムを追加する",
|
||||
"description": "このアクションは、現在のフィルターされたビュー内のすべてのアイテムを追加します"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -21,7 +21,23 @@
|
||||
},
|
||||
"viewPlaylists": "$t(entity.playlist_other) 보기",
|
||||
"setRating": "평점 지정",
|
||||
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) 편집기 펼치기"
|
||||
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) 편집기 펼치기",
|
||||
"addOrRemoveFromSelection": "선택항목에서 추가 또는 제거",
|
||||
"selectRangeOfItems": "항목의 범위 선택",
|
||||
"createRadioStation": "$t(entity.radioStation_one) 생성",
|
||||
"deleteRadioStation": "$t(entity.radioStation_one) 삭제",
|
||||
"selectAll": "전부 선택",
|
||||
"downloadStarted": "{{count}}개 항목 다운로드 시작했습니다",
|
||||
"moveUp": "위로 옮기기",
|
||||
"moveDown": "아래로 옮기기",
|
||||
"holdToMoveToTop": "맨 위로 옮기기 위해 끌기",
|
||||
"holdToMoveToBottom": "맨 아래로 옮기기 위해 끌기",
|
||||
"moveItems": "항목 옮기기",
|
||||
"shuffle": "섞기",
|
||||
"shuffleAll": "모두 섞기",
|
||||
"shuffleSelected": "선택항목 섞기",
|
||||
"viewMore": "더 보기",
|
||||
"openApplicationDirectory": "앱 디렉토리 열기"
|
||||
},
|
||||
"common": {
|
||||
"translation": "번역",
|
||||
@@ -122,7 +138,18 @@
|
||||
"recordLabel": "레이블",
|
||||
"releaseType": "발매형태",
|
||||
"explicit": "성인컨텐츠",
|
||||
"clean": "클린"
|
||||
"clean": "클린",
|
||||
"countSelected": "{{count}}개 선택됨",
|
||||
"doNotShowAgain": "다시 보지 않기",
|
||||
"view": "보기",
|
||||
"externalLinks": "외부 링크",
|
||||
"faster": "빠르게",
|
||||
"noFilters": "필터 미설정",
|
||||
"slower": "천천히",
|
||||
"sort": "정렬",
|
||||
"gridRows": "행 그리드",
|
||||
"tableColumns": "테이블 열",
|
||||
"itemsMore": "{{count}}개 더"
|
||||
},
|
||||
"entity": {
|
||||
"albumWithCount_other": "{{count}} 앨범",
|
||||
@@ -142,7 +169,9 @@
|
||||
"play_other": "{{count}} 재생",
|
||||
"playlistWithCount_other": "{{count}} 재생목록",
|
||||
"smartPlaylist": "스마트 $t(entity.playlist_one)",
|
||||
"track_other": "트랙"
|
||||
"track_other": "트랙",
|
||||
"radioStation_other": "라디오 방송국",
|
||||
"radioStationWithCount_other": "{{count}}개 라디오 방송국"
|
||||
},
|
||||
"error": {
|
||||
"systemFontError": "시스템 폰트를 가져오는데 실패하였습니다",
|
||||
@@ -409,8 +438,6 @@
|
||||
"dateAdded": "추가된 날짜"
|
||||
},
|
||||
"view": {
|
||||
"card": "카드",
|
||||
"poster": "포스터",
|
||||
"table": "표"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,10 +494,8 @@
|
||||
},
|
||||
"view": {
|
||||
"table": "tabell",
|
||||
"card": "kort",
|
||||
"grid": "rutenett",
|
||||
"list": "liste",
|
||||
"poster": "plakat"
|
||||
"list": "liste"
|
||||
},
|
||||
"general": {
|
||||
"autoFitColumns": "automatisk kolonnetilpasning",
|
||||
|
||||
+216
-17
@@ -21,7 +21,23 @@
|
||||
"lastfm": "Open in Last.fm",
|
||||
"musicbrainz": "Open in MusicBrainz"
|
||||
},
|
||||
"moveToNext": "ga naar volgende"
|
||||
"moveToNext": "ga naar volgende",
|
||||
"downloadStarted": "begonnen met downloaden van {{count}} items",
|
||||
"moveItems": "verplaats items",
|
||||
"shuffle": "shuffle",
|
||||
"shuffleAll": "shuffle alles",
|
||||
"shuffleSelected": "shuffle geselecteerde",
|
||||
"viewMore": "bekijk meer",
|
||||
"addOrRemoveFromSelection": "toevoegen of verwijderen van selectie",
|
||||
"selectRangeOfItems": "selecteer een reeks van nummers",
|
||||
"createRadioStation": "maak $t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "verwijder $t(entity.radioStation_one)",
|
||||
"selectAll": "selecteer alles",
|
||||
"moveUp": "beweeg naar boven",
|
||||
"moveDown": "beweeg naar beneden",
|
||||
"holdToMoveToTop": "ingedrukt houden om naar boven te verplaatsen",
|
||||
"holdToMoveToBottom": "ingedrukt houden om naar beneden te verplaatsen",
|
||||
"openApplicationDirectory": "applicatiefolder openen"
|
||||
},
|
||||
"common": {
|
||||
"backward": "achteruit",
|
||||
@@ -116,7 +132,27 @@
|
||||
"share": "deel",
|
||||
"explicit": "expliciet",
|
||||
"sampleRate": "sample rate",
|
||||
"tags": "tags"
|
||||
"tags": "tags",
|
||||
"albumPeak": "albumpiek",
|
||||
"doNotShowAgain": "niet opnieuw tonen",
|
||||
"externalLinks": "externe links",
|
||||
"faster": "sneller",
|
||||
"preview": "voorvertoning",
|
||||
"private": "privé",
|
||||
"public": "publiekelijk",
|
||||
"recordLabel": "platenlabel",
|
||||
"releaseType": "uitgavetype",
|
||||
"slower": "slomer",
|
||||
"sort": "sorteer",
|
||||
"trackGain": "trackvolume",
|
||||
"trackPeak": "piekniveau",
|
||||
"clean": "schoon",
|
||||
"gridRows": "rasterrijen",
|
||||
"tableColumns": "tabelkolommen",
|
||||
"itemsMore": "{{count}} meer",
|
||||
"countSelected": "{{count}} geselecteerd",
|
||||
"view": "bekijken",
|
||||
"noFilters": "geen filters ingesteld"
|
||||
},
|
||||
"filter": {
|
||||
"rating": "rating",
|
||||
@@ -185,7 +221,12 @@
|
||||
"shareItem": "deel item",
|
||||
"goToAlbum": "ga naar $t(entity.album_one)",
|
||||
"goToAlbumArtist": "ga naar $t(entity.albumArtist_one)",
|
||||
"showDetails": "haal info op"
|
||||
"showDetails": "haal info op",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"goTo": "ga naar"
|
||||
},
|
||||
"appMenu": {
|
||||
"selectServer": "selecteer server",
|
||||
@@ -199,7 +240,10 @@
|
||||
"goBack": "terug",
|
||||
"goForward": "vooruit",
|
||||
"privateModeOff": "schakel private modus uit",
|
||||
"privateModeOn": "schakel private modus in"
|
||||
"privateModeOn": "schakel private modus in",
|
||||
"selectMusicFolder": "selecteer muziekfolder",
|
||||
"noMusicFolder": "geen muziekfolder geselecteerd",
|
||||
"multipleMusicFolders": "{{count}} muziekfolders geselecteerd"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "meer van deze $t(entity.artist_one)",
|
||||
@@ -219,19 +263,23 @@
|
||||
"showLyricMatch": "toon liedtekst match",
|
||||
"synchronized": "gesynchronizeerd",
|
||||
"unsynchronized": "niet gesynchronizeerd",
|
||||
"useImageAspectRatio": "gebruik aspect ratio van de afbeelding"
|
||||
"useImageAspectRatio": "gebruik aspect ratio van de afbeelding",
|
||||
"lyricOffset": "songtekst-vertraging (ms)",
|
||||
"showLyricProvider": "toon songtekstaanbieder"
|
||||
},
|
||||
"lyrics": "liedtekst",
|
||||
"related": "gerelateerd",
|
||||
"upNext": "volgende",
|
||||
"noLyrics": "geen liedtekst gevonden"
|
||||
"noLyrics": "geen liedtekst gevonden",
|
||||
"visualizer": "visualizer"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
},
|
||||
"albumList": {
|
||||
"title": "$t(entity.album_other)",
|
||||
"artistAlbums": "albums van {{artist}}"
|
||||
"artistAlbums": "albums van {{artist}}",
|
||||
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
|
||||
},
|
||||
"albumArtistDetail": {
|
||||
"about": "Over {{artist}}",
|
||||
@@ -241,7 +289,8 @@
|
||||
"topSongs": "top nummers",
|
||||
"topSongsFrom": "top nummers van {{title}}",
|
||||
"viewAll": "bekijk alle",
|
||||
"viewAllTracks": "bekijk alle $t(entity.track_other)"
|
||||
"viewAllTracks": "bekijk alle $t(entity.track_other)",
|
||||
"recentReleases": "recente uitgaven"
|
||||
},
|
||||
"manageServers": {
|
||||
"title": "beheer servers",
|
||||
@@ -253,7 +302,8 @@
|
||||
},
|
||||
"genreList": {
|
||||
"showAlbums": "toon $t(entity.genre_one) $t(entity.album_other)",
|
||||
"showTracks": "toon $t(entity.genre_one) $t(entity.track_other)"
|
||||
"showTracks": "toon $t(entity.genre_one) $t(entity.track_other)",
|
||||
"title": "$t(entity.genre_other)"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
@@ -264,7 +314,69 @@
|
||||
"title": "commandos"
|
||||
},
|
||||
"home": {
|
||||
"explore": "ontdek van uw biblitheek"
|
||||
"explore": "ontdek van uw biblitheek",
|
||||
"mostPlayed": "meest gespeeld",
|
||||
"newlyAdded": "nieuw toegevoegde uitgaven",
|
||||
"recentlyPlayed": "recent afgespeeld",
|
||||
"recentlyReleased": "recent uitgekomen",
|
||||
"title": "$t(common.home)"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite_other)"
|
||||
},
|
||||
"itemDetail": {
|
||||
"copyPath": "kopieer pad naar klembord",
|
||||
"copiedPath": "pad succesvol gekopieerd",
|
||||
"openFile": "toon nummer in bestandsbeheerder"
|
||||
},
|
||||
"playlist": {
|
||||
"reorder": "herschikken is alleen ingeschakeld wanneer er op ID wordt gestorteerd"
|
||||
},
|
||||
"playlistList": {
|
||||
"title": "$t(entity.playlist_other)"
|
||||
},
|
||||
"setting": {
|
||||
"advanced": "geavanceerd",
|
||||
"analytics": "analyses",
|
||||
"generalTab": "algemeen",
|
||||
"hotkeysTab": "sneltoetsen",
|
||||
"playbackTab": "weergave",
|
||||
"windowTab": "venster",
|
||||
"updates": "update",
|
||||
"cache": "cache",
|
||||
"application": "applicatie",
|
||||
"queryBuilder": "querybouwer",
|
||||
"theme": "thema",
|
||||
"controls": "besturing",
|
||||
"sidebar": "zijbalk",
|
||||
"remote": "afstand",
|
||||
"exportImport": "importeren/exporteren",
|
||||
"scrobble": "scrobble",
|
||||
"audio": "geluid",
|
||||
"lyrics": "songtekst",
|
||||
"transcoding": "transcoderen",
|
||||
"discord": "discord"
|
||||
},
|
||||
"sidebar": {
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"albums": "$t(entity.album_other)",
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"folders": "$t(entity.folder_other)",
|
||||
"genres": "$t(entity.genre_other)",
|
||||
"home": "$t(common.home)",
|
||||
"myLibrary": "mijn bibliotheek",
|
||||
"nowPlaying": "nu aan het spelen",
|
||||
"playlists": "$t(entity.playlist_other)",
|
||||
"search": "$t(common.search)",
|
||||
"settings": "$t(common.setting_other)",
|
||||
"shared": "$t(entity.playlist_other) gedeeld",
|
||||
"tracks": "$t(entity.track_other)"
|
||||
},
|
||||
"trackList": {
|
||||
"artistTracks": "nummers van {{artist}}",
|
||||
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
||||
"title": "$t(entity.track_other)"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -290,7 +402,8 @@
|
||||
"badValue": "ongeldige optie \"{{value}}\". Deze waarde bestaat niet langer",
|
||||
"networkError": "een netwerkfout heeft zich voorgedaan",
|
||||
"notificationDenied": "toestemming voor meldingen werd afgewezen. Deze instelling heeft geen effect",
|
||||
"openError": "kon het bestand niet openen"
|
||||
"openError": "kon het bestand niet openen",
|
||||
"badAlbum": "je ziet deze pagina omdat dit nummer niet onderdeel is van een album. je komt waarchijnlijk dit probleem tegen als je een nummer op het bovenste niveau van je muziekmap hebt staan. Jellyfin kan alleen nummers groeperen als ze in een folder zitten"
|
||||
},
|
||||
"entity": {
|
||||
"genre_one": "genre",
|
||||
@@ -325,7 +438,13 @@
|
||||
"trackWithCount_one": "{{count}} track",
|
||||
"trackWithCount_other": "{{count}} tracks",
|
||||
"song_one": "lied",
|
||||
"song_other": "liedjes"
|
||||
"song_other": "liedjes",
|
||||
"play_one": "{{count}} keer afgespeeld",
|
||||
"play_other": "{{count}} keren afgespeeld",
|
||||
"radioStation_one": "radiostation",
|
||||
"radioStation_other": "radiostations",
|
||||
"radioStationWithCount_one": "{{count}} radiostation",
|
||||
"radioStationWithCount_other": "{{count}} radiostations"
|
||||
},
|
||||
"table": {
|
||||
"column": {
|
||||
@@ -340,7 +459,44 @@
|
||||
},
|
||||
"setting": {
|
||||
"hotkey_rate5": "rating 5 sterren",
|
||||
"hotkey_rate4": "rating 4 sterren"
|
||||
"hotkey_rate4": "rating 4 sterren",
|
||||
"discordLinkType_description": "voegt externe links van {{lastfm}} of {{musicbrainz}} toe aan het nummer- en artiestveld in {{discord}} rich presence. {{musicbrainz}} is de meest accurate, maar vereist tags en geeft geen artiestenlinks, terwijl {{lastfm}} altijd een link moet aanbieden. maakt geen extra netwerkverzoeken",
|
||||
"discordLinkType_mbz_lastfm": "{{musicbrainz}} met {{lastfm}} terugval",
|
||||
"discordLinkType_none": "$t(common.none)",
|
||||
"discordLinkType": "{{discord}} presencekoppelingen",
|
||||
"discordListening_description": "status weergeven als ‘luisterend’ in plaats van ‘afspelend’",
|
||||
"discordListening": "status weergeven als 'luisterend'",
|
||||
"discordPausedStatus_description": "wanneer ingeschakeld, wordt de status ook weergegeven als de speler gepauzeerd is",
|
||||
"discordPausedStatus": "rich presence tonen wanneer gepauseerd",
|
||||
"discordRichPresence": "{{discord}} rich presence",
|
||||
"exitToTray_description": "sluit de applicatie naar het systeemvak",
|
||||
"exitToTray": "sluit naar systeemvak",
|
||||
"exportImportSettings_control_description": "exporteer en importeer instellingen via JSON",
|
||||
"exportImportSettings_control_exportText": "exporteer instellingen",
|
||||
"exportImportSettings_control_importText": "importeer instellingen",
|
||||
"exportImportSettings_control_title": "importeer / exporteer instellingen",
|
||||
"exportImportSettings_destructiveWarning": "instellingen importeren is destructief, beoordeel bovenstaande voordat je beneden op \"importeer\" klikt!",
|
||||
"exportImportSettings_importBtn": "importeer instellingen",
|
||||
"exportImportSettings_importModalTitle": "importeer feishing-instellingen",
|
||||
"exportImportSettings_importSuccess": "instellingen zijn succesvol geïmporteerd!",
|
||||
"exportImportSettings_notValidJSON": "het ingevoerde bestand is geen valide JSON",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" is incorrect - {{reason}}",
|
||||
"externalLinks_description": "maakt het mogelijk om externe links (Last.fm, MusicBrainz) te tonen op artiesten-/albumpagina's",
|
||||
"externalLinks": "toon externe links",
|
||||
"followLyric_description": "scroll de songtekst naar de huidige positie",
|
||||
"followLyric": "volg huidige songtekst",
|
||||
"font_description": "zet het lettertype om te gebruiken in de applicatie",
|
||||
"font": "lettertype",
|
||||
"fontType_description": "ingebouwde lettertypes selecteert een van de lettertypes aangeboden door feishin. met systeemlettertype kunt u elk lettertype selecteren dat door uw besturingssysteem wordt aangeboden. met aangepast kunt u uw eigen lettertype opgeven",
|
||||
"fontType_optionBuiltIn": "ingebouwde lettertype",
|
||||
"fontType_optionCustom": "aangepaste lettertype",
|
||||
"fontType_optionSystem": "systeemlettertype",
|
||||
"fontType": "lettertype-type",
|
||||
"gaplessAudio_description": "stelt de gapless audio-instelling voor mpv in",
|
||||
"gaplessAudio_optionWeak": "zwak (aanbevolen)",
|
||||
"gaplessAudio": "gapless audio",
|
||||
"globalMediaHotkeys_description": "het gebruik van systeem mediahotkeys voor controle van afspelen aan-/uitzetten",
|
||||
"globalMediaHotkeys": "globale mediasneltoetsen"
|
||||
},
|
||||
"form": {
|
||||
"addServer": {
|
||||
@@ -355,7 +511,8 @@
|
||||
"ignoreSsl": "negeer ssl $t(common.restartRequired)",
|
||||
"ignoreCors": "negeer cors $t(common.restartRequired)",
|
||||
"error_savePassword": "er is iets mis gegaan met het opslaan van het wachtwoord",
|
||||
"input_preferInstantMix": "verkies directe mix"
|
||||
"input_preferInstantMix": "verkies directe mix",
|
||||
"input_preferInstantMixDescription": "gebruik alleen instant mix om vergelijkbare nummer te krijgen. handig wanneer je plugins hebt die dit gedrag aanpassen"
|
||||
},
|
||||
"deletePlaylist": {
|
||||
"title": "verwijder $t(entity.playlist_one)",
|
||||
@@ -371,15 +528,21 @@
|
||||
"input_owner": "$t(common.owner)"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"success": "{{message}}$t(entity.song_other) aan {{numOfPlaylists}} $t(entity.playlist_other) toegevoegd",
|
||||
"success": "$t(entity.trackWithCount, {\"count\": {{message}} }) aan $t(entity.trackWithCount, {\"count\": {{message}} }) toegevoegd",
|
||||
"title": "aan $t(entity.playlist_one) toevoegen",
|
||||
"input_skipDuplicates": "duplicaten overslaan",
|
||||
"input_playlists": "$t(entity.playlist_other)"
|
||||
"input_playlists": "$t(entity.playlist_other)",
|
||||
"create": "maak $t(entity.playlist_one) {{playlist}}",
|
||||
"searchOrCreate": "zoek $t(entity.playlist_other) of typ om een nieuwe te maken"
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "alles matchen",
|
||||
"input_optionMatchAny": "elke match",
|
||||
"title": "zoekopdrachtbewerker"
|
||||
"title": "zoekopdrachtbewerker",
|
||||
"addRuleGroup": "rolgroep toevoegen",
|
||||
"removeRuleGroup": "rolgroep verwijderen",
|
||||
"resetToDefault": "terugzetten naar standaard",
|
||||
"clearFilters": "filters wissen"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_name": "$t(common.name)",
|
||||
@@ -407,6 +570,42 @@
|
||||
"enabled": "private modus ingeschakeld, afspeelstatus is nu verborgen voor externe integraties",
|
||||
"disabled": "private modus uitgeschakeld, afspeelstatus is nu zichtbaar voor externe integraties",
|
||||
"title": "private modus"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "items toevoegen aan de wachtrij",
|
||||
"description": "Deze actie voegt alle items in de huidige gefilterde weergave toe"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "willekeurig afspelen",
|
||||
"input_genre": "$t(entity.genre_one)",
|
||||
"input_limit": "hoeveel nummers?",
|
||||
"input_minYear": "van jaar",
|
||||
"input_maxYear": "naar jaar",
|
||||
"input_played": "speel filter",
|
||||
"input_played_optionAll": "alle nummers",
|
||||
"input_played_optionUnplayed": "alleen ongespeelde nummers",
|
||||
"input_played_optionPlayed": "alleen gespeelde nummers"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"addLast": "achteraan toevoegen",
|
||||
"addNext": "als volgende toevoegen",
|
||||
"addLastShuffled": "als laatste toevoegen (geschud)",
|
||||
"addNextShuffled": "als volgende toevoegen (geschud)",
|
||||
"favorite": "favoriet",
|
||||
"mute": "dempen",
|
||||
"muted": "gedempt",
|
||||
"next": "volgende",
|
||||
"play": "afspelen",
|
||||
"playbackFetchCancel": "dit duurt even... sluit de notificatie om te annuleren",
|
||||
"playbackFetchInProgress": "nummers laden…",
|
||||
"playbackFetchNoResults": "geen nummers gevonden",
|
||||
"playbackSpeed": "weergavesnelheid",
|
||||
"playRandom": "willekeurig afspelen",
|
||||
"playSimilarSongs": "vergelijkbare nummers afspelen",
|
||||
"previous": "vorige",
|
||||
"queue_clear": "wachtrij wissen",
|
||||
"queue_moveToBottom": "verplaats geselecteerde naar boven",
|
||||
"queue_moveToTop": "verplaats geselecteerde naar beneden"
|
||||
}
|
||||
}
|
||||
|
||||
+514
-60
@@ -21,7 +21,23 @@
|
||||
"lastfm": "Otwórz w Last.fm",
|
||||
"musicbrainz": "Otwórz w MusicBrainz"
|
||||
},
|
||||
"moveToNext": "przesuń na następne"
|
||||
"moveToNext": "przesuń na następne",
|
||||
"downloadStarted": "rozpoczęto pobieranie {{count}} elementów",
|
||||
"moveItems": "przenieś elementy",
|
||||
"shuffle": "odtwarzaj losowo",
|
||||
"shuffleAll": "odtwarzaj wszystkie losowo",
|
||||
"shuffleSelected": "odtwarzaj losowo wybrane",
|
||||
"viewMore": "wyświetl więcej",
|
||||
"moveUp": "przenieś wyżej",
|
||||
"moveDown": "przenieś niżej",
|
||||
"holdToMoveToTop": "przytrzymaj aby, przesunąć na górę",
|
||||
"holdToMoveToBottom": "przytrzymaj aby, przesunąć na dół",
|
||||
"createRadioStation": "utwórz $t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "usuń $t(entity.radioStation_one)",
|
||||
"addOrRemoveFromSelection": "dodaj lub usuń z wyboru",
|
||||
"selectRangeOfItems": "wybierz zakres elementów",
|
||||
"selectAll": "wybierz wszystkie",
|
||||
"openApplicationDirectory": "otwórz katalog aplikacji"
|
||||
},
|
||||
"common": {
|
||||
"increase": "zwiększ",
|
||||
@@ -38,7 +54,7 @@
|
||||
"descending": "malejąco",
|
||||
"add": "dodaj",
|
||||
"ascending": "rosnąco",
|
||||
"dismiss": "anuluj",
|
||||
"dismiss": "odrzuć",
|
||||
"year": "rok",
|
||||
"limit": "limit",
|
||||
"minimize": "zminimalizuj",
|
||||
@@ -55,9 +71,9 @@
|
||||
"clear": "wyczyść",
|
||||
"forward": "do przodu",
|
||||
"delete": "usuń",
|
||||
"cancel": "cofnij",
|
||||
"cancel": "anuluj",
|
||||
"forceRestartRequired": "zrestartuj aby zastosować zmiany... zamknij powiadomienie aby zrestartować",
|
||||
"setting": "ustawienia",
|
||||
"setting": "ustawienie",
|
||||
"version": "wersja",
|
||||
"title": "tytuł",
|
||||
"filter_one": "filtr",
|
||||
@@ -121,39 +137,58 @@
|
||||
"viewReleaseNotes": "zobacz notatki dotyczące wydania",
|
||||
"bitDepth": "głębia bitowa",
|
||||
"sampleRate": "częstotliwość próbkowania",
|
||||
"tags": "tagi"
|
||||
"tags": "tagi",
|
||||
"explicitStatus": "status explicit",
|
||||
"doNotShowAgain": "nie pokazuj tego ponownie",
|
||||
"externalLinks": "linki zewnętrzne",
|
||||
"faster": "szybciej",
|
||||
"private": "prywatne",
|
||||
"public": "publiczne",
|
||||
"recordLabel": "wytwórnia",
|
||||
"releaseType": "typ wydania",
|
||||
"slower": "wolniej",
|
||||
"sort": "sortuj",
|
||||
"explicit": "explicit",
|
||||
"clean": "czyste",
|
||||
"gridRows": "siatka wierszy",
|
||||
"tableColumns": "tabela kolumn",
|
||||
"itemsMore": "{{count}} więcej",
|
||||
"noFilters": "nie skonfigurowano filtrów",
|
||||
"view": "wyświetl",
|
||||
"countSelected": "wybrano {{count}}",
|
||||
"retry": "spróbuj ponownie"
|
||||
},
|
||||
"entity": {
|
||||
"genre_one": "gatunek",
|
||||
"genre_few": "gatunków",
|
||||
"genre_few": "gatunki",
|
||||
"genre_many": "gatunków",
|
||||
"artist_one": "artysta",
|
||||
"artist_few": "artystów",
|
||||
"artist_many": "artystów",
|
||||
"albumArtist_one": "artysta albumu",
|
||||
"albumArtist_few": "artysta albumów",
|
||||
"albumArtist_many": "artysta albumów",
|
||||
"artist_one": "wykonawca",
|
||||
"artist_few": "wykonawców",
|
||||
"artist_many": "wykonawców",
|
||||
"albumArtist_one": "wykonawca albumu",
|
||||
"albumArtist_few": "wykonawców albumów",
|
||||
"albumArtist_many": "wykonawców albumów",
|
||||
"albumWithCount_one": "{{count}} album",
|
||||
"albumWithCount_few": "{{count}} albumów",
|
||||
"albumWithCount_few": "{{count}} albumy",
|
||||
"albumWithCount_many": "{{count}} albumów",
|
||||
"favorite_one": "ulubiony",
|
||||
"favorite_few": "ulubione",
|
||||
"favorite_many": "ulubione",
|
||||
"artistWithCount_one": "{{count}} artysta",
|
||||
"artistWithCount_few": "{{count}} artystów",
|
||||
"artistWithCount_many": "{{count}} artystów",
|
||||
"artistWithCount_one": "{{count}} wykonawca",
|
||||
"artistWithCount_few": "{{count}} wykonawców",
|
||||
"artistWithCount_many": "{{count}} wykonawców",
|
||||
"folder_one": "katalog",
|
||||
"folder_few": "katalogi",
|
||||
"folder_many": "katalogów",
|
||||
"album_one": "album",
|
||||
"album_few": "albumów",
|
||||
"album_few": "albumy",
|
||||
"album_many": "albumów",
|
||||
"playlistWithCount_one": "{{count}} lista odtwarzania",
|
||||
"playlistWithCount_few": "{{count}} listy odtwarzania",
|
||||
"playlistWithCount_many": "{{count}} list odtwarzania",
|
||||
"playlist_one": "lista odtwarzania",
|
||||
"playlist_few": "listy odtwarzania",
|
||||
"playlist_many": "list odtwarzania",
|
||||
"playlistWithCount_one": "{{count}} playlista",
|
||||
"playlistWithCount_few": "{{count}} playlisty",
|
||||
"playlistWithCount_many": "{{count}} playlist",
|
||||
"playlist_one": "playlista",
|
||||
"playlist_few": "playlisty",
|
||||
"playlist_many": "playlist",
|
||||
"folderWithCount_one": "{{count}} katalog",
|
||||
"folderWithCount_few": "{{count}} katalogi",
|
||||
"folderWithCount_many": "{{count}} katalogów",
|
||||
@@ -175,7 +210,13 @@
|
||||
"play_many": "{{count}} odtworzeń",
|
||||
"song_one": "piosenka",
|
||||
"song_few": "piosenki",
|
||||
"song_many": "piosenek"
|
||||
"song_many": "piosenek",
|
||||
"radioStation_one": "stacja radiowa",
|
||||
"radioStation_few": "stacje radiowe",
|
||||
"radioStation_many": "stacji radiowych",
|
||||
"radioStationWithCount_one": "{{count}} stacja radiowa",
|
||||
"radioStationWithCount_few": "{{count}} stacje radiowe",
|
||||
"radioStationWithCount_many": "{{count}} stacji radiowych"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "uruchom ponownie serwer aby używać nowego portu",
|
||||
@@ -201,7 +242,12 @@
|
||||
"networkError": "wystąpił błąd sieciowy",
|
||||
"openError": "nie można otworzyć pliku",
|
||||
"badValue": "niewłaściwa opcja \"{{value}}\". ta wartość już nie istnieje",
|
||||
"notificationDenied": "odmówiono uprawnień dla powiadomień. to ustawienie nie będzie miało efektu"
|
||||
"notificationDenied": "odmówiono uprawnień dla powiadomień. to ustawienie nie będzie miało efektu",
|
||||
"multipleServerSaveQueueError": "kolejka odtwarzania ma jedną lub więcej piosenek które nie pochodzą z aktualnego serwera. to nie jest wspierane",
|
||||
"saveQueueFailed": "nie udało się zapisać kolejki",
|
||||
"settingsSyncError": "zostały znalezione różnice pomiędzy ustawieniami w rendererze a głównym procesem. uruchom aplikację ponownie aby, zastosować zmiany",
|
||||
"noNetwork": "serwer niedostępny",
|
||||
"noNetworkDescription": "nie udało się połączyć z tym serwerem"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "najczęściej odtwarzane",
|
||||
@@ -245,7 +291,8 @@
|
||||
"albumCount": "liczba $t(entity.album_other)",
|
||||
"id": "id",
|
||||
"isPublic": "jest publiczny",
|
||||
"album": "$t(entity.album_one)"
|
||||
"album": "$t(entity.album_one)",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
},
|
||||
"form": {
|
||||
"deletePlaylist": {
|
||||
@@ -272,13 +319,17 @@
|
||||
"input_savePassword": "zapisz hasło",
|
||||
"ignoreSsl": "zignoruj ssl ($t(common.restartRequired))",
|
||||
"ignoreCors": "zignoruj cors ($t(common.restartRequired))",
|
||||
"error_savePassword": "wystąpił błąd podczas próby zapisania hasła"
|
||||
"error_savePassword": "wystąpił błąd podczas próby zapisania hasła",
|
||||
"input_preferInstantMix": "preferuj natychmiastowy mix",
|
||||
"input_preferInstantMixDescription": "używaj tylko natychmiastowego mixu, by otrzymać podobne piosenki. przydatne gdy masz wtyczki które zmieniają to zachowanie"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"success": "dodano $t(entity.trackWithCount, {\"count\": {{message}} }) do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "dodano do $t(entity.playlist_one)",
|
||||
"input_skipDuplicates": "pomiń duplikaty",
|
||||
"input_playlists": "$t(entity.playlist_other)"
|
||||
"input_playlists": "$t(entity.playlist_other)",
|
||||
"create": "utwórz $t(entity.playlist_one) {{playlist}}",
|
||||
"searchOrCreate": "wyszukaj $t(entity.playlist_other) lub wpisz, aby utworzyć nową"
|
||||
},
|
||||
"updateServer": {
|
||||
"title": "uaktualnij serwer",
|
||||
@@ -287,7 +338,11 @@
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "dopasuj wszystkie",
|
||||
"input_optionMatchAny": "dopasuj dowolne",
|
||||
"title": "edytor zapytań"
|
||||
"title": "edytor zapytań",
|
||||
"addRuleGroup": "dodaj grupę zasad",
|
||||
"removeRuleGroup": "usuń grupę zasad",
|
||||
"resetToDefault": "przywróć domyślne",
|
||||
"clearFilters": "wyczyść filtry"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_name": "$t(common.name)",
|
||||
@@ -297,7 +352,8 @@
|
||||
"editPlaylist": {
|
||||
"title": "edytuj $t(entity.playlist_one)",
|
||||
"success": "$t(entity.playlist_one) zaktualizowana pomyślnie",
|
||||
"publicJellyfinNote": "Z jakiegoś powodu Jellyfin nie udostępnia informacji na temat publiczności playlisty. Jeżeli chcesz, aby ta pozostała publiczna, mniej wybraną poniższą opcję"
|
||||
"publicJellyfinNote": "Z jakiegoś powodu Jellyfin nie udostępnia informacji na temat publiczności playlisty. Jeżeli chcesz, aby ta pozostała publiczna, mniej wybraną poniższą opcję",
|
||||
"editNote": "manualne edytowanie nie jest zalecane dla dużych playlist. czy na pewno zgadzasz się na ryzyko utraty danych wywołane przez nadpisanie istniejącej playlisty?"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "zezwól na pobieranie",
|
||||
@@ -311,6 +367,36 @@
|
||||
"enabled": "tryb prywatny włączony, status odtwarzania jest ukryty przed usługami zewnętrznymi",
|
||||
"disabled": "tryb prywatny wyłączony, status odtwarzania jest widoczny dla usług zewnętrznych",
|
||||
"title": "tryb prywatny"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "dodaj elementy do kolejki",
|
||||
"description": "Ta akcja doda wszystkie elementy w aktualnie przefiltrowanym widoku"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "odtwarzaj losowo",
|
||||
"input_genre": "$t(entity.genre_one)",
|
||||
"input_limit": "ile piosenek?",
|
||||
"input_minYear": "z roku",
|
||||
"input_maxYear": "do roku",
|
||||
"input_played": "filtr odtwarzania",
|
||||
"input_played_optionAll": "wszystkie utwory",
|
||||
"input_played_optionUnplayed": "tylko nieodtworzone utwory",
|
||||
"input_played_optionPlayed": "tylko odtworzone utwory"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "zapisano kolejkę odtwarzania na serwerze"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "stacja radiowa utworzona pomyślnie",
|
||||
"title": "utwórz stację radiową",
|
||||
"input_homepageUrl": "url strony głównej",
|
||||
"input_name": "nazwa",
|
||||
"input_streamUrl": "url strumienia"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "eksportuj tekst",
|
||||
"input_synced": "eksportuj zsynchronizowany tekst",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
@@ -349,7 +435,11 @@
|
||||
"goBack": "do tyłu",
|
||||
"goForward": "do przodu",
|
||||
"privateModeOff": "wyłącz tryb prywatny",
|
||||
"privateModeOn": "włącz tryb prywatny"
|
||||
"privateModeOn": "włącz tryb prywatny",
|
||||
"selectMusicFolder": "wybierz folder muzyki",
|
||||
"noMusicFolder": "nie wybrano folderu muzyki",
|
||||
"multipleMusicFolders": "wybrano {{count}} folderów muzyki",
|
||||
"commandPalette": "otwórz paletę komend"
|
||||
},
|
||||
"contextMenu": {
|
||||
"addToPlaylist": "$t(action.addToPlaylist)",
|
||||
@@ -375,7 +465,9 @@
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"goToAlbum": "przejdź do $t(entity.album_one)",
|
||||
"goToAlbumArtist": "przejdź do $t(entity.albumArtist_one)"
|
||||
"goToAlbumArtist": "przejdź do $t(entity.albumArtist_one)",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"goTo": "przejdź do"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "więcej od $t(entity.artist_one)",
|
||||
@@ -392,7 +484,7 @@
|
||||
},
|
||||
"albumList": {
|
||||
"title": "$t(entity.album_other)",
|
||||
"artistAlbums": "albumy artysty {{artist}}",
|
||||
"artistAlbums": "albumy wykonawcy {{artist}}",
|
||||
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
|
||||
},
|
||||
"sidebar": {
|
||||
@@ -408,21 +500,43 @@
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "udostępnione $t(entity.playlist_other)",
|
||||
"myLibrary": "Moja biblioteka"
|
||||
"myLibrary": "Moja biblioteka",
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"home": {
|
||||
"mostPlayed": "najczęściej odtwarzane",
|
||||
"newlyAdded": "niedawno dodane",
|
||||
"title": "$t(common.home)",
|
||||
"explore": "przeglądaj z biblioteki",
|
||||
"recentlyPlayed": "ostatnio odtwarzane"
|
||||
"recentlyPlayed": "ostatnio odtwarzane",
|
||||
"recentlyReleased": "ostatnio wydane",
|
||||
"genres": "$t(entity.genre_other)"
|
||||
},
|
||||
"setting": {
|
||||
"playbackTab": "odtworzenia",
|
||||
"generalTab": "ogólne",
|
||||
"hotkeysTab": "skróty klawiszowe",
|
||||
"windowTab": "okno",
|
||||
"advanced": "zaawansowane"
|
||||
"advanced": "zaawansowane",
|
||||
"analytics": "analityka",
|
||||
"updates": "aktualizacja",
|
||||
"cache": "cache",
|
||||
"application": "aplikacja",
|
||||
"queryBuilder": "kreator zapytań",
|
||||
"theme": "motyw",
|
||||
"controls": "sterowanie",
|
||||
"sidebar": "pasek boczny",
|
||||
"remote": "zdalne",
|
||||
"exportImport": "import/eksport",
|
||||
"scrobble": "scrobble",
|
||||
"audio": "audio",
|
||||
"lyrics": "tekst",
|
||||
"transcoding": "transkodowanie",
|
||||
"discord": "discord",
|
||||
"playerFilters": "filtry odtwarzacza",
|
||||
"logger": "logger",
|
||||
"lyricsDisplay": "wyświetlanie tekstu"
|
||||
},
|
||||
"trackList": {
|
||||
"title": "$t(entity.track_other)",
|
||||
@@ -449,7 +563,9 @@
|
||||
"viewDiscography": "przeglądaj dyskografię",
|
||||
"relatedArtists": "powiązane z $t(entity.artist_other)",
|
||||
"appearsOn": "pojawia się na",
|
||||
"viewAllTracks": "zobacz wszystko $t(entity.track_other)"
|
||||
"viewAllTracks": "zobacz wszystko $t(entity.track_other)",
|
||||
"groupingTypeAll": "wszystkie typy wydań",
|
||||
"groupingTypePrimary": "główne typy wydań"
|
||||
},
|
||||
"itemDetail": {
|
||||
"copyPath": "kopiuj ścieżkę do schowka",
|
||||
@@ -466,6 +582,15 @@
|
||||
},
|
||||
"playlist": {
|
||||
"reorder": "zmiana kolejności jest możliwa tylko podczas sortowania według id"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite_other)"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "stacje radiowe"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
@@ -480,10 +605,10 @@
|
||||
"skip_back": "przeskocz do tyłu",
|
||||
"favorite": "ulubione",
|
||||
"next": "następny",
|
||||
"shuffle": "odtwarzaj losowo",
|
||||
"shuffle": "odtwarzaj (losowo)",
|
||||
"playbackFetchNoResults": "nie znaleziono utworów",
|
||||
"playbackFetchInProgress": "wczytywanie utworów…",
|
||||
"addNext": "dodaj następny",
|
||||
"addNext": "następne",
|
||||
"playbackSpeed": "prędkość odtwarzania",
|
||||
"playbackFetchCancel": "to potrwa chwilę... zamknij powiadomienie aby anulować",
|
||||
"play": "odtwarzaj",
|
||||
@@ -495,11 +620,22 @@
|
||||
"queue_moveToTop": "przesuń zaznaczone na dół",
|
||||
"queue_moveToBottom": "przesuń zaznaczone na górę",
|
||||
"shuffle_off": "losowa kolejność wyłączona",
|
||||
"addLast": "dodaj na końcu",
|
||||
"addLast": "ostatnie",
|
||||
"mute": "wycisz",
|
||||
"skip_forward": "przeskocz do przodu",
|
||||
"viewQueue": "zobacz kolejkę",
|
||||
"playSimilarSongs": "odtwarzaj podobne"
|
||||
"playSimilarSongs": "odtwarzaj podobne",
|
||||
"addLastShuffled": "ostatnie (wylosowane)",
|
||||
"addNextShuffled": "następne (wylosowane)",
|
||||
"queueType": "typ kolejki",
|
||||
"queueType_default": "domyślna",
|
||||
"queueType_priority": "priorytetowa",
|
||||
"holdToShuffle": "przytrzymaj aby odtwarzać losowo",
|
||||
"lyrics": "tekst",
|
||||
"restoreQueueFromServer": "przywróć kolejkę z serwera",
|
||||
"saveQueueToServer": "zapisz kolejkę na serwerze",
|
||||
"artistRadio": "radio wykonawcy",
|
||||
"trackRadio": "radio utworu"
|
||||
},
|
||||
"setting": {
|
||||
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
|
||||
@@ -559,7 +695,6 @@
|
||||
"fontType_description": "wbudowana czcionka pozwala na wybranie czcionki dostarczonej z feishin. systemowa czcionka pozwala na wybranie czcionki dostarczonej przez system operacyjny. niestandardowa czcionka pozwala na wybranie własnej czcionki",
|
||||
"accentColor": "kolor akcentujący",
|
||||
"accentColor_description": "ustaw kolor akcentujący dla aplikacji",
|
||||
"floatingQueueArea": "pokaż pływającą kolejkę podczas najechania kursorem",
|
||||
"hotkey_toggleRepeat": "przełącz powtarzanie",
|
||||
"lyricOffset_description": "opóźnienie tekstu przez podaną liczbę milisekund",
|
||||
"fontType": "typ czcionki",
|
||||
@@ -578,7 +713,6 @@
|
||||
"hotkey_rate0": "wyczyść oceny",
|
||||
"discordApplicationId": "ID aplikacji {{discord}}",
|
||||
"applicationHotkeys_description": "ustaw skróty klawiszowe aplikacji. przełącz pole wyboru aby ustawić skrót globalny (tylko komputery)",
|
||||
"floatingQueueArea_description": "wyświetl ikonę najechania kursorem po prawej stronie ekranu, aby wyświetlić kolejkę odtwarzania",
|
||||
"hotkey_volumeMute": "wycisz",
|
||||
"hotkey_toggleCurrentSongFavorite": "dodaj $t(common.currentSong) do ulubionych",
|
||||
"hotkey_browserBack": "przeglądarka wstecz",
|
||||
@@ -661,21 +795,17 @@
|
||||
"buttonSize": "Rozmiar przycisku paska odtwarzacza",
|
||||
"clearQueryCache": "wyczyść pamięć podręczną feishin",
|
||||
"clearCache_description": "\"twarde wyczyszczenie\" feishin. oprócz wyczyszczenia pamięci podręcznej feishin, opróżnij pamięć podręczną przeglądarki (zapisane obrazy i inne zasoby). dane i ustawienia serwera zostaną zachowane",
|
||||
"clearQueryCache_description": "\"miękkie wyczyszczenie\" feishin. spowoduje to odświeżenie list odtwarzania, metadanych utworów i zresetowanie zapisanych tekstów. ustawienia, dane uwierzytelniające serwera i obrazy w pamięci podręcznej zostaną zachowane",
|
||||
"clearQueryCache_description": "\"miękkie wyczyszczenie\" feishin. spowoduje to odświeżenie playlist, metadanych utworów i zresetowanie zapisanych tekstów. ustawienia, dane uwierzytelniające serwera i obrazy w pamięci podręcznej zostaną zachowane",
|
||||
"buttonSize_description": "rozmiar przycisków paska odtwarzacza",
|
||||
"clearCache": "wyczyść pamięć podręczną przeglądarki",
|
||||
"playerAlbumArtResolution": "rozdzielczość okładki albumu odtwarzacza",
|
||||
"externalLinks": "pokaż zewnętrzne linki",
|
||||
"genreBehavior_description": "określa, czy kliknięcie gatunku domyślnie otwiera listę utworów czy albumów",
|
||||
"mpvExtraParameters_help": "po jednym na linię",
|
||||
"passwordStore": "hasła",
|
||||
"passwordStore_description": "jakie hasło ma być używane. zmień to, jeśli masz problemy z przechowywaniem haseł",
|
||||
"playerAlbumArtResolution_description": "rozdzielczość podglądu okładki albumu w dużym odtwarzaczu. większa sprawia, że wygląda bardziej wyraziście, ale może spowolnić ładowanie. domyślnie 0, czyli auto",
|
||||
"startMinimized": "uruchom zminimalizowany",
|
||||
"startMinimized_description": "uruchom aplikację w zasobniku systemowym",
|
||||
"clearCacheSuccess": "pamięć podręczna została wyczyszczona pomyślnie",
|
||||
"genreBehavior": "domyślne zachowanie strony gatunek",
|
||||
"externalLinks_description": "umożliwia wyświetlanie linków zewnętrznych (Last.fm, MusicBrainz) na stronach artystów/albumów",
|
||||
"externalLinks_description": "umożliwia wyświetlanie linków zewnętrznych (Last.fm, MusicBrainz) na stronach wykonawców/albumów",
|
||||
"homeConfiguration": "konfiguracja strony głównej",
|
||||
"homeConfiguration_description": "konfiguracja elementów wyświetlanych na stronie głównej i ich kolejności",
|
||||
"albumBackground_description": "dodaje obraz tła dla stron albumu zawierających grafikę albumu",
|
||||
@@ -697,7 +827,6 @@
|
||||
"customCssNotice": "Ostrzeżenie: chociaż istnieje pewne filtrowanie (uniemożliwia używanie url() i content:), używanie niestandardowego css-a może stwarzać ryzyko przez zmiany w interfejsie",
|
||||
"customCss_description": "zawartość niestandardowego css. Uwaga: content i zdalne url są niedozwolonymi właściwościami. Podgląd twojej zawartości jest pokazana poniżej. Dodatkowe pola których nie ustawiłeś, są obecne z powodu sanityzacji",
|
||||
"customCss": "niestandardowy css",
|
||||
"doubleClickBehavior": "zakolejkuj wszystkie wyszukane utwory gdy podwójnie kliknięto",
|
||||
"trayEnabled_description": "pokaż/ukryj ikonę/menu w zasobniku. jeżeli wyłączone, wyłącza też minimalizowanie.wyjście do zasobnika",
|
||||
"webAudio_description": "używaj web audio. włącza to zaawansowane funkcje takie jak replaygain. wyłącz jeżeli nie działa poprawnie",
|
||||
"artistConfiguration": "konfiguracja strony albumu wykonawcy",
|
||||
@@ -717,7 +846,6 @@
|
||||
"trayEnabled": "pokazuj w zasobniku",
|
||||
"webAudio": "używaj web audio",
|
||||
"homeFeature_description": "ustawienie powoduje to czy wyświetlana jest karuzela z polecanymi utworami na stronie głównej",
|
||||
"doubleClickBehavior_description": "jeżeli włączone, wszystkie pasujące utwory w wyszukiwaniu zostaną zakolejkowane. w przeciwnym wypadku, tylko kliknięty będzie zakolejkowany",
|
||||
"lastfmApiKey": "klucz API {{lastfm}}",
|
||||
"lastfmApiKey_description": "klucz API dla {{lastfm}}. wymagany dla okładek",
|
||||
"translationTargetLanguage": "docelowy język tłumaczenia",
|
||||
@@ -725,19 +853,115 @@
|
||||
"preferLocalLyrics": "preferuj lokalne teksty",
|
||||
"preferLocalLyrics_description": "jeśli to możliwe, preferuj lokalne teksty zamiast tekstów zdalnych",
|
||||
"lastfm": "pokazuj linki do last.fm",
|
||||
"lastfm_description": "pokazuj linki do Last.fm na stronach artystów/albumów",
|
||||
"lastfm_description": "pokazuj linki do Last.fm na stronach wykonawców/albumów",
|
||||
"musicbrainz": "pokazuj linki do MusicBrainz",
|
||||
"musicbrainz_description": "pokazuj linki do MusicBrainz na stronach artystów/albumów, gdzie istnieje MusicBrainz ID",
|
||||
"musicbrainz_description": "pokazuj linki do MusicBrainz na stronach wykonawców/albumów, gdzie istnieje MusicBrainz ID",
|
||||
"discordPausedStatus": "pokaż status podczas pauzy",
|
||||
"discordServeImage": "wysyłaj obrazy dla {{discord}} z serwera",
|
||||
"discordServeImage_description": "pokazuj okładki w statusie {{discord}} prosto z serwera, dostępne tylko dla Jellyfin i Navidrome"
|
||||
"discordServeImage_description": "pokazuj okładki w statusie {{discord}} prosto z serwera, dostępne tylko dla Jellyfin i Navidrome. {{discord}} używa bota do pobierania obrazów, więc twój serwer musi być dostępny publicznie w internecie",
|
||||
"analyticsDisable": "Zrezygnuj z analityki bazowanej na użytkowaniu",
|
||||
"analyticsDisable_description": "Zanonymizowane dane użytkowania są wysyłane do dewelopera w celu poprawienia aplikacji",
|
||||
"artistBackground": "obraz tła wykonawcy",
|
||||
"artistBackground_description": "dodaje obraz tła do stron wykonawców",
|
||||
"artistBackgroundBlur": "rozmiar rozmazania obrazu tła wykonawców",
|
||||
"artistBackgroundBlur_description": "wybiera poziom rozmazania obrazów tła wykonawców",
|
||||
"crossfadeStyle": "styl przenikania dźwięku",
|
||||
"releaseChannel_optionBeta": "beta",
|
||||
"releaseChannel_optionLatest": "najnowsza",
|
||||
"releaseChannel": "kanał wydań",
|
||||
"releaseChannel_description": "wybieraj pomiędzy stabilnymi wydaniami a wydaniami beta dla automatycznych aktualizacji",
|
||||
"discordDisplayType_artistname": "nazwa(y) wykonawców",
|
||||
"discordDisplayType_description": "zmienia co jest pokazywane jako słuchane w twoim statusie",
|
||||
"discordDisplayType_songname": "nazwa piosenki",
|
||||
"discordDisplayType": "typ wyświetlania statusu {{discord}}",
|
||||
"discordLinkType_description": "dodaje zewnętrzny link do {{lastfm}} lub {{musicbrainz}} do pól piosenki i wykonawcy w rich presence {{discord}}. {{musicbrainz}} jest najbardziej dokładnym, ale wymaga tagów i nie daje linków do wykonawców, gdy {{lastfm}} zawsze daje link. nie wywołuje dodatkowych żądań sieciowych",
|
||||
"discordLinkType_mbz_lastfm": "{{musicbrainz}} z zastępczym {{lastfm}}",
|
||||
"discordLinkType_none": "$t(common.none)",
|
||||
"discordLinkType": "linki w statusie {{discord}}",
|
||||
"discordRichPresence": "{{discord}} rich presence",
|
||||
"enableAutoTranslation_description": "włącza automatyczne tłumaczenie tekstów, kiedy są ładowane",
|
||||
"enableAutoTranslation": "włącz automatyczne tłumaczenie",
|
||||
"exportImportSettings_control_description": "eksportuj i importuj ustawienia za pomocą pliku JSON",
|
||||
"exportImportSettings_control_exportText": "eksportuj ustawienia",
|
||||
"exportImportSettings_control_importText": "importuj ustawienia",
|
||||
"exportImportSettings_control_title": "import / eksport ustawień",
|
||||
"exportImportSettings_destructiveWarning": "importowanie ustawień jest destrukcyjne, sprawdź te powyższe przed kliknięciem \"importuj\" poniżej!",
|
||||
"exportImportSettings_importBtn": "importuj ustawienia",
|
||||
"exportImportSettings_importModalTitle": "importuj ustawienia feishin",
|
||||
"exportImportSettings_importSuccess": "ustawienia zostały zaimportowane pomyślnie!",
|
||||
"exportImportSettings_notValidJSON": "podany plik nie jest właściwym plikiem JSON",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" jest nieprawidłowy - {{reason}}",
|
||||
"hotkey_navigateHome": "przejdź do strony głównej",
|
||||
"language": "język",
|
||||
"neteaseTranslation_description": "Gdy włączone, pobiera i wyświetla teksty przetłumaczone od NetEase, gdy dostępne",
|
||||
"neteaseTranslation": "Włącz tłumaczenia NetEase",
|
||||
"notify": "włącz powiadomienia piosenek",
|
||||
"notify_description": "pokazuje powiadomienie, gdy aktualna piosenka jest zmieniana",
|
||||
"playerbarSlider": "pasek suwaka odtwarzacza",
|
||||
"playerbarSliderType_optionSlider": "suwak",
|
||||
"playerbarSliderType_optionWaveform": "przebieg",
|
||||
"playerbarWaveformAlign": "wyrównanie przebiegu",
|
||||
"playerbarWaveformAlign_optionTop": "góra",
|
||||
"playerbarWaveformAlign_optionCenter": "środek",
|
||||
"playerbarWaveformAlign_optionBottom": "dół",
|
||||
"playerbarWaveformBarWidth": "szerokość paska przebiegu",
|
||||
"playerbarWaveformGap": "przerwa przebiegu",
|
||||
"playerbarWaveformRadius": "promień przebiegu",
|
||||
"showLyricsInSidebar_description": "będzie dodany panel do kolejki odtwarzania, który będzie wyświetlał tekst",
|
||||
"showLyricsInSidebar": "pokazuj tekst w bocznym pasku odtwarzacza",
|
||||
"showVisualizerInSidebar_description": "będzie dodany panel w bocznym pasku odtwarzacza, który będzie wyświetlał wizualizację",
|
||||
"showVisualizerInSidebar": "pokazuj wizualizację w bocznym pasku odtwarzacza",
|
||||
"preservePitch_description": "utrzymuje ton gdy zmieniana jest prędkość odtwarzania",
|
||||
"preservePitch": "utrzymuj ton",
|
||||
"preventSleepOnPlayback_description": "powstrzymuje ekran przed uśpieniem, gdy muzyka jest odtwarzana",
|
||||
"preventSleepOnPlayback": "powstrzymuj uśpienie podczas odtwarzania",
|
||||
"mediaSession_description": "włącza integrację z Windows Media Session, wyświetlając sterowanie mediami i metadane w systemowym oknie zmiany głośności i na ekranie blokady (tylko Windows)",
|
||||
"mediaSession": "włącz media session",
|
||||
"transcode": "włącz transkodowanie",
|
||||
"queryBuilder": "kreator zaptań",
|
||||
"queryBuilderCustomFields_inputLabel": "label",
|
||||
"queryBuilderCustomFields_inputTag": "tag",
|
||||
"queryBuilderCustomFields": "niestandardowe pola",
|
||||
"queryBuilderCustomFields_description": "dodaj niestandardowe pola do użycia w kreatorach zapytań",
|
||||
"followCurrentSong_description": "automatycznie przewija kolejkę odtwarzania do aktualnie odtwarzanej piosenki",
|
||||
"followCurrentSong": "śledź aktualną piosenkę",
|
||||
"playerFilters": "Filtruj piosenki z kolejki",
|
||||
"playerFilters_description": "nie dodawaj piosenek do kolejki na podstawie poniższych kryteriów",
|
||||
"playerbarSlider_description": "krzywe nie są zalecane w przypadku wolnego lub ograniczonego połączenia internetowego",
|
||||
"audioFadeOnStatusChange": "przenikanie dźwięku przy zmianie statusu",
|
||||
"audioFadeOnStatusChange_description": "umożliwia zanikanie lub pojawianie się dźwięku gdy zmieni się status play/pauza",
|
||||
"autoDJ": "automatyczny DJ",
|
||||
"autoDJ_description": "automatycznie dodawaj podobne piosenki do kolejki",
|
||||
"autoDJ_itemCount": "liczba elementów",
|
||||
"autoDJ_itemCount_description": "liczba elementów, które będzie próbować dodać do kolejki kiedy automatyczny DJ jest włączony",
|
||||
"autoDJ_timing": "czas dodawania",
|
||||
"autoDJ_timing_description": "ilość piosenek pozostałych w kolejce przed tym gdy zostanie włączony automatyczny DJ",
|
||||
"logLevel": "poziom logów",
|
||||
"logLevel_description": "ustawia minimalny poziom logów do wyświetlenia. debugowanie wyświetla wszystkie logi błędy wyświetla tylko błędy",
|
||||
"logLevel_optionDebug": "debugowanie",
|
||||
"logLevel_optionError": "błędy",
|
||||
"logLevel_optionInfo": "info",
|
||||
"logLevel_optionWarn": "ostrzeżenia",
|
||||
"useThemeAccentColor": "używaj koloru akcentu motywu",
|
||||
"useThemeAccentColor_description": "używaj głównego koloru ustawionego w wybranym motywie zamiast niestandardowego koloru akcentu",
|
||||
"artistRadioCount_description": "ustawia liczbę piosenek do załadowania dla radia wykonawcy i radia utworu",
|
||||
"artistRadioCount": "liczba radio wykonawców/utworów",
|
||||
"imageResolution": "rozdzielczość obrazu",
|
||||
"imageResolution_description": "rozdzielczość dla obrazów używanych w programie. użycie wartości 0 ustawi rozdzielczość na natywną",
|
||||
"imageResolution_optionTable": "tabela",
|
||||
"imageResolution_optionItemCard": "karta elementu",
|
||||
"imageResolution_optionSidebar": "pasek boczny",
|
||||
"imageResolution_optionHeader": "nagłówek",
|
||||
"imageResolution_optionFullScreenPlayer": "odtwarzacz pełnoekranowy",
|
||||
"combinedLyricsAndVisualizer_description": "połącz tekst i wizualizacje w tym samym panelu",
|
||||
"combinedLyricsAndVisualizer": "połącz tekst i wizualizacje w pasku bocznym odtwarzacza"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
"view": {
|
||||
"card": "karta",
|
||||
"table": "tabela",
|
||||
"poster": "plakat"
|
||||
"grid": "siatka",
|
||||
"list": "lista"
|
||||
},
|
||||
"general": {
|
||||
"displayType": "typ wyświetlania",
|
||||
@@ -747,7 +971,28 @@
|
||||
"size": "$t(common.size)",
|
||||
"itemSize": "rozmiar elementu (px)",
|
||||
"itemGap": "odstęp między elementami (px)",
|
||||
"followCurrentSong": "śledź aktualną piosenkę"
|
||||
"followCurrentSong": "śledź aktualną piosenkę",
|
||||
"advancedSettings": "zaawansowane ustawienia",
|
||||
"autosize": "rozmiar automatyczny",
|
||||
"moveUp": "przesuń w górę",
|
||||
"moveDown": "przesuń w dół",
|
||||
"pinToLeft": "przypnij po lewej",
|
||||
"pinToRight": "przypnij po prawej",
|
||||
"alignLeft": "wyrównaj do lewej",
|
||||
"alignCenter": "wyrównaj do środka",
|
||||
"alignRight": "wyrównaj do prawej",
|
||||
"itemsPerRow": "elementów na wiersz",
|
||||
"size_default": "domyślny",
|
||||
"size_compact": "kompaktowy",
|
||||
"size_large": "duży",
|
||||
"pagination": "numerowanie",
|
||||
"pagination_itemsPerPage": "elementów na stronę",
|
||||
"pagination_infinite": "nieskończone",
|
||||
"pagination_paginate": "numerowane",
|
||||
"alternateRowColors": "naprzemienne kolory wierszy",
|
||||
"horizontalBorders": "obwódki wierszy",
|
||||
"rowHoverHighlight": "podświetlanie wierszy po najechaniu",
|
||||
"verticalBorders": "obwódki kolumn"
|
||||
},
|
||||
"label": {
|
||||
"releaseDate": "data premiery",
|
||||
@@ -777,7 +1022,12 @@
|
||||
"year": "$t(common.year)",
|
||||
"albumArtist": "$t(entity.albumArtist_one)",
|
||||
"codec": "$t(common.codec)",
|
||||
"songCount": "$t(entity.track_other)"
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"albumCount": "$t(entity.album_other)",
|
||||
"genreBadge": "$t(entity.genre_one) (znaczki)",
|
||||
"image": "obraz",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
}
|
||||
},
|
||||
"column": {
|
||||
@@ -799,12 +1049,216 @@
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"trackNumber": "utwór",
|
||||
"genre": "$t(entity.genre_one)",
|
||||
"albumArtist": "artysta albumu",
|
||||
"albumArtist": "wykonawca albumu",
|
||||
"path": "ścieżka",
|
||||
"discNumber": "płyta",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"size": "$t(common.size)",
|
||||
"codec": "$t(common.codec)"
|
||||
"codec": "$t(common.codec)",
|
||||
"owner": "właściciel",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
}
|
||||
},
|
||||
"queryBuilder": {
|
||||
"standardTags": "tagi standardowe",
|
||||
"customTags": "tagi niestandardowe"
|
||||
},
|
||||
"releaseType": {
|
||||
"primary": {
|
||||
"album": "$t(entity.album_one)",
|
||||
"broadcast": "broadcast",
|
||||
"ep": "ep",
|
||||
"other": "inne",
|
||||
"single": "single"
|
||||
},
|
||||
"secondary": {
|
||||
"audiobook": "audiobook",
|
||||
"audioDrama": "audio drama",
|
||||
"compilation": "kompilacja",
|
||||
"djMix": "mix dj",
|
||||
"demo": "demo",
|
||||
"fieldRecording": "nagranie w terenie",
|
||||
"interview": "wywiad",
|
||||
"live": "na żywo",
|
||||
"mixtape": "mixtape",
|
||||
"remix": "remix",
|
||||
"soundtrack": "ścieżka dźwiękowa",
|
||||
"spokenWord": "słowo mówione"
|
||||
}
|
||||
},
|
||||
"dragDropZone": {
|
||||
"error_oneFileOnly": "Wybierz tylko 1 plik",
|
||||
"error_readingFile": "wystąpił problem z odczytaniem pliku: {{errorMessage}}",
|
||||
"mainText": "upuść plik tutaj"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "jest po",
|
||||
"afterDate": "jest po (dacie)",
|
||||
"before": "jest przed",
|
||||
"beforeDate": "jest przed (datą)",
|
||||
"contains": "zawiera",
|
||||
"endsWith": "kończy się na",
|
||||
"inPlaylist": "jest w",
|
||||
"inTheLast": "jest w ostatnim",
|
||||
"inTheRange": "jest w zakresie",
|
||||
"inTheRangeDate": "jest w zakresie (dat)",
|
||||
"is": "jest",
|
||||
"isNot": "nie jest",
|
||||
"isGreaterThan": "jest większe od",
|
||||
"isLessThan": "jest mniejsze od",
|
||||
"matchesRegex": "pasuje do wyrażenia regularnego (regex)",
|
||||
"notContains": "nie zawiera",
|
||||
"notInPlaylist": "nie jest w",
|
||||
"notInTheLast": "nie jest w ostatnim",
|
||||
"startsWith": "zaczyna się od"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "min",
|
||||
"secondShort": "sek",
|
||||
"hourShort": "godz",
|
||||
"dayShort": "dzień"
|
||||
},
|
||||
"visualizer": {
|
||||
"visualizerType": "Typ Wizualizacji",
|
||||
"cycleTime": "Czas cyklu (w sekundach)",
|
||||
"copyConfiguration": "Kopiuj Konfigurację",
|
||||
"pasteConfiguration": "Wklej Konfigurację",
|
||||
"pasteConfigurationPlaceholder": "Wklej konfigurację JSON tutaj...",
|
||||
"pasteFromClipboard": "Wklej z schowka",
|
||||
"applyConfiguration": "Zastosuj Konfigurację",
|
||||
"configCopied": "Konfiguracja skopiowana do schowka",
|
||||
"configCopyFailed": "Nie udało się skopiować konfiguracji",
|
||||
"configPasted": "Konfiguracja zastosowana pomyślnie",
|
||||
"configPasteFailed": "Nie udało się zastosować konfiguracji. Sprawdź jej format.",
|
||||
"configPasteReadFailed": "Nie udało się odczytać z schowka",
|
||||
"cyclePresets": "Cykl Ustawień",
|
||||
"includeAllPresets": "Uwzględnij wszystkie Ustawienia",
|
||||
"ignoredPresets": "Ignorowane Ustawienia",
|
||||
"selectedPresets": "Wybrane Ustawienia",
|
||||
"randomizeNextPreset": "Losuj Następne Ustawienie",
|
||||
"blendTime": "Czas Mieszania",
|
||||
"presets": "Ustawienia",
|
||||
"selectPreset": "Wybierz Ustawienie",
|
||||
"applyPreset": "Zastosuj Ustawienie",
|
||||
"saveAsPreset": "Zapisz jako Ustawienie",
|
||||
"updatePreset": "Uaktualnij Ustawienie",
|
||||
"presetName": "Nazwa Ustawienia",
|
||||
"presetNamePlaceholder": "Wpisz nazwę ustawienia",
|
||||
"general": "Ogólne",
|
||||
"mode": "Tryb",
|
||||
"mode1To8": "Tryb 1 - 8",
|
||||
"mode10": "Tryb 10",
|
||||
"barSpace": "Odstęp Pasków",
|
||||
"lineWidth": "Szerokość Linii",
|
||||
"fillAlpha": "Wypełnij Alpha",
|
||||
"channelLayout": "Układ Kanałów",
|
||||
"maxFPS": "Maks FPS",
|
||||
"opacity": "Nieprzezroczystość",
|
||||
"customGradients": "Niestandardowe Gradienty",
|
||||
"addCustomGradient": "Dodaj Niestandardowy Gradient",
|
||||
"gradientName": "Nazwa Gradientu",
|
||||
"gradientNamePlaceholder": "Nazwa Gradientu",
|
||||
"vertical": "Pionowy",
|
||||
"horizontal": "Poziomy",
|
||||
"colorStops": "Kroki Kolorów",
|
||||
"addColor": "Dodaj Kolor",
|
||||
"position": "Pozycja",
|
||||
"level": "Poziom",
|
||||
"remove": "Usuń",
|
||||
"custom": "Niestandardowy",
|
||||
"builtIn": "Wbudowany",
|
||||
"colors": "Kolory",
|
||||
"colorMode": "Tryb Koloru",
|
||||
"gradient": "Gradient",
|
||||
"gradientLeft": "Lewa Gradientu",
|
||||
"gradientRight": "Prawa Gradientu",
|
||||
"fft": "FFT",
|
||||
"fftSize": "Rozmiar FFT",
|
||||
"smoothing": "Wygładzanie",
|
||||
"frequencyRangeAndScaling": "Zakres częstotliwości i skalowanie",
|
||||
"minimumFrequency": "Minimalna Częstotliwość",
|
||||
"maximumFrequency": "Maksymalna Częstotliwość",
|
||||
"frequencyScale": "Skala Częstotliwości",
|
||||
"sensitivity": "Czułość",
|
||||
"weightingFilter": "Filtr Wagi",
|
||||
"minimumDecibels": "Minimum Decybeli",
|
||||
"maximumDecibels": "Maksimum Decybeli",
|
||||
"linearAmplitude": "Amplituda Linearna",
|
||||
"linearBoost": "Podbicie Linearne",
|
||||
"peakBehavior": "Zachowanie Szczytów",
|
||||
"showPeaks": "Pokaż Szczyty",
|
||||
"fadePeaks": "Zanikaj Sczyty",
|
||||
"peakLine": "Linia Szczytów",
|
||||
"gravity": "Grawitacja",
|
||||
"peakFadeTime": "Czas Zanikania Szczytów (ms)",
|
||||
"peakHoldTime": "Czas Utrzymywania Szczytu (ms)",
|
||||
"radialSpectrum": "Spektrum Promieniowe",
|
||||
"radial": "Promieniowe",
|
||||
"radialInvert": "Odwrócenie Promieniowe",
|
||||
"spinSpeed": "Prędkość Obrotu",
|
||||
"radius": "Promień",
|
||||
"reflexMirror": "Lustro refleksyjne",
|
||||
"reflexFit": "Dopasowanie Odbić",
|
||||
"reflexRatio": "Współczynnik Odbić",
|
||||
"reflexAlpha": "Alpha Odbić",
|
||||
"reflexBrightness": "Jasność Odbić",
|
||||
"mirror": "Odbij lustrzanie",
|
||||
"miscellaneousSettings": "Różne Ustawienia",
|
||||
"alphaBars": "Alpha Pasków",
|
||||
"ledBars": "Paski LED",
|
||||
"trueLeds": "Prawdziwe LEDy",
|
||||
"lumiBars": "Paski Lumi",
|
||||
"outlineBars": "Obwódki Pasków",
|
||||
"roundBars": "Zaokrąglone Paski",
|
||||
"lowResolution": "Niska Rozdzielczość",
|
||||
"splitGradient": "Rozdziel Gradient",
|
||||
"showFPS": "Pokaż FPS",
|
||||
"showScaleX": "Pokaż Skalę X",
|
||||
"noteLabels": "Etykiety Nut",
|
||||
"showScaleY": "Pokaż Skalę Y",
|
||||
"options": {
|
||||
"mode": {
|
||||
"bars": "[0] Pasków",
|
||||
"circle": "[1] Kółko",
|
||||
"wave": "[2] Fala",
|
||||
"rainbow": "[3] Tęcza",
|
||||
"rings": "[4] Pierścienie",
|
||||
"mirror": "[5] Lustro",
|
||||
"line": "[6] Linia",
|
||||
"particles": "[7] Cząsteczki",
|
||||
"fullOctave": "[8] Pełna oktawa / 10 pasm",
|
||||
"outlineBars": "[10] Paski z obwódką"
|
||||
},
|
||||
"colorMode": {
|
||||
"gradient": "Gradient",
|
||||
"barIndex": "Indeks-Paska",
|
||||
"barLevel": "Poziom-Paska"
|
||||
},
|
||||
"gradient": {
|
||||
"classic": "Klasyczny",
|
||||
"prism": "Pryzmat",
|
||||
"rainbow": "Tęcza",
|
||||
"steelblue": "Stalowoniebieski",
|
||||
"orangered": "Pomarańczowo-czerwony"
|
||||
},
|
||||
"channelLayout": {
|
||||
"single": "Pojedynczy",
|
||||
"dualCombined": "Podwójne-Połączone",
|
||||
"dualHorizontal": "Podwójne-Poziome",
|
||||
"dualVertical": "Podwójne-Pionowe"
|
||||
},
|
||||
"frequencyScale": {
|
||||
"linear": "Linearne"
|
||||
},
|
||||
"weightingFilter": {
|
||||
"none": "Żadne",
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "C",
|
||||
"d": "D",
|
||||
"z": "Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,16 +259,12 @@
|
||||
"discordLinkType_description": "Adiciona links externos para {{lastfm}} ou {{musicbrainz}} aos campos de música e artista no Rich Presence do {{discord}}. {{musicbrainz}} é o mais preciso, mas requer tags e não fornece links de artistas, enquanto {{lastfm}} deve sempre fornecer um link. Não realiza requisições de rede adicionais",
|
||||
"discordLinkType_none": "$t(common.none)",
|
||||
"discordLinkType_mbz_lastfm": "{{musicbrainz}} com alternativa para {{lastfm}}",
|
||||
"doubleClickBehavior": "Adicionar todas as faixas pesquisadas à fila ao clicar duas vezes",
|
||||
"doubleClickBehavior_description": "Se verdadeiro, todas as faixas correspondentes em uma pesquisa serão adicionadas à fila. Caso contrário, apenas a faixa clicada será adicionada",
|
||||
"enableRemote": "Ativar servidor de controle remoto",
|
||||
"enableRemote_description": "Ativa o servidor de controle remoto para permitir que outros dispositivos controlem o aplicativo",
|
||||
"externalLinks": "Mostrar links externos",
|
||||
"externalLinks_description": "Ativa a exibição de links externos (Last.fm, MusicBrainz) nas páginas de artista/álbum",
|
||||
"exitToTray": "Minimizar para a bandeja",
|
||||
"exitToTray_description": "Fechar o aplicativo para a bandeja do sistema",
|
||||
"floatingQueueArea": "Exibir painel flutuante da fila",
|
||||
"floatingQueueArea_description": "Exibir um ícone flutuante no lado direito da tela para visualizar a fila de reprodução",
|
||||
"followLyric": "Seguir a letra atual",
|
||||
"followLyric_description": "Mover a letra até o ponto atual da música",
|
||||
"preferLocalLyrics": "Preferir letras locais",
|
||||
@@ -283,8 +279,6 @@
|
||||
"gaplessAudio": "Áudio sem intervalos",
|
||||
"gaplessAudio_description": "Define a configuração de áudio sem intervalos para o MPV",
|
||||
"gaplessAudio_optionWeak": "Fraco (recomendado)",
|
||||
"genreBehavior": "Comportamento padrão da página de gênero",
|
||||
"genreBehavior_description": "Determina se ao clicar em um gênero ele é aberto por padrão na lista de faixas ou de álbuns",
|
||||
"globalMediaHotkeys": "Teclas de atalho globais de mídia",
|
||||
"globalMediaHotkeys_description": "Ativar ou desativar o uso das teclas de atalho de mídia do sistema para controlar a reprodução",
|
||||
"homeConfiguration": "Configuração da página inicial",
|
||||
@@ -362,8 +356,6 @@
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playerAlbumArtResolution": "resolução da capa do álbum no reprodutor",
|
||||
"playerAlbumArtResolution_description": "a resolução da pré-visualização da capa do álbum no reprodutor grande. Resoluções maiores deixam a imagem mais nítida, mas podem diminuir a velocidade de carregamento. O padrão é 0, ou seja, automático",
|
||||
"playerbarOpenDrawer": "alternar tela cheia na barra do reprodutor",
|
||||
"playerbarOpenDrawer_description": "permite clicar na barra do reprodutor para abrir o reprodutor em tela cheia",
|
||||
"remotePassword": "Senha do servidor de controle remoto",
|
||||
@@ -389,6 +381,8 @@
|
||||
"savePlayQueue_description": "Salvar a fila de reprodução ao fechar a aplicação e restaurá-la ao abrir a aplicação",
|
||||
"scrobble": "Scrobblar",
|
||||
"scrobble_description": "Scrobblar reproduções para o seu servidor de mídia",
|
||||
"showRatings": "exibir avaliações por estrelas",
|
||||
"showRatings_description": "exibir ou ocultar as avaliações por estrelas",
|
||||
"showSkipButton": "Exibir botões de pular",
|
||||
"showSkipButton_description": "Exibir ou ocultar os botões de pular na barra do reprodutor",
|
||||
"showSkipButtons": "Exibir botões de pular",
|
||||
@@ -490,10 +484,8 @@
|
||||
"tableColumns": "Colunas da tabela"
|
||||
},
|
||||
"view": {
|
||||
"card": "Cartão",
|
||||
"grid": "Grade",
|
||||
"list": "Lista",
|
||||
"poster": "Poster",
|
||||
"table": "Tabela"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -184,9 +184,7 @@
|
||||
"table": {
|
||||
"config": {
|
||||
"view": {
|
||||
"card": "карточки",
|
||||
"table": "таблица",
|
||||
"poster": "постер"
|
||||
"table": "таблица"
|
||||
},
|
||||
"general": {
|
||||
"displayType": "тип отображения",
|
||||
@@ -636,7 +634,6 @@
|
||||
"replayGainMode_optionTrack": "$t(entity.track_one)",
|
||||
"clearQueryCache_description": "так называемая \"мягкая очистка\" feishin: обновляются плейлисты, метаданные треков, но сохранённые тексты треков сбрасываются. настройки, учётные данные и кэшированные изображения сохраняются",
|
||||
"hotkey_favoriteCurrentSong": "добавить $t(common.currentSong) в избранное",
|
||||
"genreBehavior": "поведения страницы жанров",
|
||||
"globalMediaHotkeys": "глобальные мультимедийные горячие клавиши",
|
||||
"hotkey_browserForward": "кнопка браузера \"вперёд\"",
|
||||
"hotkey_favoritePreviousSong": "добавить $t(common.previousSong) в избранное",
|
||||
@@ -673,7 +670,6 @@
|
||||
"playButtonBehavior": "поведение кнопки воспроизведения",
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"playerAlbumArtResolution_description": "разрешение большой версии обложки альбома в проигрывателе. при большем разрешении она выглядит более четкой, но может замедлить загрузку. по умолчанию равно 0 - устанавливает разрешение автоматически",
|
||||
"playerbarOpenDrawer": "полноэкранный переключатель по панели проигрывателя",
|
||||
"playerbarOpenDrawer_description": "позволяет перейти в полноэкранный режим воспроизведения нажатием на панель проигрывателя",
|
||||
"remotePort": "порт сервера удалённого управления",
|
||||
@@ -705,8 +701,6 @@
|
||||
"useSystemTheme_description": "использует тему, заданную в системе (светлую/тёмную)",
|
||||
"zoom": "процент масштабирования",
|
||||
"zoom_description": "устанавливает процент масштабирования приложения",
|
||||
"floatingQueueArea": "показать область наведения для всплывающей очереди",
|
||||
"genreBehavior_description": "определяет, что отобразится при открытии на жанр — список треков или альбомов",
|
||||
"globalMediaHotkeys_description": "включить или отключить использование системных мультимедийных горячих клавиш для управления воспроизведением",
|
||||
"homeConfiguration_description": "позволяет настроить видимость и порядок элементов на домашней странице",
|
||||
"homeFeature": "улучшенная карусель на главной",
|
||||
@@ -716,7 +710,6 @@
|
||||
"imageAspectRatio_description": "если эта опция включена, обложки будут отображаться в соответствии с их собственным соотношением сторон. для обложек не 1:1 оставшееся пространство будет пустым",
|
||||
"minimumScrobblePercentage": "минимальное время для скробблинга (в процентах)",
|
||||
"playbackStyle": "стиль воспроизведения",
|
||||
"playerAlbumArtResolution": "разрешение обложки альбома",
|
||||
"remotePassword_description": "задает пароль для сервера удалённого управления. По умолчанию эти учетные данные передаются небезопасным способом, поэтому следует использовать уникальный пароль, который вам неважен",
|
||||
"replayGainClipping_description": "Предотвращение клиппинга, вызванного {{ReplayGain}}, путём автоматического снижения усиления",
|
||||
"replayGainFallback_description": "усиление в db для применения, если у файла нет тегов {{ReplayGain}}",
|
||||
@@ -742,7 +735,6 @@
|
||||
"customFontPath": "путь к пользовательскому шрифту",
|
||||
"customFontPath_description": "укажите путь к пользовательскому шрифту, который будет использоваться в приложении",
|
||||
"externalLinks_description": "включает отображение внешних ссылок (Last.fm, MusicBrainz) на страницах альбомов и артистов",
|
||||
"floatingQueueArea_description": "включить отображение иконки наведения на правой части экрана, чтобы показать очередь воспроизведения",
|
||||
"followLyric_description": "прокручивать текст трека до текущей позиции воспроизведения",
|
||||
"language_description": "устанавливает язык приложения ($t(common.restartRequired))",
|
||||
"lyricFetch_description": "получать тексты треков из различных интернет-источников",
|
||||
@@ -770,8 +762,6 @@
|
||||
"discordIdleStatus_description": "если включено, то обновляет статус, когда пользователь бездействует",
|
||||
"discordUpdateInterval": "интервал обновления статуса профиля {{discord}}",
|
||||
"discordUpdateInterval_description": "время в секундах между каждым обновлением (минимум 15 секунд)",
|
||||
"doubleClickBehavior": "добавить в очередь все найденные треки при двойном клике",
|
||||
"doubleClickBehavior_description": "есть включено: все найденные в поиске треки будут добавлены в очередь при двойном клике (иначе - только выбранный)",
|
||||
"lyricOffset_description": "Смещение появления текста треков на указанное количество миллисекунд",
|
||||
"skipPlaylistPage": "пропускать страницу плейлиста",
|
||||
"applicationHotkeys_description": "настройка горячих клавиш приложения. поставьте галочку, чтобы сделать горячую клавишу глобальной (только для ПК)",
|
||||
|
||||
@@ -557,16 +557,12 @@
|
||||
"discordDisplayType_description": "mení vo vašom statuse info, čo počúvate",
|
||||
"discordDisplayType_songname": "názov skladby",
|
||||
"discordDisplayType_artistname": "názov interpreta(-ov)",
|
||||
"doubleClickBehavior": "po dvojkliku zaradí do fronty všetky vyhľadané skladby",
|
||||
"doubleClickBehavior_description": "ak je povolené, všetky nájdené skladby budú zaradené do fronty. inak budú skladby zaradené iba po kliknutí",
|
||||
"enableRemote": "povoliť vzdialené ovládanie servera",
|
||||
"enableRemote_description": "pomocou vzdialeného servera umožňuje ovládanie aplikácie prostredníctvom iných zariadení",
|
||||
"externalLinks": "zobraziť externé odkazy",
|
||||
"externalLinks_description": "umožňuje zobrazovať externé odkazy (Last.fm, MusicBrainz) na stránkach umelca/albumu",
|
||||
"exitToTray": "ukončiť do lišty",
|
||||
"exitToTray_description": "po zavretí sa aplikácia minimalizuje do lišty a beží ďalej",
|
||||
"floatingQueueArea": "zobraziť ikonu výsuvnej fronty prehrávania",
|
||||
"floatingQueueArea_description": "zobraziť ikonu výsuvnej fronty prehrávania na pravej strane obrazovky",
|
||||
"followLyric": "nasleduj aktuálny text skladby",
|
||||
"followLyric_description": "posunúť sa v texte skladby na aktuálne prehrávanú pozíciu",
|
||||
"preferLocalLyrics": "uprednostniť lokálne texty skladieb",
|
||||
@@ -581,8 +577,6 @@
|
||||
"gaplessAudio": "prehrávanie bez prerušení",
|
||||
"gaplessAudio_description": "nastaví prehrávanie bez prerušení pre mpv",
|
||||
"gaplessAudio_optionWeak": "slabo (odporúčané)",
|
||||
"genreBehavior": "predvolené správanie stránky žánru",
|
||||
"genreBehavior_description": "určuje, či kliknutie na žáner otvorí zoznam skladieb alebo zoznam albumov",
|
||||
"globalMediaHotkeys": "globálne klávesové skratky médií",
|
||||
"globalMediaHotkeys_description": "povoliť alebo zakázať použitie vašich klávesových skratiek médií na ovládanie prehrávania",
|
||||
"homeConfiguration": "konfigurácia domovskej stránky",
|
||||
@@ -660,8 +654,6 @@
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playerAlbumArtResolution": "rozlíšenie obrázka albumu",
|
||||
"playerAlbumArtResolution_description": "rozlíšenie zobrazenia náhľadu veľkých obrázkov albumov. pri väčšom rozlíšení budú krajšie, ale môže sa spomaliť ich načítavanie. predvolené je 0, čo znamená automatické",
|
||||
"playerbarOpenDrawer": "zobrazenie na celú obrazovku panelom prehrávača",
|
||||
"playerbarOpenDrawer_description": "umožní kliknutím na panel prehrávača prepnúť zobrazenie prehrávača na celú obrazovku",
|
||||
"remotePassword": "heslo servera vzdialeného ovládania",
|
||||
@@ -812,10 +804,8 @@
|
||||
"year": "$t(common.year)"
|
||||
},
|
||||
"view": {
|
||||
"card": "karta",
|
||||
"grid": "mriežka",
|
||||
"list": "zoznam",
|
||||
"poster": "plagát",
|
||||
"table": "tabuľka"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,16 +560,12 @@
|
||||
"discordServeImage_description": "deli naslovne slike za {{discord}} bogato prisotnost iz samega strežnika, na voljo samo za Jellyfin in Navidrome",
|
||||
"discordUpdateInterval": "interval posodabljanja {{discord}} bogate prezence",
|
||||
"discordUpdateInterval_description": "čas v sekundah med posameznimi posodobitvami (najmanj 15 sekund)",
|
||||
"doubleClickBehavior": "dvojni klik doda vse iskane skladbe v čakalno vrsto",
|
||||
"doubleClickBehavior_description": "če je nastavitev vklopljena se bodo v čakalno vrsto dodale vse skladbe, ki ustrezajo iskanju. v nasprotnem primeru se v čakalno vrsto doda samo izbrana skladba",
|
||||
"enableRemote": "omogoči oddaljeno upravljanje strežnika",
|
||||
"enableRemote_description": "omogoči oddaljeno nadzorovanje strežnika in s tem dovoli drugim napravam da upravljajo aplikacijo",
|
||||
"externalLinks": "prikaži zunanje povezave",
|
||||
"externalLinks_description": "omogoči prikaz zunanjih povezav (Last.fm, MusicBrainz) na straneh albumov,izvajalcev",
|
||||
"exitToTray": "minimiziraj",
|
||||
"exitToTray_description": "ob izhodu se aplikacija minimizira v opravilno vrstico",
|
||||
"floatingQueueArea": "prikaži območje plavajoče čakalne vrste",
|
||||
"floatingQueueArea_description": "na desni strani zaslona prikažite ikono za ogled čakalne vrste predvajanja",
|
||||
"followLyric": "sledenje besedilu",
|
||||
"followLyric_description": "pomaknite besedilo pesmi do trenutnega položaja predvajanja",
|
||||
"preferLocalLyrics": "prioritiziraj lokalna besedila",
|
||||
@@ -584,8 +580,6 @@
|
||||
"gaplessAudio": "neprekinjen avdio",
|
||||
"gaplessAudio_description": "nastavi neprekinjen avdio za mpv",
|
||||
"gaplessAudio_optionWeak": "šibko (priporočeno)",
|
||||
"genreBehavior": "privzeto vedenje strani z zvrstmi",
|
||||
"genreBehavior_description": "določa, ali se ob kliku na zvrst privzeto odpre seznam skladb ali albumov",
|
||||
"globalMediaHotkeys": "globalne bližnjične tipke za vsebino",
|
||||
"globalMediaHotkeys_description": "omogočite ali onemogočite uporabo bližnjic za sistemske medije za nadzor predvajanja",
|
||||
"homeConfiguration": "konfiguracija domače strani",
|
||||
|
||||
@@ -142,7 +142,6 @@
|
||||
"replayGainMode": "{{ReplayGain}} režim",
|
||||
"playbackStyle_optionNormal": "normalno",
|
||||
"windowBarStyle": "stil trake prozora",
|
||||
"floatingQueueArea": "prikaži područje plutajuće liste za reprodukciju",
|
||||
"replayGainFallback_description": "jačina u dB koja će se primeniti ako datoteka nema {{ReplayGain}} oznake",
|
||||
"replayGainPreamp_description": "prilagođava pojačalo za {{ReplayGain}} vrednosti",
|
||||
"hotkey_toggleRepeat": "promeni ponavljanje",
|
||||
@@ -168,7 +167,6 @@
|
||||
"hotkey_rate0": "obrisati ocenu",
|
||||
"discordApplicationId": "{{discord}} ID aplikacije",
|
||||
"applicationHotkeys_description": "konfiguriši prečice za aplikaciju. uključite opciju za postavljanje kao globalne prečice (samo na radnoj površini)",
|
||||
"floatingQueueArea_description": "prikaz ikone na desnoj strani ekrana za pregled liste za reprodukciju",
|
||||
"hotkey_volumeMute": "isključi zvuk",
|
||||
"hotkey_toggleCurrentSongFavorite": "promeni omiljenu pesmu $t(common.currentSong)",
|
||||
"remoteUsername": "korisničko ime za daljinsku kontrolu servera",
|
||||
@@ -297,9 +295,7 @@
|
||||
"table": {
|
||||
"config": {
|
||||
"view": {
|
||||
"card": "kartica",
|
||||
"table": "tabela",
|
||||
"poster": "poster"
|
||||
"table": "tabela"
|
||||
},
|
||||
"general": {
|
||||
"displayType": "tip prikaza",
|
||||
|
||||
+145
-10
@@ -16,7 +16,28 @@
|
||||
"moveToBottom": "flytta till botten",
|
||||
"setRating": "sätt betyg",
|
||||
"toggleSmartPlaylistEditor": "växla $t(entity.smartPlaylist) redigerare",
|
||||
"removeFromFavorites": "ta bort från $t(entity.favorite_other)"
|
||||
"removeFromFavorites": "ta bort från $t(entity.favorite_other)",
|
||||
"downloadStarted": "startade nedladdning av {{count}} objekt",
|
||||
"moveToNext": "flytta till nästa",
|
||||
"moveUp": "flytta upp",
|
||||
"moveDown": "flytta ner",
|
||||
"holdToMoveToTop": "håll för att flytta till toppen",
|
||||
"holdToMoveToBottom": "håll för att flytta till botten",
|
||||
"moveItems": "flytta objekt",
|
||||
"shuffle": "slumpa",
|
||||
"shuffleAll": "slumpa alla",
|
||||
"shuffleSelected": "slumpa valda",
|
||||
"viewMore": "visa mer",
|
||||
"openIn": {
|
||||
"lastfm": "Öppna i Last.fm",
|
||||
"musicbrainz": "Öppna i MusicBrainz"
|
||||
},
|
||||
"createRadioStation": "skapa $t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "ta bort $t(entity.radioStation_one)",
|
||||
"addOrRemoveFromSelection": "lägg till eller ta bort från markerade",
|
||||
"selectRangeOfItems": "välj en mängd objekt",
|
||||
"selectAll": "markera alla",
|
||||
"openApplicationDirectory": "öppna applikationskatalog"
|
||||
},
|
||||
"common": {
|
||||
"backward": "bakåt",
|
||||
@@ -96,7 +117,38 @@
|
||||
"size": "storlek",
|
||||
"biography": "biografi",
|
||||
"note": "anteckning",
|
||||
"center": "center"
|
||||
"center": "center",
|
||||
"explicitStatus": "olämplig status",
|
||||
"additionalParticipants": "ytterligare medverkare",
|
||||
"newVersion": "en ny version har installerats {{version}}",
|
||||
"viewReleaseNotes": "se utgåveinformation",
|
||||
"bitDepth": "bitdjup",
|
||||
"close": "stäng",
|
||||
"codec": "kodek",
|
||||
"doNotShowAgain": "visa inte detta igen",
|
||||
"view": "visa",
|
||||
"externalLinks": "externa länkar",
|
||||
"faster": "snabbare",
|
||||
"mbid": "MusicBrainz ID",
|
||||
"noFilters": "inga filter konfigurerade",
|
||||
"preview": "förhandsvisa",
|
||||
"private": "privat",
|
||||
"public": "allmän",
|
||||
"recordLabel": "skivbolag",
|
||||
"releaseType": "utgåvetyp",
|
||||
"reload": "ladda om",
|
||||
"sampleRate": "samplingstakt",
|
||||
"slower": "långsammare",
|
||||
"share": "dela",
|
||||
"sort": "sortera",
|
||||
"tags": "taggar",
|
||||
"translation": "översättning",
|
||||
"explicit": "olämplig",
|
||||
"clean": "städad",
|
||||
"gridRows": "rutnätsrader",
|
||||
"tableColumns": "tabellkolumner",
|
||||
"itemsMore": "{{count}} fler",
|
||||
"countSelected": "{{count}} markerade"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "starta om servern för att tillämpa den nya porten",
|
||||
@@ -117,7 +169,14 @@
|
||||
"mpvRequired": "MPV krävs",
|
||||
"audioDeviceFetchError": "ett fel uppstod vid hämtning av ljudenheter",
|
||||
"invalidServer": "ogiltig server",
|
||||
"loginRateError": "för många inloggningsförsök, försök igen om några sekunder"
|
||||
"loginRateError": "för många inloggningsförsök, försök igen om några sekunder",
|
||||
"badAlbum": "du ser denna sidan eftersom denna låten inte är en del av ett album. du ser troligtvis detta problemet för att du har en låt på toppnivån i din musikmapp. Jellyfin grupperar bara låtar om de finns i en mapp",
|
||||
"badValue": "felaktigt alternativ \"{{value}}\". detta värde existerar inte längre",
|
||||
"multipleServerSaveQueueError": "spelningskön har en eller flera låtar som inte är från den nuvarande valda servern. detta är inte stöttat",
|
||||
"networkError": "en nätverksfel uppstod",
|
||||
"notificationDenied": "åtkomst till notifieringarna var nekad. inställningen har ingen verkan",
|
||||
"openError": "kunde inte öppna filen",
|
||||
"settingsSyncError": "diskrepans hittades mellan inställningarna för renderingsprocessen och huvudprocessen. starta om applikationen för att ändringarna ska tillämpas"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "mest spelade",
|
||||
@@ -160,7 +219,9 @@
|
||||
"album": "$t(entity.album_one)",
|
||||
"trackNumber": "spår",
|
||||
"songCount": "sångräkning",
|
||||
"criticRating": "kritikerbetyg"
|
||||
"criticRating": "kritikerbetyg",
|
||||
"albumCount": "$t(entity.album_other) antal",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
},
|
||||
"form": {
|
||||
"deletePlaylist": {
|
||||
@@ -187,13 +248,17 @@
|
||||
"input_savePassword": "spara lösenord",
|
||||
"ignoreSsl": "ignorera ssl ($t(common.restartRequired))",
|
||||
"ignoreCors": "ignorera cors ($t(common.restartRequired))",
|
||||
"error_savePassword": "ett fel uppstod när lösenordet skulle sparas"
|
||||
"error_savePassword": "ett fel uppstod när lösenordet skulle sparas",
|
||||
"input_preferInstantMix": "föredra instant mixning",
|
||||
"input_preferInstantMixDescription": "använd bara instant mixning för att få liknande låtar. användbar om du har plugin för att förändra detta beteendet"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"success": "tillade {{message}} $t(entity.track_other) til {{numOfPlaylists}} $t(entity.playlist_other)",
|
||||
"success": "lade till $t(entity.trackWithCount, {\"count\": {{message}} }) till $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "lägg till i $t(entity.playlist_one)",
|
||||
"input_skipDuplicates": "hoppa över dubbletter",
|
||||
"input_playlists": "$t(entity.playlist_other)"
|
||||
"input_playlists": "$t(entity.playlist_other)",
|
||||
"create": "skapa $t(entity.playlist_one) {{playlist}}",
|
||||
"searchOrCreate": "sök $t(entity.playlist_other) eller skriv för att skapa en ny"
|
||||
},
|
||||
"updateServer": {
|
||||
"title": "uppdatera server",
|
||||
@@ -209,7 +274,19 @@
|
||||
"title": "sångtext sök"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"title": "redigera $t(entity.playlist_one)"
|
||||
"title": "redigera $t(entity.playlist_one)",
|
||||
"publicJellyfinNote": "Jellyfin visar av någon anledning inte om en spellista är publik eller inte. Om du önskar att denna ska förbli publik, så får du ha följande indata markerade"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "lägg till objekt till kön",
|
||||
"description": "Åtgärden kommer att lägga till alla objekt till den nuvarande filtrerade vyn"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "radiostation skapades",
|
||||
"title": "skapa radiostation",
|
||||
"input_homepageUrl": "hemside-URL",
|
||||
"input_name": "namn",
|
||||
"input_streamUrl": "stream url"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
@@ -257,7 +334,17 @@
|
||||
"addFavorite": "$t(action.addToFavorites)",
|
||||
"play": "$t(player.play)",
|
||||
"numberSelected": "{{count}} vald",
|
||||
"removeFromQueue": "$t(action.removeFromQueue)"
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"download": "ladda ner",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "dela objekt",
|
||||
"goTo": "gå till",
|
||||
"goToAlbum": "gå till $t(entity.album_one)",
|
||||
"goToAlbumArtist": "gå till $t(entity.albumArtist_one)",
|
||||
"showDetails": "hämta information"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "mer från $t(entity.artist_one)",
|
||||
@@ -291,6 +378,12 @@
|
||||
"searchFor": "sök efter {{query}}"
|
||||
},
|
||||
"title": "kommandon"
|
||||
},
|
||||
"manageServers": {
|
||||
"url": "URL",
|
||||
"username": "användarnamn",
|
||||
"editServerDetailsTooltip": "redigera serverinställningar",
|
||||
"removeServer": "ta bort server"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -317,7 +410,22 @@
|
||||
"track_one": "spår",
|
||||
"track_other": "spår",
|
||||
"trackWithCount_one": "{{count}} spår",
|
||||
"trackWithCount_other": "{{count}} spår"
|
||||
"trackWithCount_other": "{{count}} spår",
|
||||
"artistWithCount_one": "{{count}} artist",
|
||||
"artistWithCount_other": "{{count}} artister",
|
||||
"genre_one": "genre",
|
||||
"genre_other": "genrer",
|
||||
"genreWithCount_one": "{{count}} genre",
|
||||
"genreWithCount_other": "{{count}} genrer",
|
||||
"play_one": "{{count}} spelning",
|
||||
"play_other": "{{count}} spelningar",
|
||||
"smartPlaylist": "smart $t(entity.playlist_one)",
|
||||
"song_one": "låt",
|
||||
"song_other": "låtar",
|
||||
"radioStation_one": "radiostation",
|
||||
"radioStation_other": "radiostationer",
|
||||
"radioStationWithCount_one": "{{count}} radiostation",
|
||||
"radioStationWithCount_other": "{{count}} radiostationer"
|
||||
},
|
||||
"player": {
|
||||
"repeat_all": "repetera alla",
|
||||
@@ -341,5 +449,32 @@
|
||||
"queue_moveToBottom": "flytta markerad till toppen",
|
||||
"addLast": "lägg till sist",
|
||||
"mute": "muta"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "min",
|
||||
"secondShort": "sek",
|
||||
"hourShort": "h",
|
||||
"dayShort": "dag"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "är efter",
|
||||
"afterDate": "är efter (datum)",
|
||||
"before": "är före",
|
||||
"beforeDate": "är före (datum)",
|
||||
"contains": "innehåller",
|
||||
"endsWith": "slutar med",
|
||||
"inPlaylist": "är inom",
|
||||
"inTheLast": "är i den sista",
|
||||
"inTheRange": "är i spannet",
|
||||
"inTheRangeDate": "är i spannet (datum)",
|
||||
"is": "är",
|
||||
"isNot": "är inte",
|
||||
"isGreaterThan": "är större än",
|
||||
"isLessThan": "är mindre än",
|
||||
"matchesRegex": "matchar regex",
|
||||
"notContains": "innehåller inte",
|
||||
"notInPlaylist": "är inte inom",
|
||||
"notInTheLast": "är inte inom den sista",
|
||||
"startsWith": "startar med"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,8 +491,6 @@
|
||||
"discordApplicationId": "{{discord}} பயன்பாட்டு ஐடி",
|
||||
"discordListening": "கேட்பது என நிலையைக் காட்டுங்கள்",
|
||||
"exitToTray_description": "கணினி தட்டில் பயன்பாட்டிலிருந்து வெளியேறவும்",
|
||||
"floatingQueueArea": "மிதக்கும் வரிசை ஓவர் பகுதியைக் காட்டு",
|
||||
"floatingQueueArea_description": "நாடக வரிசையைக் காண திரையின் வலது பக்கத்தில் ஒரு ஓவர் ஐகானைக் காண்பி",
|
||||
"followLyric": "தற்போதைய பாடலைப் பின்பற்றுங்கள்",
|
||||
"followLyric_description": "தற்போதைய விளையாட்டு நிலைக்கு பாடலை உருட்டவும்",
|
||||
"font": "எழுத்துரு",
|
||||
@@ -505,8 +503,6 @@
|
||||
"gaplessAudio": "இடைவெளி இல்லாத ஆடியோ",
|
||||
"gaplessAudio_description": "MPV க்கான இடைவெளி இல்லாத ஆடியோ அமைப்பை அமைக்கிறது",
|
||||
"gaplessAudio_optionWeak": "பலவீனமான (பரிந்துரைக்கப்படுகிறது)",
|
||||
"genreBehavior": "வகை பக்கம் இயல்புநிலை நடத்தை",
|
||||
"genreBehavior_description": "ஒரு வகையைக் சொடுக்கு செய்வது டிராக் அல்லது ஆல்பம் பட்டியலில் இயல்பாகத் திறக்கிறதா என்பதை தீர்மானிக்கிறது",
|
||||
"globalMediaHotkeys_description": "பிளேபேக்கைக் கட்டுப்படுத்த உங்கள் கணினி மீடியா ஆட்கீசின் பயன்பாட்டை இயக்கவும் அல்லது முடக்கவும்",
|
||||
"homeConfiguration": "முகப்பு பக்க உள்ளமைவு",
|
||||
"homeFeature": "வீட்டில் கொணர்வி இடம்பெற்றது",
|
||||
@@ -554,8 +550,6 @@
|
||||
"playButtonBehavior_description": "வரிசையில் பாடல்களைச் சேர்க்கும்போது ப்ளே பொத்தானின் இயல்புநிலை நடத்தை அமைக்கிறது",
|
||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playerAlbumArtResolution": "பிளேயர் ஆல்பம் கலைத் தீர்மானம்",
|
||||
"playerAlbumArtResolution_description": "பெரிய வீரரின் ஆல்பம் கலை முன்னோட்டத்திற்கான தீர்மானம். பெரியது இது மிகவும் மிருதுவானதாக தோற்றமளிக்கிறது, ஆனால் மெதுவாக ஏற்றுவதை மெதுவாகக் கொண்டிருக்கலாம். இயல்புநிலை 0 க்கு, அதாவது ஆட்டோ",
|
||||
"playerbarOpenDrawer": "பிளேயர்பார் முழுத்திரை மாற்று",
|
||||
"playerbarOpenDrawer_description": "முழு திரை பிளேயரைத் திறக்க பிளேயர்பாரைக் சொடுக்கு செய்ய அனுமதிக்கிறது",
|
||||
"remotePassword": "ரிமோட் கண்ட்ரோல் சர்வர் கடவுச்சொல்",
|
||||
@@ -621,8 +615,6 @@
|
||||
"discordListening_description": "விளையாடுவதற்குப் பதிலாக கேட்பது என்று அந்த நிலையைக் காட்டுங்கள்",
|
||||
"discordRichPresence_description": "{{discord}} பணக்கார இருப்பில் பின்னணி நிலையை இயக்கவும். பட விசைகள்: {{icon}}, {{playing}}, மற்றும் {{paused}}",
|
||||
"customCss_description": "தனிப்பயன் சிஎச்எச் உள்ளடக்கம். குறிப்பு: உள்ளடக்கம் மற்றும் தொலைநிலை முகவரி கள் அனுமதிக்கப்படாத பண்புகள். உங்கள் உள்ளடக்கத்தின் முன்னோட்டம் கீழே காட்டப்பட்டுள்ளது. நீங்கள் அமைக்காத கூடுதல் புலங்கள் சுத்திகரிப்பு காரணமாக உள்ளன",
|
||||
"doubleClickBehavior": "இரட்டை சொடுக்கு செய்யும் போது தேடப்பட்ட அனைத்து தடங்களையும் வரிசைப்படுத்தவும்",
|
||||
"doubleClickBehavior_description": "உண்மை என்றால், தட தேடலில் பொருந்தக்கூடிய அனைத்து தடங்களும் வரிசையில் நிற்கப்படும். இல்லையெனில், சொடுக்கு செய்யப்பட்ட ஒன்று மட்டுமே வரிசையில் நிற்கப்படும்",
|
||||
"enableRemote": "ரிமோட் கண்ட்ரோல் சேவையகத்தை இயக்கவும்",
|
||||
"enableRemote_description": "பயன்பாட்டைக் கட்டுப்படுத்த மற்ற சாதனங்களை அனுமதிக்க ரிமோட் கண்ட்ரோல் சேவையகத்தை இயக்குகிறது",
|
||||
"externalLinks": "வெளிப்புற இணைப்புகளைக் காட்டு",
|
||||
@@ -740,9 +732,7 @@
|
||||
"titleCombined": "$t(common.title) (இணைந்தது)"
|
||||
},
|
||||
"view": {
|
||||
"card": "அட்டை",
|
||||
"table": "அட்டவணை",
|
||||
"poster": "சுவரொட்டி",
|
||||
"grid": "வலைவாய்",
|
||||
"list": "பட்டியல்"
|
||||
},
|
||||
|
||||
+11
-12
@@ -21,7 +21,15 @@
|
||||
"goToPage": "sayfaya git",
|
||||
"moveToNext": "sonrakine geç",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) düzenleyiciye geç"
|
||||
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) düzenleyiciye geç",
|
||||
"addOrRemoveFromSelection": "seçime ekle veya seçimi kaldır",
|
||||
"selectRangeOfItems": "bir dizi öğe seçin",
|
||||
"createRadioStation": "$t(entity.radioStation_one) oluştur",
|
||||
"deleteRadioStation": "$t(entity.radioStation_one) istasyonunu sil",
|
||||
"selectAll": "tümünü seç",
|
||||
"downloadStarted": "{{count}} öğenin indirilmesine başlandı",
|
||||
"moveUp": "yukarı kaydır",
|
||||
"moveDown": "aşağı kaydır"
|
||||
},
|
||||
"common": {
|
||||
"action_one": "eylem",
|
||||
@@ -120,7 +128,8 @@
|
||||
"trackGain": "parça kazancı",
|
||||
"trackPeak": "parça zirvesi",
|
||||
"private": "gizli",
|
||||
"clean": "temiz"
|
||||
"clean": "temiz",
|
||||
"countSelected": "{{count}} adet seçildi"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "albüm",
|
||||
@@ -536,12 +545,9 @@
|
||||
"discordServeImage_description": "sunucudan {{discord}} Rich Presence için kapak resmi paylaşın, yalnızca Jellyfin ve Navidrome için kullanılabilir",
|
||||
"discordUpdateInterval": "{{discord}} Rich Presence güncelleme aralığı",
|
||||
"discordUpdateInterval_description": "her güncelleme arasındaki saniye cinsinden süre (minimum 15 saniye)",
|
||||
"doubleClickBehavior": "çift tıklandığında aranan tüm parçaları sıraya koyma",
|
||||
"gaplessAudio": "aralıksız ses",
|
||||
"gaplessAudio_description": "mpv için aralıksız ses ayarını belirler",
|
||||
"gaplessAudio_optionWeak": "zayıf (tavsiye edilen)",
|
||||
"genreBehavior": "tür sayfası varsayılan davranışı",
|
||||
"genreBehavior_description": "bir türe tıklandığında varsayılan olarak parça mı yoksa albüm listesinde mi açılacağını belirler",
|
||||
"globalMediaHotkeys": "evrensel medya kısayol tuşları",
|
||||
"globalMediaHotkeys_description": "oynatmayı kontrol etmek için sistem medya kısayol tuşlarınızın kullanımını etkinleştirin veya devre dışı bırakın",
|
||||
"homeConfiguration": "ana sayfa yapılandırma",
|
||||
@@ -607,8 +613,6 @@
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playerAlbumArtResolution": "oynatıcı albüm resmi çözünürlüğü",
|
||||
"playerAlbumArtResolution_description": "büyük oynatıcının albüm resmi önizlemesi için çözünürlük. daha büyük değerler daha net görünmesini sağlar, ancak yüklemeyi yavaşlatabilir. varsayılan değer 0, otomatik olarak çalışır",
|
||||
"playerbarOpenDrawer": "oynatma çubuğu tam ekran geçişi",
|
||||
"playerbarOpenDrawer_description": "tam ekran oynatıcıyı açmak için oynatma çubuğuna tıklamaya izin verir",
|
||||
"remotePassword": "uzaktan kontrol sunucusu şifresi",
|
||||
@@ -669,15 +673,12 @@
|
||||
"windowBarStyle_description": "pencere çubuğunun stilini seçin",
|
||||
"zoom": "yakınlaştırma yüzdesi",
|
||||
"zoom_description": "uygulama için yakınlaştırma yüzdesini ayarlar",
|
||||
"doubleClickBehavior_description": "evet ise, bir parça aramasında eşleşen tüm parçalar sıraya alınır. aksi takdirde, yalnızca tıklanan parça sıraya alınır",
|
||||
"enableRemote": "uzaktan kontrol sunucusunu etkinleştir",
|
||||
"enableRemote_description": "uzaktan kumanda sunucusunun diğer cihazların uygulamayı kontrol etmesine izin vermesini sağlar",
|
||||
"externalLinks": "harici bağlantıları göster",
|
||||
"externalLinks_description": "sanatçı/albüm sayfalarında dış bağlantıların (Last.fm, MusicBrainz) gösterilmesini sağlar",
|
||||
"exitToTray": "tepsiye çıkış",
|
||||
"exitToTray_description": "uygulamadan sistem tepsisine çıkma",
|
||||
"floatingQueueArea": "kayan liste üzerine gelinen alanı göster",
|
||||
"floatingQueueArea_description": "oynatma kuyruğunu görüntülemek için ekranın sağ tarafında fareyle üzerine gelinen bir simge görüntüleyin",
|
||||
"followLyric": "güncel şarkı sözlerini takip et",
|
||||
"followLyric_description": "şarkı sözünü geçerli çalma konumuna kaydırma",
|
||||
"preferLocalLyrics": "yerel sözleri tercih edin",
|
||||
@@ -800,10 +801,8 @@
|
||||
"year": "$t(common.year)"
|
||||
},
|
||||
"view": {
|
||||
"card": "kart",
|
||||
"grid": "ızgara",
|
||||
"list": "liste",
|
||||
"poster": "poster",
|
||||
"table": "tablo"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,27 +1,42 @@
|
||||
{
|
||||
"action": {
|
||||
"editPlaylist": "编辑$t(entity.playlist_one)",
|
||||
"editPlaylist": "编辑 $t(entity.playlist_one)",
|
||||
"moveToTop": "移至顶部",
|
||||
"clearQueue": "清空播放队列",
|
||||
"addToFavorites": "添加到 $t(entity.favorite_other)",
|
||||
"addToPlaylist": "添加到 $t(entity.playlist_one)",
|
||||
"createPlaylist": "创建$t(entity.playlist_one)",
|
||||
"removeFromPlaylist": "从$t(entity.playlist_one)移除",
|
||||
"viewPlaylists": "查看$t(entity.playlist_other)",
|
||||
"createPlaylist": "创建 $t(entity.playlist_one)",
|
||||
"removeFromPlaylist": "从 $t(entity.playlist_one) 移除",
|
||||
"viewPlaylists": "查看 $t(entity.playlist_other)",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"deletePlaylist": "删除$t(entity.playlist_one)",
|
||||
"deletePlaylist": "删除 $t(entity.playlist_one)",
|
||||
"removeFromQueue": "从播放队列中移除",
|
||||
"deselectAll": "取消全选",
|
||||
"moveToBottom": "移至底部",
|
||||
"setRating": "评分",
|
||||
"toggleSmartPlaylistEditor": "切换$t(entity.smartPlaylist)编辑器",
|
||||
"removeFromFavorites": "从$t(entity.favorite_other)移除",
|
||||
"toggleSmartPlaylistEditor": "切换 $t(entity.smartPlaylist) 编辑器",
|
||||
"removeFromFavorites": "从 $t(entity.favorite_other) 移除",
|
||||
"goToPage": "前往页面",
|
||||
"openIn": {
|
||||
"lastfm": "在 Last.fm 中打开",
|
||||
"musicbrainz": "在 MusicBrainz 中打开"
|
||||
},
|
||||
"moveToNext": "移至下一首"
|
||||
"moveToNext": "移至下一首",
|
||||
"downloadStarted": "开始下载 {{count}} 个项目",
|
||||
"moveUp": "向上移动",
|
||||
"moveDown": "向下移动",
|
||||
"holdToMoveToTop": "按住即可移至到顶部",
|
||||
"holdToMoveToBottom": "按住即可移动到底部",
|
||||
"moveItems": "移动项目",
|
||||
"shuffle": "随机播放",
|
||||
"shuffleAll": "随机播放全部",
|
||||
"shuffleSelected": "随机播放选定的内容",
|
||||
"viewMore": "查看更多",
|
||||
"addOrRemoveFromSelection": "在所选内容中添加或移除",
|
||||
"selectRangeOfItems": "批量选择",
|
||||
"selectAll": "全选",
|
||||
"createRadioStation": "创建$t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "删除$t(entity.radioStation_one)"
|
||||
},
|
||||
"common": {
|
||||
"increase": "增高",
|
||||
@@ -122,7 +137,17 @@
|
||||
"private": "私人",
|
||||
"public": "公开",
|
||||
"recordLabel": "唱片公司",
|
||||
"releaseType": "发布类型"
|
||||
"releaseType": "发布类型",
|
||||
"doNotShowAgain": "不要再显示此内容",
|
||||
"view": "查看",
|
||||
"externalLinks": "外部链接",
|
||||
"faster": "更快",
|
||||
"noFilters": "未配置任何筛选器",
|
||||
"slower": "更慢",
|
||||
"sort": "排序",
|
||||
"gridRows": "网格行",
|
||||
"tableColumns": "表格列",
|
||||
"itemsMore": "{{count}} 更多"
|
||||
},
|
||||
"entity": {
|
||||
"albumArtist_other": "专辑艺术家",
|
||||
@@ -169,7 +194,7 @@
|
||||
"queue_moveToTop": "将所选项移至底部",
|
||||
"queue_moveToBottom": "将所选项移至顶部",
|
||||
"shuffle_off": "禁用随机播放",
|
||||
"addLast": "添加至播放列表末尾",
|
||||
"addLast": "上一曲",
|
||||
"mute": "静音",
|
||||
"skip_forward": "向前跳过",
|
||||
"playbackSpeed": "播放速度",
|
||||
@@ -293,7 +318,6 @@
|
||||
"replayGainMode": "{{ReplayGain}}模式",
|
||||
"playbackStyle_optionNormal": "正常",
|
||||
"windowBarStyle": "窗口顶栏风格",
|
||||
"floatingQueueArea": "显示浮动队列悬停区域",
|
||||
"replayGainFallback_description": "如果文件没有 {{ReplayGain}} 标签,则在数据库中应用增益",
|
||||
"hotkey_toggleRepeat": "切换循环",
|
||||
"lyricOffset_description": "将歌词偏移指定的毫秒数",
|
||||
@@ -308,7 +332,6 @@
|
||||
"hotkey_zoomOut": "缩小",
|
||||
"hotkey_unfavoriteCurrentSong": "取消收藏$t(common.currentSong)",
|
||||
"hotkey_rate0": "清除评分",
|
||||
"floatingQueueArea_description": "在屏幕右侧显示一个悬停图标,以查看播放队列",
|
||||
"hotkey_volumeMute": "静音",
|
||||
"hotkey_toggleCurrentSongFavorite": "切换收藏$t(common.currentSong)",
|
||||
"remoteUsername": "远程控制服务器用户名",
|
||||
@@ -349,10 +372,6 @@
|
||||
"startMinimized_description": "在系统托盘中启动应用程序",
|
||||
"passwordStore_description": "使用什么密码/密钥存储。如果您在存储密码时遇到问题,请更改此设置",
|
||||
"clearCacheSuccess": "缓存清除成功",
|
||||
"playerAlbumArtResolution": "播放器专辑封面分辨率",
|
||||
"playerAlbumArtResolution_description": "大型播放器专辑封面预览的分辨率。较大使其看起来更清晰,但可能会减慢加载速度。默认为0,表示自动",
|
||||
"genreBehavior": "类型页面默认行为",
|
||||
"genreBehavior_description": "确定单击流派是否默认在曲目或专辑列表中打开",
|
||||
"homeConfiguration": "主页配置",
|
||||
"homeConfiguration_description": "配置主页上显示的项目以及显示顺序",
|
||||
"passwordStore": "密码/密钥存储",
|
||||
@@ -360,8 +379,6 @@
|
||||
"homeFeature": "首页 精选 轮播",
|
||||
"imageAspectRatio": "保留封面图像纵横比",
|
||||
"imageAspectRatio_description": "如果启用,封面图像将保留纵横比显示。对于不是1:1的图像,剩余的空间将是空的",
|
||||
"doubleClickBehavior_description": "如果为真,则曲目搜索中所有匹配的曲目都将被加入播放队列。否则,只有单击的曲目才会被加入播放队列",
|
||||
"doubleClickBehavior": "双击时将所有搜索到的曲目加入播放队列",
|
||||
"volumeWidth": "音量滑块宽度",
|
||||
"volumeWidth_description": "音量滑块的宽度",
|
||||
"discordListening": "显示状态为正在监听",
|
||||
@@ -756,8 +773,6 @@
|
||||
},
|
||||
"view": {
|
||||
"table": "表格",
|
||||
"poster": "海报",
|
||||
"card": "卡片",
|
||||
"grid": "网格",
|
||||
"list": "列表"
|
||||
},
|
||||
@@ -829,5 +844,12 @@
|
||||
"album": "$t(entity.album_one)",
|
||||
"broadcast": "播送"
|
||||
}
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "之后",
|
||||
"afterDate": "晚于(日期)",
|
||||
"before": "之前",
|
||||
"beforeDate": "早于(日期)",
|
||||
"contains": "包含"
|
||||
}
|
||||
}
|
||||
|
||||
+315
-43
@@ -91,7 +91,24 @@
|
||||
"tags": "標籤",
|
||||
"trackGain": "曲目增益",
|
||||
"trackPeak": "歌曲峰值",
|
||||
"translation": "翻譯"
|
||||
"translation": "翻譯",
|
||||
"doNotShowAgain": "不再顯示",
|
||||
"externalLinks": "外部連結",
|
||||
"faster": "更快",
|
||||
"private": "私人",
|
||||
"public": "公開",
|
||||
"recordLabel": "唱片公司",
|
||||
"releaseType": "發行類型",
|
||||
"slower": "更慢",
|
||||
"sort": "排序",
|
||||
"tableColumns": "表格欄位",
|
||||
"clean": "清除",
|
||||
"explicitStatus": "Explicit狀態",
|
||||
"explicit": "Explicit",
|
||||
"gridRows": "網格行",
|
||||
"noFilters": "未設定任何過濾器",
|
||||
"countSelected": "{{count}}個已選取",
|
||||
"retry": "重試"
|
||||
},
|
||||
"error": {
|
||||
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
|
||||
@@ -117,7 +134,12 @@
|
||||
"badValue": "無效選項“{{value}}”。該值不再存在",
|
||||
"networkError": "發生網路錯誤",
|
||||
"notificationDenied": "通知權限被拒絕。此設定無效",
|
||||
"openError": "無法開啟檔案"
|
||||
"openError": "無法開啟檔案",
|
||||
"multipleServerSaveQueueError": "播放佇列中包含不是來自目前伺服器的歌曲,此操作不受支援",
|
||||
"saveQueueFailed": "儲存播放佇列失敗",
|
||||
"settingsSyncError": "偵測到渲染器與主程序之間的設定不一致,請重新啟動應用程式以套用變更",
|
||||
"noNetwork": "伺服器無法連線",
|
||||
"noNetworkDescription": "無法連接到此伺服器"
|
||||
},
|
||||
"page": {
|
||||
"contextMenu": {
|
||||
@@ -144,7 +166,9 @@
|
||||
"shareItem": "分享項目",
|
||||
"showDetails": "取得資訊",
|
||||
"goToAlbum": "前往 $t(entity.album_one)",
|
||||
"goToAlbumArtist": "前往 $t(entity.albumArtist_one)"
|
||||
"goToAlbumArtist": "前往 $t(entity.albumArtist_one)",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"goTo": "前往"
|
||||
},
|
||||
"globalSearch": {
|
||||
"title": "指令",
|
||||
@@ -160,7 +184,8 @@
|
||||
"title": "$t(common.home)",
|
||||
"mostPlayed": "最多播放",
|
||||
"newlyAdded": "最近新增的發行",
|
||||
"recentlyReleased": "最近發佈"
|
||||
"recentlyReleased": "最近發佈",
|
||||
"genres": "$t(entity.genre_other)"
|
||||
},
|
||||
"appMenu": {
|
||||
"openBrowserDevtools": "打開瀏覽器開發者工具",
|
||||
@@ -174,7 +199,11 @@
|
||||
"version": "版本 {{version}}",
|
||||
"manageServers": "管理伺服器",
|
||||
"privateModeOff": "關閉私人模式",
|
||||
"privateModeOn": "開啟私人模式"
|
||||
"privateModeOn": "開啟私人模式",
|
||||
"selectMusicFolder": "選擇媒體庫",
|
||||
"noMusicFolder": "未選取任何媒體庫",
|
||||
"multipleMusicFolders": "已選取 {{count}} 個媒體庫",
|
||||
"commandPalette": "開啟命令面板"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
@@ -207,7 +236,25 @@
|
||||
"playbackTab": "播放",
|
||||
"windowTab": "視窗",
|
||||
"generalTab": "一般",
|
||||
"advanced": "進階"
|
||||
"advanced": "進階",
|
||||
"analytics": "分析",
|
||||
"updates": "更新",
|
||||
"cache": "快取",
|
||||
"application": "應用程式",
|
||||
"theme": "主題",
|
||||
"controls": "控制面板",
|
||||
"sidebar": "側邊攔",
|
||||
"remote": "遠端控制",
|
||||
"exportImport": "匯入/匯出",
|
||||
"scrobble": "記錄播放資訊",
|
||||
"audio": "音訊",
|
||||
"lyrics": "歌詞",
|
||||
"transcoding": "轉碼",
|
||||
"discord": "Discord",
|
||||
"queryBuilder": "查詢建構器",
|
||||
"playerFilters": "播放過濾器",
|
||||
"logger": "日誌記錄器",
|
||||
"lyricsDisplay": "歌詞顯示"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
@@ -240,7 +287,9 @@
|
||||
"nowPlaying": "正在播放",
|
||||
"playlists": "$t(entity.playlist_other)",
|
||||
"myLibrary": "我的資料庫",
|
||||
"shared": "已分享 $t(entity.playlist_other)"
|
||||
"shared": "已分享 $t(entity.playlist_other)",
|
||||
"favorites": "$t(entity.favorite_other)",
|
||||
"radio": "$t(entity.radioStation_other)"
|
||||
},
|
||||
"trackList": {
|
||||
"title": "$t(entity.track_other)",
|
||||
@@ -273,12 +322,21 @@
|
||||
},
|
||||
"playlist": {
|
||||
"reorder": "僅當按 ID 排序時才啟用重新排序"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite_other)"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder_other)"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "電台"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"playbackFetchInProgress": "正在載入歌曲…",
|
||||
"addLast": "新增到尾端",
|
||||
"addNext": "新增到下一首",
|
||||
"addLast": "新增至尾端",
|
||||
"addNext": "新增至下一首",
|
||||
"favorite": "收藏",
|
||||
"mute": "靜音",
|
||||
"muted": "已靜音",
|
||||
@@ -291,7 +349,7 @@
|
||||
"repeat": "循環",
|
||||
"repeat_all": "全部循環",
|
||||
"repeat_off": "不循環",
|
||||
"shuffle": "隨機播放",
|
||||
"shuffle": "播放 (隨機)",
|
||||
"shuffle_off": "未啟用隨機播放",
|
||||
"skip": "跳過",
|
||||
"skip_back": "向後跳過",
|
||||
@@ -306,7 +364,18 @@
|
||||
"queue_moveToBottom": "使所選置頂",
|
||||
"queue_moveToTop": "使所選置底",
|
||||
"playSimilarSongs": "播放相似歌曲",
|
||||
"viewQueue": "檢視佇列"
|
||||
"viewQueue": "檢視佇列",
|
||||
"addLastShuffled": "新增至尾端 (隨機)",
|
||||
"addNextShuffled": "新增至下一首 (隨機)",
|
||||
"queueType": "佇列類型",
|
||||
"queueType_default": "預設",
|
||||
"queueType_priority": "優先",
|
||||
"holdToShuffle": "按住以隨機",
|
||||
"lyrics": "歌詞",
|
||||
"restoreQueueFromServer": "從伺服器還原播放佇列",
|
||||
"saveQueueToServer": "將播放佇列儲存至伺服器",
|
||||
"artistRadio": "藝人電台",
|
||||
"trackRadio": "曲目電台"
|
||||
},
|
||||
"setting": {
|
||||
"audioPlayer_description": "選擇用於播放的音訊播放器",
|
||||
@@ -342,8 +411,7 @@
|
||||
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
|
||||
"enableRemote": "啟用遠端控制伺服器",
|
||||
"enableRemote_description": "啟用遠端控制伺服器,以允許其他設備控制此應用程式",
|
||||
"exitToTray": "退出時最小化到托盤",
|
||||
"floatingQueueArea_description": "在螢幕右側顯示一個懸停圖示,以查看佇列",
|
||||
"exitToTray": "關閉時到將視窗最小化",
|
||||
"followLyric": "跟隨目前歌詞",
|
||||
"font_description": "設定應用程式使用的字體",
|
||||
"fontType": "字體類型",
|
||||
@@ -388,8 +456,8 @@
|
||||
"lyricOffset": "歌詞偏移(毫秒)",
|
||||
"lyricOffset_description": "將歌詞偏移指定的毫秒數",
|
||||
"lyricFetchProvider_description": "選擇歌詞來源。 來源順序即為搜尋的順序",
|
||||
"minimizeToTray": "最小化到托盤",
|
||||
"minimizeToTray_description": "將應用程式最小化到系統托盤",
|
||||
"minimizeToTray": "最小化到系統匣",
|
||||
"minimizeToTray_description": "將應用程式最小化到系統匣",
|
||||
"minimumScrobbleSeconds": "最小紀錄時間(秒)",
|
||||
"minimumScrobbleSeconds_description": "歌曲被記錄為已播放(scrobble)所需的最小播放時間",
|
||||
"mpvExecutablePath": "mpv 執行檔路徑",
|
||||
@@ -424,10 +492,10 @@
|
||||
"sidebarConfiguration": "側邊欄設定",
|
||||
"sidebarConfiguration_description": "選擇側邊欄包含的項目與順序",
|
||||
"sidebarPlaylistList_description": "顯示或隱藏側邊欄歌單清單",
|
||||
"sidePlayQueueStyle": "側邊播放清單樣式",
|
||||
"sidePlayQueueStyle_description": "設置側邊播放清單樣式",
|
||||
"sidePlayQueueStyle": "側邊播放佇列樣式",
|
||||
"sidePlayQueueStyle_description": "設置側邊播放佇列樣式",
|
||||
"sidePlayQueueStyle_optionAttached": "吸附",
|
||||
"sidePlayQueueStyle_optionDetached": "不吸附",
|
||||
"sidePlayQueueStyle_optionDetached": "分離",
|
||||
"skipDuration": "跳過時長",
|
||||
"skipDuration_description": "設置每次按下跳過按鈕將會跳過的時長",
|
||||
"skipPlaylistPage": "跳過播放清單頁面",
|
||||
@@ -446,8 +514,7 @@
|
||||
"sampleRate": "取樣率",
|
||||
"showSkipButtons_description": "在播放條顯示/隱藏播放按鈕",
|
||||
"playbackStyle": "播放風格",
|
||||
"exitToTray_description": "退出應用程式時最小化到系統托盤而非關閉",
|
||||
"floatingQueueArea": "顯示浮動佇列懸停區域",
|
||||
"exitToTray_description": "退出應用程式時最小化到系統匣而非關閉",
|
||||
"followLyric_description": "滾動歌詞到目前播放位置",
|
||||
"font": "字體",
|
||||
"globalMediaHotkeys_description": "啟用或禁用系統媒體快捷鍵以控制播放",
|
||||
@@ -491,14 +558,10 @@
|
||||
"discordListening_description": "將狀態顯示為\"正在聽\"而不是\"正在玩\"",
|
||||
"discordServeImage": "從伺服器提供{{discord}}圖片",
|
||||
"discordServeImage_description": "從伺服器本身分享 {{discord}} Rich Presence的封面圖片,僅支援 Jellyfin 與 Navidrome。{{discord}} 會透過機器人擷取圖片,因此您的伺服器必須能從公開網路連線",
|
||||
"doubleClickBehavior": "雙擊時將所有搜尋到的曲目加入佇列",
|
||||
"doubleClickBehavior_description": "如果為 true,則歌曲搜尋中所有符合的歌曲都會被加入佇列。否則,只有被點擊的歌曲才會被加入佇列",
|
||||
"externalLinks": "顯示外部連結",
|
||||
"externalLinks_description": "在藝術家/專輯頁面顯示外部連結(Last.fm, MusicBrainz)",
|
||||
"preferLocalLyrics": "偏好本地歌詞",
|
||||
"preferLocalLyrics_description": "優先選擇本地歌詞,而不是遠端歌詞(如果可用)",
|
||||
"genreBehavior": "曲風頁面預設動作",
|
||||
"genreBehavior_description": "決定點擊某個曲風時是否預設開啟曲目或專輯列表",
|
||||
"homeConfiguration": "首頁配置",
|
||||
"homeConfiguration_description": "配置在首頁上顯示哪些項目以及顯示順序",
|
||||
"homeFeature": "首頁特色輪播",
|
||||
@@ -517,12 +580,10 @@
|
||||
"passwordStore": "密碼/secret儲存",
|
||||
"passwordStore_description": "使用什麼密碼/secret儲存。如果您在儲存密碼時遇到問題,請變更此項目",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playerAlbumArtResolution": "播放器專輯封面解析度",
|
||||
"playerAlbumArtResolution_description": "大型播放器專輯封面預覽的解析度。較大的解析度使其看起來更清晰,但可能會減慢載入速度。預設為 0,表示自動",
|
||||
"playerbarOpenDrawer": "播放器列全螢幕切換",
|
||||
"playerbarOpenDrawer_description": "允許點擊播放器列以開啟全螢幕播放器",
|
||||
"startMinimized": "最小化啟動",
|
||||
"startMinimized_description": "在系統托盤中啟動應用程式",
|
||||
"startMinimized": "啟動時最小化",
|
||||
"startMinimized_description": "在系統匣中啟動應用程式",
|
||||
"transcode_description": "啟用轉碼到不同格式",
|
||||
"transcodeBitrate": "要轉碼的比特率",
|
||||
"transcodeBitrate_description": "選擇要轉碼的比特率。 0 表示讓伺服器選擇",
|
||||
@@ -534,8 +595,8 @@
|
||||
"translationApiKey_description": "翻譯的API金鑰(僅限全域服務端點)",
|
||||
"translationTargetLanguage": "目標翻譯語言",
|
||||
"translationTargetLanguage_description": "翻譯的目標語言",
|
||||
"trayEnabled": "顯示托盤",
|
||||
"trayEnabled_description": "顯示/隱藏托盤圖示/選單。如果停用,則也會停用最小化/退出到托盤",
|
||||
"trayEnabled": "顯示系統匣",
|
||||
"trayEnabled_description": "顯示/隱藏系統匣圖示/選單。如果停用,將同時停用最小化/退出到系統匣",
|
||||
"volumeWidth": "音量條寬度",
|
||||
"volumeWidth_description": "音量條的寬度",
|
||||
"webAudio": "使用網頁音訊",
|
||||
@@ -546,7 +607,7 @@
|
||||
"artistBackground_description": "為藝人頁面新增含藝人圖片的背景圖像",
|
||||
"artistBackgroundBlur": "藝人背景圖片模糊程度",
|
||||
"artistBackgroundBlur_description": "調整套用至藝人背景圖片的模糊程度",
|
||||
"releaseChannel_optionLatest": "穩定版",
|
||||
"releaseChannel_optionLatest": "最新版本",
|
||||
"releaseChannel_optionBeta": "測試版",
|
||||
"releaseChannel_description": "選擇自動更新時要使用穩定版本或是測試版本",
|
||||
"discordDisplayType": "{{discord}} presence 顯示類型",
|
||||
@@ -562,7 +623,77 @@
|
||||
"preventSleepOnPlayback_description": "在音樂播放時防止螢幕進入睡眠狀態",
|
||||
"mediaSession": "啟用Media Session",
|
||||
"mediaSession_description": "啟用 Windows Media Session 整合功能,於系統音量Overlay和鎖定畫面中顯示媒體資料與控制面板(僅限 Windows)",
|
||||
"releaseChannel": "發佈通道"
|
||||
"releaseChannel": "發佈通道",
|
||||
"analyticsDisable": "選擇退出使用情況分析",
|
||||
"analyticsDisable_description": "經過匿名處理的使用情況資料將傳送給開發者,以協助改進應用程式",
|
||||
"crossfadeStyle": "交叉淡入淡出風格",
|
||||
"discordRichPresence": "{{discord}} Rich Presence",
|
||||
"enableAutoTranslation_description": "歌詞載入時自動啟用翻譯功能",
|
||||
"enableAutoTranslation": "啟用自動翻譯",
|
||||
"exportImportSettings_control_description": "使用 JSON 匯出與匯入設定",
|
||||
"exportImportSettings_control_exportText": "匯出設定",
|
||||
"exportImportSettings_control_importText": "匯入設定",
|
||||
"exportImportSettings_control_title": "匯入/匯出設定",
|
||||
"exportImportSettings_destructiveWarning": "匯入設定會覆蓋現有資料,請在點擊下方「匯入」按鈕前詳閱上述內容!",
|
||||
"exportImportSettings_importBtn": "匯入設定",
|
||||
"exportImportSettings_importModalTitle": "匯入Feishin設定",
|
||||
"exportImportSettings_importSuccess": "設定已成功匯入!",
|
||||
"exportImportSettings_notValidJSON": "傳遞的檔案不是有效的 JSON",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" 不正確 - {{reason}}",
|
||||
"language": "語言",
|
||||
"notify": "啟用歌曲通知",
|
||||
"notify_description": "當歌曲變更時顯示通知",
|
||||
"playerbarSlider": "播放進度條",
|
||||
"playerbarSliderType_optionSlider": "滑桿",
|
||||
"playerbarSliderType_optionWaveform": "波形",
|
||||
"playerbarWaveformAlign": "波形對齊",
|
||||
"playerbarWaveformAlign_optionTop": "靠上對齊",
|
||||
"playerbarWaveformAlign_optionCenter": "置中對齊",
|
||||
"playerbarWaveformAlign_optionBottom": "靠下對齊",
|
||||
"playerbarWaveformBarWidth": "波形寬度",
|
||||
"playerbarWaveformGap": "波形間距",
|
||||
"playerbarWaveformRadius": "波形圓角",
|
||||
"showLyricsInSidebar_description": "在播放佇列增加一個面板來顯示歌詞",
|
||||
"showLyricsInSidebar": "在播放器側邊欄顯示歌詞",
|
||||
"showVisualizerInSidebar_description": "在播放佇列增加一個面板來顯示視覺化效果",
|
||||
"showVisualizerInSidebar": "在播放器側邊欄顯示視覺化效果",
|
||||
"transcode": "啟用轉碼功能",
|
||||
"queryBuilderCustomFields_inputLabel": "唱片公司",
|
||||
"queryBuilderCustomFields_inputTag": "標籤",
|
||||
"queryBuilderCustomFields": "自訂欄位",
|
||||
"audioFadeOnStatusChange": "狀態變更時音訊淡入淡出",
|
||||
"audioFadeOnStatusChange_description": "當播放/暫停狀態變更時,啟用淡入淡出效果",
|
||||
"queryBuilder": "查詢建構器",
|
||||
"queryBuilderCustomFields_description": "在查詢建構器中新增自訂欄位",
|
||||
"followCurrentSong_description": "自動將播放佇列捲動至當前播放的歌曲",
|
||||
"followCurrentSong": "跟隨當前歌曲",
|
||||
"playerbarSlider_description": "不建議在網路速度緩慢或計費的網路下使用波形",
|
||||
"playerFilters": "從佇列中過濾歌曲",
|
||||
"playerFilters_description": "根據以下條件,排除要新增至佇列中的歌曲",
|
||||
"autoDJ": "Auto DJ",
|
||||
"autoDJ_description": "自動將相似的歌曲加入到播放佇列",
|
||||
"autoDJ_itemCount": "歌曲數量",
|
||||
"autoDJ_itemCount_description": "在啟用Auto DJ時嘗試加入佇列的歌曲數量",
|
||||
"autoDJ_timing_description": "佇列中剩餘多少歌曲時啟動 Auto DJ",
|
||||
"autoDJ_timing": "觸發時機",
|
||||
"logLevel": "log等級",
|
||||
"logLevel_description": "設定要顯示的最低日誌等級。Debug 會顯示所有日誌,Error 僅會顯示錯誤訊息",
|
||||
"logLevel_optionDebug": "Debug",
|
||||
"logLevel_optionError": "Error",
|
||||
"logLevel_optionInfo": "Info",
|
||||
"logLevel_optionWarn": "Warn",
|
||||
"useThemeAccentColor": "使用主題強調色",
|
||||
"useThemeAccentColor_description": "使用所選主題中定義的主要顏色,而非自訂的強調色",
|
||||
"artistRadioCount_description": "設定為藝人電台與曲目電台擷取的歌曲數量",
|
||||
"imageResolution": "圖片解析度",
|
||||
"imageResolution_description": "應用程式中所使用圖片的解析度。設定為 0 時,將使用圖片的原始解析度",
|
||||
"imageResolution_optionTable": "表格",
|
||||
"imageResolution_optionItemCard": "項目卡片",
|
||||
"imageResolution_optionSidebar": "側邊欄",
|
||||
"imageResolution_optionHeader": "頁首",
|
||||
"imageResolution_optionFullScreenPlayer": "全螢幕播放器",
|
||||
"combinedLyricsAndVisualizer_description": "將歌詞與視覺化效果整合至同一個面板",
|
||||
"combinedLyricsAndVisualizer": "在播放器側邊欄整合歌詞與視覺化效果"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
@@ -574,7 +705,28 @@
|
||||
"autoFitColumns": "列寬自適應",
|
||||
"followCurrentSong": "跟隨目前歌曲",
|
||||
"itemGap": "項目間隔 (px)",
|
||||
"itemSize": "項目大小 (px)"
|
||||
"itemSize": "項目大小 (px)",
|
||||
"advancedSettings": "進階設定",
|
||||
"autosize": "自動調整大小",
|
||||
"moveUp": "往上",
|
||||
"moveDown": "往下",
|
||||
"pinToLeft": "固定在左側",
|
||||
"pinToRight": "固定在右側",
|
||||
"alignLeft": "靠左對齊",
|
||||
"alignCenter": "置中對齊",
|
||||
"alignRight": "靠右對齊",
|
||||
"itemsPerRow": "每行項目數",
|
||||
"size_default": "預設",
|
||||
"size_compact": "緊湊",
|
||||
"size_large": "大型",
|
||||
"pagination": "分頁模式",
|
||||
"pagination_itemsPerPage": "每頁項目數",
|
||||
"pagination_infinite": "無限滾動",
|
||||
"pagination_paginate": "分頁式",
|
||||
"alternateRowColors": "隔行上色",
|
||||
"horizontalBorders": "行邊框線",
|
||||
"rowHoverHighlight": "滑鼠懸停Highlight",
|
||||
"verticalBorders": "列邊框線"
|
||||
},
|
||||
"label": {
|
||||
"actions": "$t(common.action_other)",
|
||||
@@ -604,11 +756,14 @@
|
||||
"year": "$t(common.year)",
|
||||
"rating": "$t(common.rating)",
|
||||
"codec": "$t(common.codec)",
|
||||
"songCount": "$t(entity.track_other)"
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"albumCount": "$t(entity.album_other)",
|
||||
"genreBadge": "$t(entity.genre_one) (徽章)",
|
||||
"image": "圖片",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
},
|
||||
"view": {
|
||||
"card": "卡片",
|
||||
"poster": "海報",
|
||||
"table": "表格",
|
||||
"grid": "網格",
|
||||
"list": "列表"
|
||||
@@ -638,7 +793,10 @@
|
||||
"title": "標題",
|
||||
"trackNumber": "曲目編號",
|
||||
"size": "$t(common.size)",
|
||||
"codec": "$t(common.codec)"
|
||||
"codec": "$t(common.codec)",
|
||||
"owner": "擁有者",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
@@ -663,7 +821,22 @@
|
||||
"openIn": {
|
||||
"lastfm": "在Last.fm開啟",
|
||||
"musicbrainz": "在MusicBrainz開啟"
|
||||
}
|
||||
},
|
||||
"downloadStarted": "已開始下載 {{count}} 項內容",
|
||||
"moveItems": "移動項目",
|
||||
"shuffle": "隨機播放",
|
||||
"shuffleAll": "全部隨機播放",
|
||||
"shuffleSelected": "隨機播放選取項目",
|
||||
"viewMore": "查看更多",
|
||||
"moveUp": "向上移動",
|
||||
"moveDown": "向下移動",
|
||||
"holdToMoveToTop": "按住以移動至頂部",
|
||||
"holdToMoveToBottom": "按住以移動至底部",
|
||||
"createRadioStation": "創建 $t(entity.radioStation_one)",
|
||||
"deleteRadioStation": "刪除 $t(entity.radioStation_one)",
|
||||
"openApplicationDirectory": "開啟應用程式目錄",
|
||||
"addOrRemoveFromSelection": "新增或移除選取項目",
|
||||
"selectAll": "全選"
|
||||
},
|
||||
"entity": {
|
||||
"album_other": "專輯",
|
||||
@@ -683,7 +856,9 @@
|
||||
"trackWithCount_other": "{{count}} 首曲目",
|
||||
"albumWithCount_other": "{{count}} 張專輯",
|
||||
"play_other": "{{count}}次播放",
|
||||
"song_other": "歌曲"
|
||||
"song_other": "歌曲",
|
||||
"radioStation_other": "電台",
|
||||
"radioStationWithCount_other": "{{count}} 個電台"
|
||||
},
|
||||
"filter": {
|
||||
"albumCount": "$t(entity.album_other)數",
|
||||
@@ -727,7 +902,8 @@
|
||||
"search": "搜尋",
|
||||
"title": "標題",
|
||||
"toYear": "從年份",
|
||||
"trackNumber": "曲目"
|
||||
"trackNumber": "曲目",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
},
|
||||
"form": {
|
||||
"addServer": {
|
||||
@@ -749,7 +925,9 @@
|
||||
"input_playlists": "$t(entity.playlist_other)",
|
||||
"input_skipDuplicates": "跳過重複",
|
||||
"success": "新增 $t(entity.trackWithCount, {\"count\": {{message}} }) 到 $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "新增到$t(entity.playlist_one)"
|
||||
"title": "新增到$t(entity.playlist_one)",
|
||||
"create": "創建 $t(entity.playlist_one) {{playlist}}",
|
||||
"searchOrCreate": "搜尋$t(entity.playlist_other) 或輸入內容以建立新項目"
|
||||
},
|
||||
"createPlaylist": {
|
||||
"input_description": "$t(common.description)",
|
||||
@@ -767,7 +945,11 @@
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "匹配全部",
|
||||
"input_optionMatchAny": "匹配任何",
|
||||
"title": "查詢編輯器"
|
||||
"title": "查詢編輯器",
|
||||
"addRuleGroup": "新增規則群組",
|
||||
"removeRuleGroup": "移除規則群組",
|
||||
"resetToDefault": "恢復為預設值",
|
||||
"clearFilters": "清除篩選"
|
||||
},
|
||||
"updateServer": {
|
||||
"success": "伺服器已更新成功",
|
||||
@@ -781,7 +963,8 @@
|
||||
"editPlaylist": {
|
||||
"title": "編輯$t(entity.playlist_one)",
|
||||
"publicJellyfinNote": "Jellyfin 出於某種原因,不會顯示播放清單是否公開。如果您希望保持公開狀態,請選擇以下輸入",
|
||||
"success": "$t(entity.playlist_one) 更新成功"
|
||||
"success": "$t(entity.playlist_one) 更新成功",
|
||||
"editNote": "不建議手動編輯大型播放清單,你確定要承擔覆寫現有播放清單可能造成的資料遺失風險嗎?"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "允許下載",
|
||||
@@ -795,6 +978,95 @@
|
||||
"enabled": "已啟用私人模式,播放狀態將對外部整合隱藏",
|
||||
"disabled": "已停用私人模式,播放狀態現對已啟用的外部整合可見",
|
||||
"title": "私人模式"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "將項目加入播放佇列",
|
||||
"description": "此操作將新增目前篩選檢視中的所有項目"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "隨機播放",
|
||||
"input_genre": "$t(entity.genre_one)",
|
||||
"input_limit": "多少曲目?",
|
||||
"input_minYear": "起始年份",
|
||||
"input_maxYear": "結束年份",
|
||||
"input_played": "播放過濾器",
|
||||
"input_played_optionAll": "所有曲目",
|
||||
"input_played_optionUnplayed": "僅未播放的曲目",
|
||||
"input_played_optionPlayed": "僅播放過的曲目"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "電台創建成功",
|
||||
"title": "創建電台",
|
||||
"input_homepageUrl": "首頁連結",
|
||||
"input_name": "名稱",
|
||||
"input_streamUrl": "串流網址"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "已將播放佇列儲存至伺服器"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "匯出歌詞",
|
||||
"input_synced": "匯出同步歌詞",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
}
|
||||
},
|
||||
"releaseType": {
|
||||
"primary": {
|
||||
"album": "$t(entity.album_one)",
|
||||
"ep": "EP",
|
||||
"other": "其他",
|
||||
"broadcast": "廣播",
|
||||
"single": "單曲"
|
||||
},
|
||||
"secondary": {
|
||||
"audiobook": "有聲書",
|
||||
"audioDrama": "廣播劇",
|
||||
"compilation": "合輯",
|
||||
"djMix": "DJ Mix",
|
||||
"fieldRecording": "現場錄音",
|
||||
"demo": "Demo",
|
||||
"interview": "訪談",
|
||||
"live": "Live",
|
||||
"mixtape": "混音帶",
|
||||
"remix": "Remix",
|
||||
"soundtrack": "原聲帶"
|
||||
}
|
||||
},
|
||||
"dragDropZone": {
|
||||
"error_oneFileOnly": "請僅選擇一個檔案",
|
||||
"error_readingFile": "讀取檔案時發生問題:{{errorMessage}}",
|
||||
"mainText": "將檔案拖放到此處"
|
||||
},
|
||||
"queryBuilder": {
|
||||
"standardTags": "標準標籤",
|
||||
"customTags": "自訂標籤"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "在…之後",
|
||||
"afterDate": "晚於 (日期)",
|
||||
"before": "在…之前",
|
||||
"beforeDate": "早於 (日期)",
|
||||
"contains": "包含",
|
||||
"endsWith": "以…結尾",
|
||||
"inPlaylist": "在…之中",
|
||||
"inTheRange": "在範圍內",
|
||||
"inTheRangeDate": "在(日期)範圍內",
|
||||
"is": "是",
|
||||
"isNot": "不是",
|
||||
"isGreaterThan": "大於",
|
||||
"isLessThan": "小於",
|
||||
"matchesRegex": "符合正規表達式",
|
||||
"notContains": "不包含",
|
||||
"notInPlaylist": "不在…之中",
|
||||
"startsWith": "以…開頭"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "分",
|
||||
"secondShort": "秒",
|
||||
"hourShort": "小時",
|
||||
"dayShort": "天"
|
||||
},
|
||||
"visualizer": {
|
||||
"visualizerType": "視覺化效果類型"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@ export async function query(
|
||||
|
||||
try {
|
||||
result = await axios.get<LrcLibTrackResponse>(FETCH_URL, {
|
||||
headers: {
|
||||
'User-Agent': 'LRCGET v0.2.0 (https://github.com/jeffvli/feishin)',
|
||||
},
|
||||
params: {
|
||||
album_name: params.album,
|
||||
artist_name: params.artist,
|
||||
|
||||
@@ -20,24 +20,6 @@ export interface Result {
|
||||
songs: Song[];
|
||||
}
|
||||
|
||||
export interface Song {
|
||||
album: Album;
|
||||
alias: string[];
|
||||
artists: Artist[];
|
||||
copyrightId: number;
|
||||
duration: number;
|
||||
fee: number;
|
||||
ftype: number;
|
||||
id: number;
|
||||
mark: number;
|
||||
mvid: number;
|
||||
name: string;
|
||||
rtype: number;
|
||||
rUrl: null;
|
||||
status: number;
|
||||
transNames?: string[];
|
||||
}
|
||||
|
||||
interface Album {
|
||||
artist: Artist;
|
||||
copyrightId: number;
|
||||
@@ -69,6 +51,24 @@ interface NetEaseResponse {
|
||||
result: Result;
|
||||
}
|
||||
|
||||
interface Song {
|
||||
album: Album;
|
||||
alias: string[];
|
||||
artists: Artist[];
|
||||
copyrightId: number;
|
||||
duration: number;
|
||||
fee: number;
|
||||
ftype: number;
|
||||
id: number;
|
||||
mark: number;
|
||||
mvid: number;
|
||||
name: string;
|
||||
rtype: number;
|
||||
rUrl: null;
|
||||
status: number;
|
||||
transNames?: string[];
|
||||
}
|
||||
|
||||
export async function getLyricsBySongId(songId: string): Promise<null | string> {
|
||||
let result: AxiosResponse<any, any>;
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Fuse from 'fuse.js';
|
||||
import Fuse, { IFuseOptions } from 'fuse.js';
|
||||
|
||||
import {
|
||||
InternetProviderLyricSearchResponse,
|
||||
@@ -11,7 +11,7 @@ export const orderSearchResults = (args: {
|
||||
}) => {
|
||||
const { params, results } = args;
|
||||
|
||||
const options: Fuse.IFuseOptions<InternetProviderLyricSearchResponse> = {
|
||||
const options: IFuseOptions<InternetProviderLyricSearchResponse> = {
|
||||
fieldNormWeight: 1,
|
||||
includeScore: true,
|
||||
keys: [
|
||||
|
||||
@@ -4,11 +4,14 @@ import { rm } from 'fs/promises';
|
||||
import uniq from 'lodash/uniq';
|
||||
import MpvAPI from 'node-mpv';
|
||||
import { pid } from 'node:process';
|
||||
import process from 'process';
|
||||
|
||||
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
||||
import { createLog, isWindows } from '../../../utils';
|
||||
import { store } from '../settings';
|
||||
|
||||
import { PlayerData } from '/@/shared/types/domain-types';
|
||||
|
||||
declare module 'node-mpv';
|
||||
|
||||
// function wait(timeout: number) {
|
||||
@@ -20,6 +23,7 @@ declare module 'node-mpv';
|
||||
// }
|
||||
|
||||
let mpvInstance: MpvAPI | null = null;
|
||||
let currentPlayerData: null | PlayerData = null;
|
||||
const socketPath = isWindows() ? `\\\\.\\pipe\\mpvserver-${pid}` : `/tmp/node-mpv-${pid}.sock`;
|
||||
|
||||
const NodeMpvErrorCode = {
|
||||
@@ -113,7 +117,7 @@ const createMpv = async (data: {
|
||||
mpv.on('status', (status) => {
|
||||
if (status.property === 'playlist-pos') {
|
||||
if (status.value === -1) {
|
||||
mpv?.stop();
|
||||
mpv?.pause();
|
||||
}
|
||||
|
||||
if (status.value !== 0) {
|
||||
@@ -149,12 +153,28 @@ export const getMpvInstance = () => {
|
||||
return mpvInstance;
|
||||
};
|
||||
|
||||
const quit = async () => {
|
||||
const instance = getMpvInstance();
|
||||
if (instance) {
|
||||
await instance.quit();
|
||||
const quit = async (instance?: MpvAPI | null) => {
|
||||
const mpv = instance || getMpvInstance();
|
||||
if (mpv) {
|
||||
try {
|
||||
await mpv.quit();
|
||||
} catch {
|
||||
// If quit() fails, try to kill the process directly
|
||||
const mpvProcess = (mpv as any).process || (mpv as any).mpvProcess;
|
||||
if (mpvProcess && typeof mpvProcess.kill === 'function') {
|
||||
try {
|
||||
mpvProcess.kill('SIGTERM');
|
||||
} catch (killErr) {
|
||||
mpvLog({ action: 'Failed to kill mpv process' }, killErr as NodeMpvError);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isWindows()) {
|
||||
await rm(socketPath);
|
||||
try {
|
||||
await rm(socketPath);
|
||||
} catch {
|
||||
// Ignore errors when removing socket file
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -356,16 +376,12 @@ ipcMain.on('player-set-queue-next', async (_event, url?: string) => {
|
||||
try {
|
||||
const size = await getMpvInstance()?.getPlaylistSize();
|
||||
|
||||
if (!size) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (size > 1) {
|
||||
if (size && size > 1) {
|
||||
await getMpvInstance()?.playlistRemove(1);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
await getMpvInstance()?.load(url, 'append');
|
||||
getMpvInstance()?.load(url, 'append');
|
||||
}
|
||||
} catch (err: any | NodeMpvError) {
|
||||
mpvLog({ action: `Failed to set play queue` }, err);
|
||||
@@ -377,6 +393,7 @@ ipcMain.on('player-auto-next', async (_event, url?: string) => {
|
||||
// Always keep the current song as position 0 in the mpv queue
|
||||
// This allows us to easily set update the next song in the queue without
|
||||
// disturbing the currently playing song
|
||||
|
||||
try {
|
||||
await getMpvInstance()
|
||||
?.playlistRemove(0)
|
||||
@@ -423,6 +440,91 @@ ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
|
||||
}
|
||||
});
|
||||
|
||||
// Updates the current player metadata (song data)
|
||||
ipcMain.on('player-update-metadata', (_event, data: PlayerData) => {
|
||||
currentPlayerData = data;
|
||||
});
|
||||
|
||||
// Returns the current player metadata (song data)
|
||||
ipcMain.handle('player-metadata', async (): Promise<null | PlayerData> => {
|
||||
return currentPlayerData;
|
||||
});
|
||||
|
||||
// Returns the stream metadata from mpv (for radio streams)
|
||||
ipcMain.handle(
|
||||
'player-stream-metadata',
|
||||
async (): Promise<null | { artist: null | string; title: null | string }> => {
|
||||
try {
|
||||
const metadata = await getMpvInstance()?.getProperty('metadata');
|
||||
if (metadata && typeof metadata === 'object') {
|
||||
// Try to get separate title and artist fields first
|
||||
let artist: null | string =
|
||||
(metadata['artist'] as string) ||
|
||||
(metadata['ARTIST'] as string) ||
|
||||
(metadata['icy-artist'] as string) ||
|
||||
null;
|
||||
let title: null | string =
|
||||
(metadata['title'] as string) || (metadata['TITLE'] as string) || null;
|
||||
|
||||
// If we don't have separate fields, try to parse from combined formats
|
||||
if (!title && !artist) {
|
||||
const combinedTitle =
|
||||
(metadata['icy-title'] as string) ||
|
||||
(metadata['StreamTitle'] as string) ||
|
||||
(metadata['stream-title'] as string) ||
|
||||
null;
|
||||
|
||||
if (combinedTitle && typeof combinedTitle === 'string') {
|
||||
// Try to parse "Artist - Title" format
|
||||
const match = combinedTitle.match(/^(.*?)\s*[-–—]\s*(.+)$/);
|
||||
if (match) {
|
||||
artist = match[1].trim() || null;
|
||||
title = match[2].trim() || null;
|
||||
} else {
|
||||
// If no separator found, treat the whole thing as title
|
||||
title = combinedTitle;
|
||||
}
|
||||
}
|
||||
} else if (!title) {
|
||||
// If we have artist but no title, try to get from combined format
|
||||
const combinedTitle =
|
||||
(metadata['icy-title'] as string) ||
|
||||
(metadata['StreamTitle'] as string) ||
|
||||
(metadata['stream-title'] as string) ||
|
||||
null;
|
||||
if (combinedTitle && typeof combinedTitle === 'string') {
|
||||
title = combinedTitle;
|
||||
}
|
||||
} else if (!artist) {
|
||||
// If we have title but no artist, try to get from combined format
|
||||
const combinedTitle =
|
||||
(metadata['icy-title'] as string) ||
|
||||
(metadata['StreamTitle'] as string) ||
|
||||
(metadata['stream-title'] as string) ||
|
||||
null;
|
||||
if (
|
||||
combinedTitle &&
|
||||
typeof combinedTitle === 'string' &&
|
||||
combinedTitle !== title
|
||||
) {
|
||||
// Try to parse artist from combined format
|
||||
const match = combinedTitle.match(/^(.*?)\s*[-–—]\s*(.+)$/);
|
||||
if (match && match[2].trim() === title) {
|
||||
artist = match[1].trim() || null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { artist, title };
|
||||
}
|
||||
return null;
|
||||
} catch (err: any | NodeMpvError) {
|
||||
mpvLog({ action: `Failed to get stream metadata` }, err);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
enum MpvState {
|
||||
STARTED,
|
||||
IN_PROGRESS,
|
||||
@@ -431,6 +533,36 @@ enum MpvState {
|
||||
|
||||
let mpvState = MpvState.STARTED;
|
||||
|
||||
// Cleanup function that can be called from multiple places
|
||||
const cleanupMpv = async (force = false) => {
|
||||
if (mpvState === MpvState.DONE && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = getMpvInstance();
|
||||
if (instance) {
|
||||
try {
|
||||
if (!force) {
|
||||
await instance.stop();
|
||||
}
|
||||
await quit(instance);
|
||||
} catch (err: any | NodeMpvError) {
|
||||
mpvLog({ action: `Failed to cleanup mpv` }, err);
|
||||
// Force kill as fallback
|
||||
const mpvProcess = (instance as any).process || (instance as any).mpvProcess;
|
||||
if (mpvProcess && typeof mpvProcess.kill === 'function') {
|
||||
try {
|
||||
mpvProcess.kill('SIGKILL');
|
||||
} catch {
|
||||
// Ignore kill errors
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mpvInstance = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
app.on('before-quit', async (event) => {
|
||||
switch (mpvState) {
|
||||
case MpvState.DONE:
|
||||
@@ -442,8 +574,7 @@ app.on('before-quit', async (event) => {
|
||||
try {
|
||||
mpvState = MpvState.IN_PROGRESS;
|
||||
event.preventDefault();
|
||||
await getMpvInstance()?.stop();
|
||||
await quit();
|
||||
await cleanupMpv();
|
||||
} catch (err: any | NodeMpvError) {
|
||||
mpvLog({ action: `Failed to cleanly before-quit` }, err);
|
||||
} finally {
|
||||
@@ -454,3 +585,46 @@ app.on('before-quit', async (event) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle process exit events to ensure mpv is killed even if app crashes
|
||||
process.on('exit', () => {
|
||||
const instance = getMpvInstance();
|
||||
if (instance) {
|
||||
// Try to access and kill the process directly
|
||||
const mpvProcess = (instance as any).process || (instance as any).mpvProcess;
|
||||
if (mpvProcess && typeof mpvProcess.kill === 'function') {
|
||||
try {
|
||||
mpvProcess.kill('SIGKILL');
|
||||
} catch {
|
||||
// Ignore errors during exit
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle signals that can terminate the process
|
||||
process.on('SIGINT', async () => {
|
||||
await cleanupMpv(true);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await cleanupMpv(true);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Handle uncaught exceptions - cleanup mpv before crashing
|
||||
process.on('uncaughtException', async (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
await cleanupMpv(true).catch(() => {
|
||||
// Ignore cleanup errors during crash
|
||||
});
|
||||
});
|
||||
|
||||
// Handle unhandled rejections - cleanup mpv
|
||||
process.on('unhandledRejection', async (reason) => {
|
||||
console.error('Unhandled rejection:', reason);
|
||||
await cleanupMpv(true).catch(() => {
|
||||
// Ignore cleanup errors
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
|
||||
|
||||
import { isMacOS, isWindows } from '../../../utils';
|
||||
import { isLinux, isMacOS } from '../../../utils';
|
||||
import { store } from '../settings';
|
||||
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
export const enableMediaKeys = (window: BrowserWindow | null) => {
|
||||
if (isMacOS()) {
|
||||
@@ -25,10 +25,10 @@ export const enableMediaKeys = (window: BrowserWindow | null) => {
|
||||
}
|
||||
}
|
||||
|
||||
const enableWindowsMediaSession = store.get('mediaSession', false) as boolean;
|
||||
const playbackType = store.get('playbackType', PlaybackType.WEB) as PlaybackType;
|
||||
const enableMediaSession = store.get('mediaSession', false) as boolean;
|
||||
const playbackType = store.get('playbackType', PlayerType.WEB) as PlayerType;
|
||||
|
||||
if (!enableWindowsMediaSession || !isWindows() || playbackType !== PlaybackType.WEB) {
|
||||
if (!enableMediaSession || isLinux() || playbackType !== PlayerType.WEB) {
|
||||
globalShortcut.register('MediaStop', () => {
|
||||
window?.webContents.send('renderer-player-stop');
|
||||
});
|
||||
|
||||
@@ -382,7 +382,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
getMainWindow()?.webContents.send('request-favorite', {
|
||||
favorite,
|
||||
id,
|
||||
serverId: currentState.song.serverId,
|
||||
serverId: currentState.song._serverId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -443,7 +443,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
getMainWindow()?.webContents.send('request-rating', {
|
||||
id,
|
||||
rating,
|
||||
serverId: currentState.song.serverId,
|
||||
serverId: currentState.song._serverId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -578,7 +578,7 @@ ipcMain.on('remote-username', (_event, username: string) => {
|
||||
});
|
||||
|
||||
ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids: string[]) => {
|
||||
if (currentState.song?.serverId !== serverId) return;
|
||||
if (currentState.song?._serverId !== serverId) return;
|
||||
|
||||
const id = currentState.song.id;
|
||||
|
||||
@@ -592,7 +592,7 @@ ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids:
|
||||
});
|
||||
|
||||
ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: string[]) => {
|
||||
if (currentState.song?.serverId !== serverId) return;
|
||||
if (currentState.song?._serverId !== serverId) return;
|
||||
|
||||
const id = currentState.song.id;
|
||||
|
||||
@@ -657,6 +657,9 @@ if (mprisPlayer) {
|
||||
}
|
||||
currentState.volume = volume;
|
||||
broadcast({ data: volume, event: 'volume' });
|
||||
getMainWindow()?.webContents.send('request-volume', {
|
||||
volume,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,46 @@ import type { TitleTheme } from '/@/shared/types/types';
|
||||
import { dialog, ipcMain, nativeTheme, OpenDialogOptions, safeStorage } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
|
||||
export const store = new Store({
|
||||
const getFrame = () => {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
|
||||
if (isWindows) {
|
||||
return 'windows';
|
||||
}
|
||||
|
||||
if (isMacOS) {
|
||||
return 'macOS';
|
||||
}
|
||||
|
||||
return 'linux';
|
||||
};
|
||||
|
||||
export const store = new Store<any>({
|
||||
beforeEachMigration: (_store, context) => {
|
||||
console.log(`settings migrate from ${context.fromVersion} → ${context.toVersion}`);
|
||||
},
|
||||
defaults: {
|
||||
disable_auto_updates: false,
|
||||
enableNeteaseTranslation: false,
|
||||
global_media_hotkeys: true,
|
||||
mediaSession: false,
|
||||
playbackType: 'web',
|
||||
should_prompt_accessibility: true,
|
||||
shown_accessibility_warning: false,
|
||||
window_enable_tray: true,
|
||||
window_exit_to_tray: false,
|
||||
window_minimize_to_tray: false,
|
||||
window_start_minimized: false,
|
||||
window_window_bar_style: getFrame(),
|
||||
},
|
||||
migrations: {
|
||||
'>=0.21.2': (store) => {
|
||||
store.set('window_bar_style', 'linux');
|
||||
},
|
||||
'>=1.0.0': (store) => {
|
||||
store.clear();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ ipcMain.on('update-position', (_event, arg: number) => {
|
||||
mprisPlayer.getPosition = () => arg * 1e6;
|
||||
});
|
||||
|
||||
ipcMain.on('mpris-update-seek', (_event, arg) => {
|
||||
ipcMain.on('update-seek', (_event, arg) => {
|
||||
mprisPlayer.seeked(arg * 1e6);
|
||||
});
|
||||
|
||||
@@ -141,48 +141,48 @@ ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
|
||||
mprisPlayer.shuffle = shuffle;
|
||||
});
|
||||
|
||||
ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
|
||||
try {
|
||||
if (!song?.id) {
|
||||
mprisPlayer.metadata = {};
|
||||
return;
|
||||
ipcMain.on(
|
||||
'update-song',
|
||||
(_event, song: QueueSong | undefined, imageUrl: null | string | undefined) => {
|
||||
try {
|
||||
if (!song?.id) {
|
||||
mprisPlayer.metadata = {};
|
||||
return;
|
||||
}
|
||||
|
||||
mprisPlayer.metadata = {
|
||||
'mpris:artUrl': imageUrl || null,
|
||||
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e3) : null,
|
||||
'mpris:trackid': song.id
|
||||
? mprisPlayer.objectPath(`track/${song.id?.replace('-', '')}`)
|
||||
: '',
|
||||
'xesam:album': song.album || null,
|
||||
'xesam:albumArtist': song.albumArtists?.length
|
||||
? song.albumArtists.map((artist) => artist.name)
|
||||
: null,
|
||||
'xesam:artist': song.artists?.length
|
||||
? song.artists.map((artist) => artist.name)
|
||||
: null,
|
||||
'xesam:audioBpm': song.bpm,
|
||||
// Comment is a `list of strings` type
|
||||
'xesam:comment': song.comment ? [song.comment] : null,
|
||||
'xesam:contentCreated': song.releaseDate,
|
||||
'xesam:discNumber': song.discNumber ? song.discNumber : null,
|
||||
'xesam:genre': song.genres?.length
|
||||
? song.genres.map((genre: any) => genre.name)
|
||||
: null,
|
||||
'xesam:lastUsed': song.lastPlayedAt,
|
||||
'xesam:title': song.name || null,
|
||||
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
|
||||
'xesam:useCount':
|
||||
song.playCount !== null && song.playCount !== undefined ? song.playCount : null,
|
||||
// User ratings are only on Navidrome/Subsonic and are on a scale of 1-5
|
||||
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const upsizedImageUrl = song.imageUrl
|
||||
? song.imageUrl
|
||||
?.replace(/&size=\d+/, '&size=300')
|
||||
.replace(/\?width=\d+/, '?width=300')
|
||||
.replace(/&height=\d+/, '&height=300')
|
||||
: null;
|
||||
|
||||
mprisPlayer.metadata = {
|
||||
'mpris:artUrl': upsizedImageUrl,
|
||||
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e3) : null,
|
||||
'mpris:trackid': song.id
|
||||
? mprisPlayer.objectPath(`track/${song.id?.replace('-', '')}`)
|
||||
: '',
|
||||
'xesam:album': song.album || null,
|
||||
'xesam:albumArtist': song.albumArtists?.length
|
||||
? song.albumArtists.map((artist) => artist.name)
|
||||
: null,
|
||||
'xesam:artist': song.artists?.length ? song.artists.map((artist) => artist.name) : null,
|
||||
'xesam:audioBpm': song.bpm,
|
||||
// Comment is a `list of strings` type
|
||||
'xesam:comment': song.comment ? [song.comment] : null,
|
||||
'xesam:contentCreated': song.releaseDate,
|
||||
'xesam:discNumber': song.discNumber ? song.discNumber : null,
|
||||
'xesam:genre': song.genres?.length ? song.genres.map((genre: any) => genre.name) : null,
|
||||
'xesam:lastUsed': song.lastPlayedAt,
|
||||
'xesam:title': song.name || null,
|
||||
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
|
||||
'xesam:useCount':
|
||||
song.playCount !== null && song.playCount !== undefined ? song.playCount : null,
|
||||
// User ratings are only on Navidrome/Subsonic and are on a scale of 1-5
|
||||
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export { mprisPlayer };
|
||||
|
||||
+25
-82
@@ -19,9 +19,8 @@ import {
|
||||
import electronLocalShortcut from 'electron-localshortcut';
|
||||
import log from 'electron-log/main';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { access, constants, readFile, writeFile } from 'fs';
|
||||
import { access, constants } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
import { deflate, inflate } from 'zlib';
|
||||
|
||||
import packageJson from '../../package.json';
|
||||
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
||||
@@ -31,6 +30,7 @@ import MenuBuilder from './menu';
|
||||
import {
|
||||
autoUpdaterLogInterface,
|
||||
createLog,
|
||||
disableAutoUpdates,
|
||||
hotkeyToElectronAccelerator,
|
||||
isLinux,
|
||||
isMacOS,
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
} from './utils';
|
||||
import './features';
|
||||
|
||||
import { PlaybackType, TitleTheme } from '/@/shared/types/types';
|
||||
import { PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||
|
||||
export default class AppUpdater {
|
||||
constructor() {
|
||||
@@ -115,10 +115,10 @@ const installExtensions = async () => {
|
||||
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
|
||||
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
|
||||
|
||||
return installer
|
||||
.default(
|
||||
installer
|
||||
.installExtension(
|
||||
extensions.map((name) => installer[name]),
|
||||
forceDownload,
|
||||
{ forceDownload },
|
||||
)
|
||||
.then((installedExtensions) => {
|
||||
createLog({
|
||||
@@ -372,36 +372,6 @@ async function createWindow(first = true): Promise<void> {
|
||||
disableMediaKeys();
|
||||
});
|
||||
|
||||
ipcMain.on('player-restore-queue', () => {
|
||||
if (store.get('resume')) {
|
||||
const queueLocation = join(app.getPath('userData'), 'queue');
|
||||
|
||||
access(queueLocation, constants.F_OK, (accessError) => {
|
||||
if (accessError) {
|
||||
console.error('unable to access saved queue: ', accessError);
|
||||
return;
|
||||
}
|
||||
|
||||
readFile(queueLocation, (readError, buffer) => {
|
||||
if (readError) {
|
||||
console.error('failed to read saved queue: ', readError);
|
||||
return;
|
||||
}
|
||||
|
||||
inflate(buffer, (decompressError, data) => {
|
||||
if (decompressError) {
|
||||
console.error('failed to decompress queue: ', decompressError);
|
||||
return;
|
||||
}
|
||||
|
||||
const queue = JSON.parse(data.toString());
|
||||
getMainWindow()?.webContents.send('renderer-restore-queue', queue);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('download-url', (_event, url: string) => {
|
||||
mainWindow?.webContents.downloadURL(url);
|
||||
});
|
||||
@@ -442,8 +412,6 @@ async function createWindow(first = true): Promise<void> {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
let saved = false;
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
store.set('bounds', mainWindow?.getNormalBounds());
|
||||
store.set('maximized', mainWindow?.isMaximized());
|
||||
@@ -454,46 +422,8 @@ async function createWindow(first = true): Promise<void> {
|
||||
mainWindow?.hide();
|
||||
}
|
||||
|
||||
if (!saved && store.get('resume')) {
|
||||
event.preventDefault();
|
||||
saved = true;
|
||||
|
||||
ipcMain.once('player-save-queue', async (_event, data: Record<string, any>) => {
|
||||
const queueLocation = join(app.getPath('userData'), 'queue');
|
||||
const serialized = JSON.stringify(data);
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
deflate(serialized, { level: 1 }, (error, deflated) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
writeFile(queueLocation, deflated, (writeError) => {
|
||||
if (writeError) {
|
||||
reject(writeError);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('error saving queue state: ', error);
|
||||
} finally {
|
||||
if (!isMacOS()) {
|
||||
mainWindow?.close();
|
||||
}
|
||||
if (forceQuit) {
|
||||
app.exit();
|
||||
}
|
||||
}
|
||||
});
|
||||
getMainWindow()?.webContents.send('renderer-save-queue');
|
||||
} else {
|
||||
if (forceQuit) {
|
||||
app.exit();
|
||||
}
|
||||
if (forceQuit) {
|
||||
app.exit();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -527,7 +457,7 @@ async function createWindow(first = true): Promise<void> {
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
if (store.get('disable_auto_updates') !== true) {
|
||||
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
|
||||
new AppUpdater();
|
||||
}
|
||||
|
||||
@@ -548,10 +478,15 @@ async function createWindow(first = true): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const enableWindowsMediaSession = store.get('mediaSession', false) as boolean;
|
||||
const playbackType = store.get('playbackType', PlaybackType.WEB) as PlaybackType;
|
||||
// Only allow hardware media key handling if:
|
||||
// 1. The "Enable Media Session" setting is enabled
|
||||
// 2. The playback type is WEB (mpv not supported)
|
||||
// 3. The platform is not Linux (because we are using mpris instead)
|
||||
const enableMediaSession = store.get('mediaSession', false) as boolean;
|
||||
const playbackType = store.get('playbackType', PlayerType.WEB) as PlayerType;
|
||||
const shouldDisableMediaFeatures =
|
||||
!isWindows() || !enableWindowsMediaSession || playbackType !== PlaybackType.WEB;
|
||||
isLinux() || !enableMediaSession || playbackType !== PlayerType.WEB;
|
||||
|
||||
if (shouldDisableMediaFeatures) {
|
||||
app.commandLine.appendSwitch(
|
||||
'disable-features',
|
||||
@@ -768,3 +703,11 @@ if (!ipcMain.eventNames().includes('open-item')) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Register 'open-application-directory' handler globally, ensuring it is only registered once
|
||||
if (!ipcMain.eventNames().includes('open-application-directory')) {
|
||||
ipcMain.handle('open-application-directory', async () => {
|
||||
const userDataPath = app.getPath('userData');
|
||||
shell.openPath(userDataPath);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ if (process.env.NODE_ENV === 'development') {
|
||||
};
|
||||
}
|
||||
|
||||
export const disableAutoUpdates = () => {
|
||||
return process.env['DISABLE_AUTO_UPDATES'];
|
||||
};
|
||||
|
||||
export const isMacOS = () => {
|
||||
return process.platform === 'darwin';
|
||||
};
|
||||
|
||||
+40
-9
@@ -1,21 +1,42 @@
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
|
||||
import { PlayerRepeat } from '/@/shared/types/types';
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
const updatePosition = (timeSec: number) => {
|
||||
ipcRenderer.send('mpris-update-position', timeSec);
|
||||
ipcRenderer.send('update-position', timeSec);
|
||||
};
|
||||
|
||||
const updateSeek = (timeSec: number) => {
|
||||
ipcRenderer.send('mpris-update-seek', timeSec);
|
||||
ipcRenderer.send('update-seek', timeSec);
|
||||
};
|
||||
|
||||
const toggleRepeat = () => {
|
||||
ipcRenderer.send('mpris-toggle-repeat');
|
||||
const updateVolume = (volume: number) => {
|
||||
ipcRenderer.send('update-volume', volume);
|
||||
};
|
||||
|
||||
const toggleShuffle = () => {
|
||||
ipcRenderer.send('mpris-toggle-shuffle');
|
||||
const updateStatus = (status: PlayerStatus) => {
|
||||
ipcRenderer.send('update-playback', status);
|
||||
};
|
||||
|
||||
const updateRepeat = (repeat: PlayerRepeat) => {
|
||||
ipcRenderer.send('update-repeat', repeat);
|
||||
};
|
||||
|
||||
const updateShuffle = (shuffle: boolean) => {
|
||||
ipcRenderer.send('update-shuffle', shuffle);
|
||||
};
|
||||
|
||||
const updateSong = (song: QueueSong | undefined, imageUrl?: null | string) => {
|
||||
ipcRenderer.send('update-song', song, imageUrl);
|
||||
};
|
||||
|
||||
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
||||
ipcRenderer.on('request-seek', cb);
|
||||
};
|
||||
|
||||
const requestPosition = (cb: (event: IpcRendererEvent, data: { position: number }) => void) => {
|
||||
ipcRenderer.on('request-position', cb);
|
||||
};
|
||||
|
||||
const requestToggleRepeat = (
|
||||
@@ -30,13 +51,23 @@ const requestToggleShuffle = (
|
||||
ipcRenderer.on('mpris-request-toggle-shuffle', cb);
|
||||
};
|
||||
|
||||
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
|
||||
ipcRenderer.on('request-volume', cb);
|
||||
};
|
||||
|
||||
export const mpris = {
|
||||
requestPosition,
|
||||
requestSeek,
|
||||
requestToggleRepeat,
|
||||
requestToggleShuffle,
|
||||
toggleRepeat,
|
||||
toggleShuffle,
|
||||
requestVolume,
|
||||
updatePosition,
|
||||
updateRepeat,
|
||||
updateSeek,
|
||||
updateShuffle,
|
||||
updateSong,
|
||||
updateStatus,
|
||||
updateVolume,
|
||||
};
|
||||
|
||||
export type Mpris = typeof mpris;
|
||||
|
||||
@@ -86,6 +86,18 @@ const getCurrentTime = async () => {
|
||||
return ipcRenderer.invoke('player-get-time');
|
||||
};
|
||||
|
||||
const updateMetadata = (data: PlayerData) => {
|
||||
ipcRenderer.send('player-update-metadata', data);
|
||||
};
|
||||
|
||||
const getMetadata = async () => {
|
||||
return ipcRenderer.invoke('player-metadata');
|
||||
};
|
||||
|
||||
const getStreamMetadata = async () => {
|
||||
return ipcRenderer.invoke('player-stream-metadata');
|
||||
};
|
||||
|
||||
const rendererAutoNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-auto-next', cb);
|
||||
};
|
||||
@@ -163,6 +175,8 @@ export const mpvPlayer = {
|
||||
cleanup,
|
||||
currentTime,
|
||||
getCurrentTime,
|
||||
getMetadata,
|
||||
getStreamMetadata,
|
||||
initialize,
|
||||
isRunning,
|
||||
mute,
|
||||
@@ -178,6 +192,7 @@ export const mpvPlayer = {
|
||||
setQueue,
|
||||
setQueueNext,
|
||||
stop,
|
||||
updateMetadata,
|
||||
volume,
|
||||
};
|
||||
|
||||
|
||||
+5
-19
@@ -1,25 +1,13 @@
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
|
||||
import { isLinux, isMacOS, isWindows } from '../main/utils';
|
||||
|
||||
const saveQueue = (data: Record<string, any>) => {
|
||||
ipcRenderer.send('player-save-queue', data);
|
||||
};
|
||||
|
||||
const restoreQueue = () => {
|
||||
ipcRenderer.send('player-restore-queue');
|
||||
};
|
||||
import { disableAutoUpdates, isLinux, isMacOS, isWindows } from '../main/utils';
|
||||
|
||||
const openItem = async (path: string) => {
|
||||
return ipcRenderer.invoke('open-item', path);
|
||||
};
|
||||
|
||||
const onSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-save-queue', cb);
|
||||
};
|
||||
|
||||
const onRestoreQueue = (cb: (event: IpcRendererEvent, data: Partial<any>) => void) => {
|
||||
ipcRenderer.on('renderer-restore-queue', cb);
|
||||
const openApplicationDirectory = async () => {
|
||||
return ipcRenderer.invoke('open-application-directory');
|
||||
};
|
||||
|
||||
const playerErrorListener = (cb: (event: IpcRendererEvent, data: { code: number }) => void) => {
|
||||
@@ -52,18 +40,16 @@ const download = (url: string) => {
|
||||
};
|
||||
|
||||
export const utils = {
|
||||
disableAutoUpdates,
|
||||
download,
|
||||
isLinux,
|
||||
isMacOS,
|
||||
isWindows,
|
||||
logger,
|
||||
mainMessageListener,
|
||||
onRestoreQueue,
|
||||
onSaveQueue,
|
||||
openApplicationDirectory,
|
||||
openItem,
|
||||
playerErrorListener,
|
||||
restoreQueue,
|
||||
saveQueue,
|
||||
};
|
||||
|
||||
export type Utils = typeof utils;
|
||||
|
||||
@@ -96,7 +96,7 @@ export const RemoteContainer = () => {
|
||||
}}
|
||||
variant="transparent"
|
||||
/>
|
||||
{(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && (
|
||||
{(song?._serverType === 'navidrome' || song?._serverType === 'subsonic') && (
|
||||
<div style={{ margin: 'auto' }}>
|
||||
<Tooltip label="Double click to clear" openDelay={1000}>
|
||||
<Rating
|
||||
|
||||
+170
-5
@@ -3,6 +3,8 @@ import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { createWithEqualityFn } from 'zustand/traditional';
|
||||
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/shared/types/remote-types';
|
||||
|
||||
@@ -40,6 +42,9 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
immer((set, get) => ({
|
||||
actions: {
|
||||
reconnect: async () => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].reconnectInitiated, {
|
||||
category: LogCategory.REMOTE,
|
||||
});
|
||||
const existing = get().socket;
|
||||
|
||||
if (existing) {
|
||||
@@ -47,6 +52,10 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
existing.readyState === WebSocket.OPEN ||
|
||||
existing.readyState === WebSocket.CONNECTING
|
||||
) {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].closingExistingSocket, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { readyState: existing.readyState },
|
||||
});
|
||||
existing.natural = true;
|
||||
existing.close(4001);
|
||||
}
|
||||
@@ -55,28 +64,63 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
let authHeader: string | undefined;
|
||||
|
||||
try {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].fetchingCredentials, {
|
||||
category: LogCategory.REMOTE,
|
||||
});
|
||||
const credentials = await fetch('/credentials');
|
||||
authHeader = await credentials.text();
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].credentialsFetched, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { hasAuthHeader: !!authHeader },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get credentials', error);
|
||||
logFn.error(logMsg[LogCategory.REMOTE].failedToGetCredentials, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { error },
|
||||
});
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const socket = new WebSocket(
|
||||
location.href.replace('http', 'ws'),
|
||||
) as StatefulWebSocket;
|
||||
const wsUrl = location.href.replace('http', 'ws');
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].creatingWebSocket, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { url: wsUrl },
|
||||
});
|
||||
const socket = new WebSocket(wsUrl) as StatefulWebSocket;
|
||||
|
||||
socket.natural = false;
|
||||
|
||||
socket.addEventListener('message', (message) => {
|
||||
const { data, event } = JSON.parse(message.data) as ServerEvent;
|
||||
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].webSocketMessageReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { data, event },
|
||||
});
|
||||
|
||||
switch (event) {
|
||||
case 'error': {
|
||||
logFn.error(
|
||||
logMsg[LogCategory.REMOTE].webSocketErrorEvent,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { data },
|
||||
},
|
||||
);
|
||||
toast.error({ message: data, title: 'Socket error' });
|
||||
break;
|
||||
}
|
||||
case 'favorite': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].favoriteEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
favorite: data.favorite,
|
||||
id: data.id,
|
||||
},
|
||||
},
|
||||
);
|
||||
set((state) => {
|
||||
if (state.info.song?.id === data.id) {
|
||||
state.info.song.userFavorite = data.favorite;
|
||||
@@ -85,18 +129,39 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
break;
|
||||
}
|
||||
case 'playback': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].playbackEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { status: data },
|
||||
},
|
||||
);
|
||||
set((state) => {
|
||||
state.info.status = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'position': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].positionEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { position: data },
|
||||
},
|
||||
);
|
||||
set((state) => {
|
||||
state.info.position = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'proxy': {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].proxyEventReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
dataLength: data?.length,
|
||||
hasData: !!data,
|
||||
},
|
||||
});
|
||||
set((state) => {
|
||||
if (state.info.song) {
|
||||
state.info.song.imageUrl = `data:image/jpeg;base64,${data}`;
|
||||
@@ -105,6 +170,16 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
break;
|
||||
}
|
||||
case 'rating': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].ratingEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
id: data.id,
|
||||
rating: data.rating,
|
||||
},
|
||||
},
|
||||
);
|
||||
set((state) => {
|
||||
if (state.info.song?.id === data.id) {
|
||||
state.info.song.userRating = data.rating;
|
||||
@@ -113,30 +188,68 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
break;
|
||||
}
|
||||
case 'repeat': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].repeatEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { repeat: data },
|
||||
},
|
||||
);
|
||||
set((state) => {
|
||||
state.info.repeat = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'shuffle': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].shuffleEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { shuffle: data },
|
||||
},
|
||||
);
|
||||
set((state) => {
|
||||
state.info.shuffle = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'song': {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].songEventReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
artistName: data?.artistName,
|
||||
id: data?.id,
|
||||
name: data?.name,
|
||||
},
|
||||
});
|
||||
set((state) => {
|
||||
state.info.song = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'state': {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].stateEventReceived, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
hasSong: !!data.song,
|
||||
position: data.position,
|
||||
status: data.status,
|
||||
volume: data.volume,
|
||||
},
|
||||
});
|
||||
set((state) => {
|
||||
state.info = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'volume': {
|
||||
logFn.debug(
|
||||
logMsg[LogCategory.REMOTE].volumeEventReceived,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { volume: data },
|
||||
},
|
||||
);
|
||||
set((state) => {
|
||||
state.info.volume = data;
|
||||
});
|
||||
@@ -145,7 +258,17 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
});
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].webSocketOpened, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
hasAuthHeader: !!authHeader,
|
||||
readyState: socket.readyState,
|
||||
},
|
||||
});
|
||||
if (authHeader) {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].sendingAuthentication, {
|
||||
category: LogCategory.REMOTE,
|
||||
});
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
event: 'authenticate',
|
||||
@@ -157,14 +280,40 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
});
|
||||
|
||||
socket.addEventListener('close', (reason) => {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].webSocketClosed, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
code: reason.code,
|
||||
natural: socket.natural,
|
||||
reason: reason.reason,
|
||||
wasClean: reason.wasClean,
|
||||
},
|
||||
});
|
||||
if (reason.code === 4002 || reason.code === 4003) {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].reloadingPage, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { code: reason.code },
|
||||
});
|
||||
location.reload();
|
||||
} else if (reason.code === 4000) {
|
||||
logFn.warn(logMsg[LogCategory.REMOTE].serverIsDown, {
|
||||
category: LogCategory.REMOTE,
|
||||
});
|
||||
toast.warn({
|
||||
message: 'Feishin remote server is down',
|
||||
title: 'Connection closed',
|
||||
});
|
||||
} else if (reason.code !== 4001 && !socket.natural) {
|
||||
logFn.error(
|
||||
logMsg[LogCategory.REMOTE].socketClosedUnexpectedly,
|
||||
{
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
code: reason.code,
|
||||
reason: reason.reason,
|
||||
},
|
||||
},
|
||||
);
|
||||
toast.error({
|
||||
message: 'Socket closed for unexpected reason',
|
||||
title: 'Connection closed',
|
||||
@@ -180,7 +329,23 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
});
|
||||
},
|
||||
send: (data: ClientEvent) => {
|
||||
get().socket?.send(JSON.stringify(data));
|
||||
const socket = get().socket;
|
||||
if (socket) {
|
||||
logFn.debug(logMsg[LogCategory.REMOTE].sendingEventToServer, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: {
|
||||
data: data,
|
||||
event: data.event,
|
||||
readyState: socket.readyState,
|
||||
},
|
||||
});
|
||||
socket.send(JSON.stringify(data));
|
||||
} else {
|
||||
logFn.warn(logMsg[LogCategory.REMOTE].cannotSendEvent, {
|
||||
category: LogCategory.REMOTE,
|
||||
meta: { event: data.event },
|
||||
});
|
||||
}
|
||||
},
|
||||
toggleIsDark: () => {
|
||||
set((state) => {
|
||||
|
||||
+218
-14
@@ -2,6 +2,7 @@ import i18n from '/@/i18n/i18n';
|
||||
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
||||
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
|
||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder';
|
||||
import { getServerById, useAuthStore } from '/@/renderer/store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
@@ -99,6 +100,20 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
createInternetRadioStation(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: createInternetRadioStation`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'createInternetRadioStation',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
createPlaylist(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
@@ -127,6 +142,20 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
deleteInternetRadioStation(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteInternetRadioStation`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'deleteInternetRadioStation',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
deletePlaylist(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
@@ -167,7 +196,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getAlbumArtistList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getAlbumArtistListCount(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -181,7 +214,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getAlbumArtistListCount',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getAlbumDetail(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -223,7 +260,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getAlbumList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getAlbumListCount(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -237,7 +278,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getAlbumListCount',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getArtistList(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -251,7 +296,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getArtistList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getArtistListCount(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -265,6 +314,24 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getArtistListCount',
|
||||
server.type,
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getArtistRadio(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getArtistRadio`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'getArtistRadio',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
getDownloadUrl(args) {
|
||||
@@ -281,6 +348,24 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
getFolder(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getFolder`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'getFolder',
|
||||
server.type,
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getGenreList(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
@@ -293,6 +378,37 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getGenreList',
|
||||
server.type,
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getImageUrl(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
apiController(
|
||||
'getImageUrl',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }) || null
|
||||
);
|
||||
},
|
||||
getInternetRadioStations(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getInternetRadioStations`,
|
||||
);
|
||||
}
|
||||
return apiController(
|
||||
'getInternetRadioStations',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
getLyrics(args) {
|
||||
@@ -379,6 +495,20 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
getPlayQueue(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlayQueue`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'getPlayQueue',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
getRandomSongList(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
@@ -391,7 +521,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getRandomSongList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getRoles(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -433,7 +567,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getSimilarSongs',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getSongDetail(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -461,7 +599,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getSongList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getSongListCount(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -475,6 +617,22 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'getSongListCount',
|
||||
server.type,
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getStreamUrl(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'getStreamUrl',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
getStructuredLyrics(args) {
|
||||
@@ -491,7 +649,7 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
getTags(args) {
|
||||
getTagList(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
@@ -501,7 +659,7 @@ export const controller: GeneralController = {
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'getTags',
|
||||
'getTagList',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
@@ -519,17 +677,17 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
getTranscodingUrl(args) {
|
||||
getUserInfo(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getTranscodingUrl`,
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getUserInfo`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'getTranscodingUrl',
|
||||
'getUserInfo',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
@@ -575,6 +733,34 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
replacePlaylist(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: replacePlaylist`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'replacePlaylist',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
savePlayQueue(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: savePlayQueue`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'savePlayQueue',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
scrobble(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
@@ -601,7 +787,11 @@ export const controller: GeneralController = {
|
||||
return apiController(
|
||||
'search',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
)?.({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
setRating(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
@@ -631,6 +821,20 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
updateInternetRadioStation(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: updateInternetRadioStation`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'updateInternetRadioStation',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
updatePlaylist(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
|
||||
@@ -116,6 +116,15 @@ export const contract = c.router({
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
getFolder: {
|
||||
method: 'GET',
|
||||
path: 'users/:userId/items',
|
||||
query: jfType._parameters.folder,
|
||||
responses: {
|
||||
200: jfType._response.folderList,
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
getGenreList: {
|
||||
method: 'GET',
|
||||
path: 'musicgenres',
|
||||
@@ -169,6 +178,15 @@ export const contract = c.router({
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
getPlayQueue: {
|
||||
method: 'GET',
|
||||
path: 'sessions',
|
||||
query: jfType._parameters.getQueue,
|
||||
responses: {
|
||||
200: jfType._response.getSessions,
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
getServerInfo: {
|
||||
method: 'GET',
|
||||
path: 'system/info',
|
||||
@@ -238,6 +256,14 @@ export const contract = c.router({
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
getUser: {
|
||||
method: 'GET',
|
||||
path: 'users/:id',
|
||||
responses: {
|
||||
200: jfType._response.user,
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
movePlaylistItem: {
|
||||
body: null,
|
||||
method: 'POST',
|
||||
@@ -266,6 +292,15 @@ export const contract = c.router({
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
savePlayQueue: {
|
||||
body: jfType._parameters.saveQueue,
|
||||
method: 'POST',
|
||||
path: 'sessions/playing',
|
||||
responses: {
|
||||
200: jfType._response.scrobble,
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
scrobblePlaying: {
|
||||
body: jfType._parameters.scrobble,
|
||||
method: 'POST',
|
||||
@@ -413,7 +448,7 @@ export const jfApiClient = (args: {
|
||||
return {
|
||||
body: response?.data,
|
||||
headers: response?.headers as any,
|
||||
status: response.status,
|
||||
status: response?.status,
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { set } from 'idb-keyval';
|
||||
import chunk from 'lodash/chunk';
|
||||
import filter from 'lodash/filter';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
||||
import { useRadioStore } from '/@/renderer/features/radio/store/radio-store';
|
||||
import { jfNormalize } from '/@/shared/api/jellyfin/jellyfin-normalize';
|
||||
import { JFSongListSort, JFSortOrder, jfType } from '/@/shared/api/jellyfin/jellyfin-types';
|
||||
import { getFeatures, hasFeature, VersionInfo } from '/@/shared/api/utils';
|
||||
import { getFeatures, hasFeature, sortSongList, VersionInfo } from '/@/shared/api/utils';
|
||||
import {
|
||||
albumArtistListSortMap,
|
||||
albumListSortMap,
|
||||
Folder,
|
||||
genreListSortMap,
|
||||
InternalControllerEndpoint,
|
||||
LibraryItem,
|
||||
Played,
|
||||
playlistListSortMap,
|
||||
ServerType,
|
||||
Song,
|
||||
SongListSort,
|
||||
songListSortMap,
|
||||
SortOrder,
|
||||
sortOrderMap,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
@@ -84,6 +92,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return {
|
||||
credential: res.body.AccessToken,
|
||||
isAdmin: Boolean(res.body.User.Policy.IsAdministrator),
|
||||
userId: res.body.User.Id,
|
||||
username: res.body.User.Name,
|
||||
};
|
||||
@@ -107,6 +116,26 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
createInternetRadioStation: async (args) => {
|
||||
const { apiClientProps, body } = args;
|
||||
|
||||
if (!apiClientProps.serverId) {
|
||||
throw new Error('No serverId found');
|
||||
}
|
||||
|
||||
const state = useRadioStore.getState();
|
||||
if (!state?.actions?.createStation) {
|
||||
throw new Error('Radio store not initialized');
|
||||
}
|
||||
|
||||
state.actions.createStation(apiClientProps.serverId, {
|
||||
homepageUrl: body.homepageUrl || null,
|
||||
name: body.name,
|
||||
streamUrl: body.streamUrl,
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
createPlaylist: async (args) => {
|
||||
const { apiClientProps, body } = args;
|
||||
|
||||
@@ -150,6 +179,22 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
deleteInternetRadioStation: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
if (!apiClientProps.serverId) {
|
||||
throw new Error('No serverId found');
|
||||
}
|
||||
|
||||
const state = useRadioStore.getState();
|
||||
if (!state?.actions?.deleteStation) {
|
||||
throw new Error('Radio store not initialized');
|
||||
}
|
||||
|
||||
state.actions.deleteStation(apiClientProps.serverId, query.id);
|
||||
|
||||
return null;
|
||||
},
|
||||
deletePlaylist: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -208,7 +253,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
Fields: 'Genres, DateCreated, ExternalUrls, Overview',
|
||||
ImageTypeLimit: 1,
|
||||
Limit: query.limit,
|
||||
ParentId: query.musicFolderId,
|
||||
ParentId: getLibraryId(query.musicFolderId),
|
||||
Recursive: true,
|
||||
SearchTerm: query.searchTerm,
|
||||
SortBy: albumArtistListSortMap.jellyfin[query.sortBy] || 'SortName,Name',
|
||||
@@ -315,18 +360,18 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
},
|
||||
query: {
|
||||
...artistQuery,
|
||||
Fields: 'People, Tags',
|
||||
GenreIds: query.genres ? query.genres.join(',') : undefined,
|
||||
Fields: 'People, Tags, Studios',
|
||||
GenreIds: query.genreIds ? query.genreIds.join(',') : undefined,
|
||||
IncludeItemTypes: 'MusicAlbum',
|
||||
IsFavorite: query.favorite,
|
||||
Limit: query.limit,
|
||||
ParentId: query.musicFolderId,
|
||||
ParentId: getLibraryId(query.musicFolderId),
|
||||
Recursive: true,
|
||||
SearchTerm: query.searchTerm,
|
||||
SortBy: albumListSortMap.jellyfin[query.sortBy] || 'SortName',
|
||||
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||
StartIndex: query.startIndex,
|
||||
...query._custom?.jellyfin,
|
||||
...query._custom,
|
||||
Years: yearsFilter,
|
||||
},
|
||||
});
|
||||
@@ -354,7 +399,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
Fields: 'Genres, DateCreated, ExternalUrls, Overview',
|
||||
ImageTypeLimit: 1,
|
||||
Limit: query.limit,
|
||||
ParentId: query.musicFolderId,
|
||||
ParentId: getLibraryId(query.musicFolderId),
|
||||
Recursive: true,
|
||||
SearchTerm: query.searchTerm,
|
||||
SortBy: albumArtistListSortMap.jellyfin[query.sortBy] || 'SortName,Name',
|
||||
@@ -381,10 +426,238 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getArtistRadio: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
// For Jellyfin, use instant mix for artist radio
|
||||
const res = await jfApiClient(apiClientProps).getInstantMix({
|
||||
params: {
|
||||
itemId: query.artistId,
|
||||
},
|
||||
query: {
|
||||
Fields: 'Genres, DateCreated, MediaSources, ParentId',
|
||||
Limit: query.count,
|
||||
UserId: apiClientProps.server?.userId || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get artist radio songs');
|
||||
}
|
||||
|
||||
return res.body.Items.map((song) => jfNormalize.song(song, apiClientProps.server));
|
||||
},
|
||||
getDownloadUrl: (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
return `${apiClientProps.server?.url}/items/${query.id}/download?api_key=${apiClientProps.server?.credential}`;
|
||||
return `${apiClientProps.server?.url}/items/${query.id}/download?apiKey=${apiClientProps.server?.credential}`;
|
||||
},
|
||||
getFolder: async ({ apiClientProps, query }) => {
|
||||
const userId = apiClientProps.server?.userId;
|
||||
|
||||
if (!userId) throw new Error('No userId found');
|
||||
|
||||
const sortOrder = (query.sortOrder?.toLowerCase() ?? 'asc') as 'asc' | 'desc';
|
||||
const isRootFolderId = query.id === '0';
|
||||
|
||||
if (isRootFolderId) {
|
||||
if (query.musicFolderId) {
|
||||
// If music folder is provided, directly get the folder
|
||||
const musicFolderRes = await jfApiClient(apiClientProps).getFolder({
|
||||
params: {
|
||||
userId,
|
||||
},
|
||||
query: {
|
||||
ParentId: getLibraryId(query.musicFolderId)!,
|
||||
},
|
||||
});
|
||||
|
||||
if (musicFolderRes.status !== 200) {
|
||||
throw new Error('Failed to get music folder list');
|
||||
}
|
||||
|
||||
let items = musicFolderRes.body.Items.filter((item) => item.Type !== 'Audio');
|
||||
|
||||
if (query.searchTerm) {
|
||||
items = filter(items, (item) => {
|
||||
return item.Name.toLowerCase().includes(query.searchTerm!.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
const folders = items
|
||||
.filter((item) => item.Type !== 'Audio')
|
||||
.map((item) => jfNormalize.folder(item, apiClientProps.server));
|
||||
|
||||
const sortedFolders = orderBy(folders, [(v) => v.name.toLowerCase()], [sortOrder]);
|
||||
|
||||
return {
|
||||
_itemType: LibraryItem.FOLDER,
|
||||
_serverId: apiClientProps.server?.id || 'unknown',
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
children: {
|
||||
folders: sortedFolders,
|
||||
songs: [],
|
||||
},
|
||||
id: query.id,
|
||||
name: '~',
|
||||
parentId: undefined,
|
||||
};
|
||||
} else {
|
||||
// Use the root music folder list if no music folder id is provided
|
||||
const musicFolderRes = await jfApiClient(apiClientProps).getMusicFolderList({
|
||||
params: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (musicFolderRes.status !== 200) {
|
||||
throw new Error('Failed to get music folder list');
|
||||
}
|
||||
|
||||
let items = musicFolderRes.body.Items.filter((item) => item.Type !== 'Audio');
|
||||
|
||||
if (query.searchTerm) {
|
||||
items = filter(items, (item) => {
|
||||
return item.Name.toLowerCase().includes(query.searchTerm!.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
const folders = items
|
||||
.filter((item) => item.Type !== 'Audio')
|
||||
.map((item) =>
|
||||
jfNormalize.folder(
|
||||
item as unknown as z.infer<typeof jfType._response.folder>,
|
||||
apiClientProps.server,
|
||||
),
|
||||
);
|
||||
|
||||
const sortedFolders = orderBy(folders, [(v) => v.name.toLowerCase()], [sortOrder]);
|
||||
|
||||
return {
|
||||
_itemType: LibraryItem.FOLDER,
|
||||
_serverId: apiClientProps.server?.id || 'unknown',
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
children: {
|
||||
folders: sortedFolders,
|
||||
songs: [],
|
||||
},
|
||||
id: query.id,
|
||||
name: '~',
|
||||
parentId: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const folderDetailRes = await jfApiClient(apiClientProps).getFolder({
|
||||
params: {
|
||||
userId,
|
||||
},
|
||||
query: {
|
||||
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId',
|
||||
ParentId: query.id,
|
||||
SortBy: query.sortBy
|
||||
? (songListSortMap.jellyfin[query.sortBy] as string) || 'SortName'
|
||||
: 'SortName',
|
||||
SortOrder: sortOrderMap.jellyfin[query.sortOrder || SortOrder.ASC],
|
||||
},
|
||||
});
|
||||
|
||||
if (folderDetailRes.status !== 200) {
|
||||
throw new Error('Failed to get folder');
|
||||
}
|
||||
|
||||
// Get parent folder info - we'll use the first child's ParentId to infer the folder's parentId
|
||||
// The folder name will be inferred from the query.id or we can try to get it from a parent query
|
||||
let parentId: string | undefined;
|
||||
let folderName = 'Unknown folder';
|
||||
|
||||
if (folderDetailRes.body.Items?.length > 0) {
|
||||
const firstItem = folderDetailRes.body.Items[0];
|
||||
parentId = firstItem.ParentId;
|
||||
|
||||
// Try to get the folder name by querying its parent's children
|
||||
if (parentId) {
|
||||
const parentFolderRes = await jfApiClient(apiClientProps).getFolder({
|
||||
params: {
|
||||
userId,
|
||||
},
|
||||
query: {
|
||||
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId',
|
||||
ParentId: parentId,
|
||||
},
|
||||
});
|
||||
|
||||
if (parentFolderRes.status === 200) {
|
||||
const parentFolderItem = parentFolderRes.body.Items?.find(
|
||||
(item) => item.Id === query.id,
|
||||
);
|
||||
if (parentFolderItem) {
|
||||
folderName = parentFolderItem.Name || 'Unknown folder';
|
||||
parentId = parentFolderItem.ParentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const items = folderDetailRes.body.Items || [];
|
||||
|
||||
let filteredFolders = items
|
||||
.filter((item) => item.Type !== 'Audio')
|
||||
.map((item) => jfNormalize.folder(item, apiClientProps.server));
|
||||
let filteredSongs = items
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Type === 'Audio' &&
|
||||
(item as unknown as z.infer<typeof jfType._response.song>).MediaSources,
|
||||
)
|
||||
.map((item) =>
|
||||
jfNormalize.song(
|
||||
item as unknown as z.infer<typeof jfType._response.song>,
|
||||
apiClientProps.server,
|
||||
),
|
||||
);
|
||||
|
||||
if (query.searchTerm) {
|
||||
const searchTermLower = query.searchTerm.toLowerCase();
|
||||
filteredFolders = filter(filteredFolders, (f) =>
|
||||
f.name.toLowerCase().includes(searchTermLower),
|
||||
);
|
||||
filteredSongs = filter(filteredSongs, (s) => {
|
||||
const name = s.name?.toLowerCase() || '';
|
||||
const album = s.album?.toLowerCase() || '';
|
||||
const artist = s.artistName?.toLowerCase() || '';
|
||||
return (
|
||||
name.includes(searchTermLower) ||
|
||||
album.includes(searchTermLower) ||
|
||||
artist.includes(searchTermLower)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
filteredFolders = orderBy(filteredFolders, [(v) => v.name.toLowerCase()], [sortOrder]);
|
||||
|
||||
if (filteredSongs.length > 0) {
|
||||
filteredSongs = sortSongList(
|
||||
filteredSongs,
|
||||
query.sortBy || SongListSort.NAME,
|
||||
query.sortOrder || SortOrder.ASC,
|
||||
);
|
||||
}
|
||||
|
||||
const folder: Folder = {
|
||||
_itemType: LibraryItem.FOLDER,
|
||||
_serverId: apiClientProps.server?.id || 'unknown',
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
children: {
|
||||
folders: filteredFolders,
|
||||
songs: filteredSongs,
|
||||
},
|
||||
id: query.id,
|
||||
name: folderName,
|
||||
parentId,
|
||||
};
|
||||
|
||||
return folder;
|
||||
},
|
||||
getGenreList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
@@ -397,8 +670,8 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
query: {
|
||||
EnableTotalRecordCount: true,
|
||||
Fields: 'ItemCounts',
|
||||
Limit: query.limit,
|
||||
ParentId: query?.musicFolderId,
|
||||
Limit: query.limit === -1 ? undefined : query.limit,
|
||||
ParentId: getLibraryId(query.musicFolderId),
|
||||
Recursive: true,
|
||||
SearchTerm: query?.searchTerm,
|
||||
SortBy: genreListSortMap.jellyfin[query.sortBy] || 'SortName',
|
||||
@@ -418,6 +691,36 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
totalRecordCount: res.body?.TotalRecordCount || 0,
|
||||
};
|
||||
},
|
||||
getImageUrl: ({ apiClientProps: { server }, query }) => {
|
||||
const { id, size } = query;
|
||||
const imageSize = size;
|
||||
|
||||
if (!server?.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For Jellyfin, we construct the URL pattern
|
||||
// The server will return a 404 or placeholder if no image exists
|
||||
const baseUrl = `${server.url}/Items/${id}/Images/Primary?quality=96${imageSize ? `&width=${imageSize}` : ''}`;
|
||||
|
||||
// For songs, we might want to fall back to album art, but we don't have albumId here
|
||||
// The caller can handle this if needed
|
||||
return baseUrl;
|
||||
},
|
||||
getInternetRadioStations: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
if (!apiClientProps.serverId) {
|
||||
throw new Error('No serverId found');
|
||||
}
|
||||
|
||||
const state = useRadioStore.getState();
|
||||
if (!state?.actions?.getStations) {
|
||||
throw new Error('Radio store not initialized');
|
||||
}
|
||||
|
||||
return state.actions.getStations(apiClientProps.serverId);
|
||||
},
|
||||
getLyrics: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -552,11 +855,14 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
|
||||
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.TotalRecordCount,
|
||||
};
|
||||
},
|
||||
getPlayQueue: async () => {
|
||||
throw new Error('Not supported');
|
||||
},
|
||||
getRandomSongList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -588,7 +894,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
? true
|
||||
: undefined,
|
||||
Limit: query.limit,
|
||||
ParentId: query.musicFolderId,
|
||||
ParentId: getLibraryId(query.musicFolderId),
|
||||
Recursive: true,
|
||||
SortBy: JFSongListSort.RANDOM,
|
||||
SortOrder: JFSortOrder.ASC,
|
||||
@@ -602,7 +908,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
|
||||
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.Items.length || 0,
|
||||
};
|
||||
@@ -617,7 +923,12 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get server info');
|
||||
}
|
||||
|
||||
const features = getFeatures(VERSION_INFO, res.body.Version);
|
||||
const defaultFeatures = {};
|
||||
|
||||
const features = {
|
||||
...defaultFeatures,
|
||||
...getFeatures(VERSION_INFO, res.body.Version),
|
||||
};
|
||||
|
||||
return {
|
||||
features,
|
||||
@@ -647,7 +958,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
if (res.status === 200 && res.body.Items.length) {
|
||||
const results = res.body.Items.reduce<Song[]>((acc, song) => {
|
||||
if (song.Id !== query.songId) {
|
||||
acc.push(jfNormalize.song(song, apiClientProps.server, ''));
|
||||
acc.push(jfNormalize.song(song, apiClientProps.server));
|
||||
}
|
||||
|
||||
return acc;
|
||||
@@ -676,7 +987,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return mix.body.Items.reduce<Song[]>((acc, song) => {
|
||||
if (song.Id !== query.songId) {
|
||||
acc.push(jfNormalize.song(song, apiClientProps.server, ''));
|
||||
acc.push(jfNormalize.song(song, apiClientProps.server));
|
||||
}
|
||||
|
||||
return acc;
|
||||
@@ -696,7 +1007,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get song detail');
|
||||
}
|
||||
|
||||
return jfNormalize.song(res.body, apiClientProps.server, '');
|
||||
return jfNormalize.song(res.body, apiClientProps.server);
|
||||
},
|
||||
getSongList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
@@ -742,13 +1053,13 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
IncludeItemTypes: 'Audio',
|
||||
IsFavorite: query.favorite,
|
||||
Limit: query.limit,
|
||||
ParentId: query.musicFolderId,
|
||||
ParentId: getLibraryId(query.musicFolderId),
|
||||
Recursive: true,
|
||||
SearchTerm: query.searchTerm,
|
||||
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
|
||||
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||
StartIndex: query.startIndex,
|
||||
...query._custom?.jellyfin,
|
||||
...query._custom,
|
||||
Years: yearsFilter,
|
||||
},
|
||||
});
|
||||
@@ -765,25 +1076,25 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
? formatCommaDelimitedString(query.albumIds)
|
||||
: undefined;
|
||||
|
||||
const parentIdFilter = [albumIdsFilter, artistIdsFilter].filter(Boolean).join(',');
|
||||
|
||||
const res = await jfApiClient(apiClientProps).getSongList({
|
||||
params: {
|
||||
userId: apiClientProps.server?.userId,
|
||||
},
|
||||
query: {
|
||||
AlbumIds: albumIdsFilter,
|
||||
ArtistIds: artistIdsFilter,
|
||||
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags',
|
||||
GenreIds: query.genreIds?.join(','),
|
||||
IncludeItemTypes: 'Audio',
|
||||
IsFavorite: query.favorite,
|
||||
Limit: query.limit,
|
||||
ParentId: query.musicFolderId,
|
||||
ParentId: parentIdFilter,
|
||||
Recursive: true,
|
||||
SearchTerm: query.searchTerm,
|
||||
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
|
||||
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||
StartIndex: query.startIndex,
|
||||
...query._custom?.jellyfin,
|
||||
...query._custom,
|
||||
Years: yearsFilter,
|
||||
},
|
||||
});
|
||||
@@ -808,9 +1119,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: items.map((item) =>
|
||||
jfNormalize.song(item, apiClientProps.server, '', query.imageSize),
|
||||
),
|
||||
items: items.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount,
|
||||
};
|
||||
@@ -820,11 +1129,46 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getTags: async (args) => {
|
||||
getStreamUrl: ({ apiClientProps: { server }, query }) => {
|
||||
const { bitrate, format, id, transcode } = query;
|
||||
const deviceId = '';
|
||||
|
||||
let url = `${server?.url}/Items/${id}/Download?apiKey=${server?.credential}&playSessionId=${deviceId}`;
|
||||
|
||||
if (transcode) {
|
||||
// Some format appears to be required. Fall back to trusty MP3 if not specified
|
||||
// Otherwise, ffmpeg appears to crash
|
||||
const realFormat = format || 'mp3';
|
||||
|
||||
url =
|
||||
`${server?.url}/audio` +
|
||||
`/${id}/universal` +
|
||||
`?userId=${server?.userId}` +
|
||||
`&deviceId=${deviceId}` +
|
||||
'&audioCodec=aac' +
|
||||
`&apiKey=${server?.credential}` +
|
||||
`&playSessionId=${deviceId}` +
|
||||
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg';
|
||||
|
||||
url += `&transcodingProtocol=http&transcodingContainer=${realFormat}`;
|
||||
url = url.replace('audioCodec=aac', `audioCodec=${realFormat}`);
|
||||
url = url.replace(
|
||||
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg',
|
||||
`&container=${realFormat}`,
|
||||
);
|
||||
|
||||
if (bitrate !== undefined) {
|
||||
url += `&maxStreamingBitrate=${bitrate * 1000}`;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
getTagList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
if (!hasFeature(apiClientProps.server, ServerFeature.TAGS)) {
|
||||
return { boolTags: undefined, enumTags: undefined };
|
||||
return { boolTags: undefined, enumTags: undefined, excluded: { album: [], song: [] } };
|
||||
}
|
||||
|
||||
const res = await jfApiClient(apiClientProps).getFilterList({
|
||||
@@ -843,6 +1187,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
boolTags: res.body.Tags?.sort((a, b) =>
|
||||
a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()),
|
||||
),
|
||||
excluded: { album: [], song: [] },
|
||||
};
|
||||
},
|
||||
getTopSongs: async (args) => {
|
||||
@@ -862,7 +1207,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
IncludeItemTypes: 'Audio',
|
||||
Limit: query.limit,
|
||||
Recursive: true,
|
||||
SortBy: 'PlayCount,SortName',
|
||||
SortBy: 'CommunityRating,SortName',
|
||||
SortOrder: 'Descending',
|
||||
UserId: apiClientProps.server?.userId,
|
||||
},
|
||||
@@ -873,23 +1218,29 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
|
||||
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.TotalRecordCount,
|
||||
};
|
||||
},
|
||||
getTranscodingUrl: (args) => {
|
||||
const { base, bitrate, format } = args.query;
|
||||
let url = base.replace('transcodingProtocol=hls', 'transcodingProtocol=http');
|
||||
if (format) {
|
||||
url = url.replace('audioCodec=aac', `audioCodec=${format}`);
|
||||
url = url.replace('transcodingContainer=ts', `transcodingContainer=${format}`);
|
||||
}
|
||||
if (bitrate !== undefined) {
|
||||
url += `&maxStreamingBitrate=${bitrate * 1000}`;
|
||||
getUserInfo: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await jfApiClient(apiClientProps).getUser({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get user info');
|
||||
}
|
||||
|
||||
return url;
|
||||
return {
|
||||
id: res.body.Id,
|
||||
isAdmin: Boolean(res.body.Policy.IsAdministrator),
|
||||
name: res.body.Name,
|
||||
};
|
||||
},
|
||||
movePlaylistItem: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
@@ -928,6 +1279,116 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
replacePlaylist: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
if (!apiClientProps.server?.userId) {
|
||||
throw new Error('No userId found');
|
||||
}
|
||||
|
||||
// 1. Fetch existing songs from the playlist
|
||||
const existingSongsRes = await jfApiClient(apiClientProps).getPlaylistSongList({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags',
|
||||
IncludeItemTypes: 'Audio',
|
||||
UserId: apiClientProps.server?.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSongsRes.status !== 200) {
|
||||
throw new Error('Failed to fetch existing playlist songs');
|
||||
}
|
||||
|
||||
const existingSongs = existingSongsRes.body.Items.map((item) =>
|
||||
jfNormalize.song(item, apiClientProps.server),
|
||||
);
|
||||
|
||||
// 2. Get playlist detail to get the name
|
||||
const playlistDetailRes = await jfApiClient(apiClientProps).getPlaylistDetail({
|
||||
params: {
|
||||
id: query.id,
|
||||
userId: apiClientProps.server?.userId,
|
||||
},
|
||||
query: {
|
||||
Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId',
|
||||
Ids: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (playlistDetailRes.status !== 200) {
|
||||
throw new Error('Failed to get playlist detail');
|
||||
}
|
||||
|
||||
const playlist = jfNormalize.playlist(playlistDetailRes.body, apiClientProps.server);
|
||||
|
||||
// 3. Make a backup of the playlist ids and their order, along with the id of the playlist and name
|
||||
const backup = {
|
||||
id: query.id,
|
||||
name: playlist.name,
|
||||
songIds: existingSongs.map((song) => song.id),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Store backup in IndexedDB using idb-keyval
|
||||
const backupKey = `playlist-backup-${query.id}`;
|
||||
await set(backupKey, backup);
|
||||
|
||||
// 4. Remove all songs from the playlist
|
||||
if (existingSongs.length > 0) {
|
||||
const existingPlaylistItemIds = existingSongs
|
||||
.map((song) => song.playlistItemId)
|
||||
.filter((id): id is string => id !== undefined && id !== null);
|
||||
|
||||
if (existingPlaylistItemIds.length > 0) {
|
||||
const chunks = chunk(existingPlaylistItemIds, MAX_ITEMS_PER_PLAYLIST_ADD);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const removeRes = await jfApiClient(apiClientProps).removeFromPlaylist({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
EntryIds: chunk.join(','),
|
||||
},
|
||||
});
|
||||
|
||||
if (removeRes.status !== 204) {
|
||||
throw new Error('Failed to remove songs from playlist');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Add the new song ids to the playlist
|
||||
if (body.songId.length > 0) {
|
||||
const chunks = chunk(body.songId, MAX_ITEMS_PER_PLAYLIST_ADD);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const addRes = await jfApiClient(apiClientProps).addToPlaylist({
|
||||
body: null,
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
Ids: chunk.join(','),
|
||||
UserId: apiClientProps.server?.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (addRes.status !== 204) {
|
||||
throw new Error('Failed to add songs to playlist');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
savePlayQueue: async () => {
|
||||
throw new Error('Not supported');
|
||||
},
|
||||
scrobble: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -1082,9 +1543,29 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
jfNormalize.albumArtist(item, apiClientProps.server),
|
||||
),
|
||||
albums: albums.map((item) => jfNormalize.album(item, apiClientProps.server)),
|
||||
songs: songs.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
|
||||
songs: songs.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||
};
|
||||
},
|
||||
updateInternetRadioStation: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
if (!apiClientProps.serverId) {
|
||||
throw new Error('No serverId found');
|
||||
}
|
||||
|
||||
const state = useRadioStore.getState();
|
||||
if (!state?.actions?.updateStation) {
|
||||
throw new Error('Radio store not initialized');
|
||||
}
|
||||
|
||||
state.actions.updateStation(apiClientProps.serverId, query.id, {
|
||||
homepageUrl: body.homepageUrl || null,
|
||||
name: body.name,
|
||||
streamUrl: body.streamUrl,
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
updatePlaylist: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
@@ -1140,3 +1621,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
// totalRecordCount: res.body.TotalRecordCount,
|
||||
// };
|
||||
// };
|
||||
|
||||
function getLibraryId(musicFolderId?: string | string[]) {
|
||||
return Array.isArray(musicFolderId) ? musicFolderId[0] : musicFolderId;
|
||||
}
|
||||
|
||||
@@ -123,6 +123,14 @@ export const contract = c.router({
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
getQueue: {
|
||||
method: 'GET',
|
||||
path: 'queue',
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.queue),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
getSongDetail: {
|
||||
method: 'GET',
|
||||
path: 'song/:id',
|
||||
@@ -140,11 +148,12 @@ export const contract = c.router({
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
getTags: {
|
||||
getTagList: {
|
||||
method: 'GET',
|
||||
path: 'tag',
|
||||
query: ndType._parameters.tagList,
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.tags),
|
||||
200: resultWithHeaders(ndType._response.tagList),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
@@ -176,6 +185,15 @@ export const contract = c.router({
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
saveQueue: {
|
||||
body: ndType._parameters.saveQueue,
|
||||
method: 'POST',
|
||||
path: 'queue',
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.saveQueue),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
shareItem: {
|
||||
body: ndType._parameters.shareItem,
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { set } from 'idb-keyval';
|
||||
|
||||
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
import { ndNormalize } from '/@/shared/api/navidrome/navidrome-normalize';
|
||||
import { NDSongListSort } from '/@/shared/api/navidrome/navidrome-types';
|
||||
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
|
||||
import { getFeatures, hasFeature, VersionInfo } from '/@/shared/api/utils';
|
||||
import { getFeatures, hasFeature, hasFeatureWithVersion, VersionInfo } from '/@/shared/api/utils';
|
||||
import {
|
||||
albumArtistListSortMap,
|
||||
albumListSortMap,
|
||||
@@ -15,14 +17,17 @@ import {
|
||||
PlaylistSongListArgs,
|
||||
PlaylistSongListResponse,
|
||||
ServerListItemWithCredential,
|
||||
Song,
|
||||
songListSortMap,
|
||||
sortOrderMap,
|
||||
tagListSortMap,
|
||||
userListSortMap,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
const VERSION_INFO: VersionInfo = [
|
||||
// Why 2? Subsonic controller will return 1 for its own implementation
|
||||
// Use 2 to denote that Navidrome's own API has a different endpoint
|
||||
['0.57.0', { [ServerFeature.SERVER_PLAY_QUEUE]: [2] }],
|
||||
['0.56.0', { [ServerFeature.TRACK_ALBUM_ARTIST_SEARCH]: [1] }],
|
||||
['0.55.0', { [ServerFeature.BFR]: [1], [ServerFeature.TAGS]: [1] }],
|
||||
['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
|
||||
@@ -45,7 +50,31 @@ const NAVIDROME_ROLES: Array<string | { label: string; value: string }> = [
|
||||
'remixer',
|
||||
];
|
||||
|
||||
const EXCLUDED_TAGS = new Set<string>(['disctotal', 'genre', 'tracktotal']);
|
||||
// Tags that are irrelevant or non-functional as filters
|
||||
const EXCLUDED_TAGS = new Set<string>([
|
||||
'genre', // Duplicate of genre filter
|
||||
]);
|
||||
|
||||
const EXCLUDED_ALBUM_TAGS = new Set<string>([
|
||||
'asin',
|
||||
'barcode',
|
||||
'copyright',
|
||||
'disctotal',
|
||||
'encodedby',
|
||||
'isrc',
|
||||
'key',
|
||||
'language',
|
||||
'musicbrainz_workid',
|
||||
'script',
|
||||
'tracktotal',
|
||||
'website',
|
||||
'work',
|
||||
]);
|
||||
|
||||
const EXCLUDED_SONG_TAGS = new Set<string>([]);
|
||||
|
||||
// Tags that use IDs as values as opposed to the tag value
|
||||
const ID_TAGS = new Set<string>(['albumversion', 'mood']);
|
||||
|
||||
const excludeMissing = (server?: null | ServerListItemWithCredential) => {
|
||||
if (!server) {
|
||||
@@ -59,6 +88,14 @@ const excludeMissing = (server?: null | ServerListItemWithCredential) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getLibraryId = (musicFolderId?: string | string[]): string[] | undefined => {
|
||||
if (!musicFolderId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Array.isArray(musicFolderId) ? musicFolderId : [musicFolderId];
|
||||
};
|
||||
|
||||
const getArtistSongKey = (server: null | ServerListItemWithCredential) =>
|
||||
hasFeature(server, ServerFeature.TRACK_ALBUM_ARTIST_SEARCH) ? 'artists_id' : 'album_artist_id';
|
||||
|
||||
@@ -97,12 +134,14 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
|
||||
return {
|
||||
credential: `u=${body.username}&s=${res.body.data.subsonicSalt}&t=${res.body.data.subsonicToken}`,
|
||||
isAdmin: Boolean(res.body.data.isAdmin),
|
||||
ndCredential: res.body.data.token,
|
||||
userId: res.body.data.id,
|
||||
username: res.body.data.username,
|
||||
};
|
||||
},
|
||||
createFavorite: SubsonicController.createFavorite,
|
||||
createInternetRadioStation: SubsonicController.createInternetRadioStation,
|
||||
createPlaylist: async (args) => {
|
||||
const { apiClientProps, body } = args;
|
||||
|
||||
@@ -110,9 +149,10 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
body: {
|
||||
comment: body.comment,
|
||||
name: body.name,
|
||||
ownerId: body.ownerId,
|
||||
public: body.public,
|
||||
rules: body._custom?.navidrome?.rules,
|
||||
sync: body._custom?.navidrome?.sync,
|
||||
rules: body.queryBuilderRules,
|
||||
sync: body.sync,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -125,6 +165,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
};
|
||||
},
|
||||
deleteFavorite: SubsonicController.deleteFavorite,
|
||||
deleteInternetRadioStation: SubsonicController.deleteInternetRadioStation,
|
||||
deletePlaylist: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -190,8 +231,10 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: albumArtistListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
name: query.searchTerm,
|
||||
...query._custom?.navidrome,
|
||||
starred: query.favorite,
|
||||
...query._custom,
|
||||
role: hasFeature(apiClientProps.server, ServerFeature.BFR) ? 'albumartist' : '',
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
@@ -276,8 +319,12 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const genres = hasFeature(apiClientProps.server, ServerFeature.BFR)
|
||||
? query.genres
|
||||
: query.genres?.[0];
|
||||
? query.genreIds
|
||||
: query.genreIds?.[0];
|
||||
|
||||
const artistIds = hasFeature(apiClientProps.server, ServerFeature.BFR)
|
||||
? query.artistIds
|
||||
: query.artistIds?.[0];
|
||||
|
||||
const res = await ndApiClient(apiClientProps).getAlbumList({
|
||||
query: {
|
||||
@@ -285,12 +332,16 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: albumListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
artist_id: query.artistIds?.[0],
|
||||
artist_id: artistIds,
|
||||
compilation: query.compilation,
|
||||
genre_id: genres,
|
||||
has_rating: query.hasRating,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
name: query.searchTerm,
|
||||
...query._custom?.navidrome,
|
||||
recently_played: query.isRecentlyPlayed,
|
||||
starred: query.favorite,
|
||||
year: query.maxYear || query.minYear,
|
||||
...query._custom,
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
});
|
||||
@@ -319,9 +370,11 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: albumArtistListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
name: query.searchTerm,
|
||||
...query._custom?.navidrome,
|
||||
role: query.role || undefined,
|
||||
starred: query.favorite,
|
||||
...query._custom,
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
});
|
||||
@@ -352,16 +405,78 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getArtistRadio: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
// Use getSimilarSongs2 API for artist radio
|
||||
const res = await ssApiClient({
|
||||
...apiClientProps,
|
||||
silent: true,
|
||||
}).getSimilarSongs2({
|
||||
query: {
|
||||
count: query.count,
|
||||
id: query.artistId,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get artist radio songs');
|
||||
}
|
||||
|
||||
if (!res.body.similarSongs2?.song) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return res.body.similarSongs2.song.map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
);
|
||||
},
|
||||
getDownloadUrl: SubsonicController.getDownloadUrl,
|
||||
getFolder: SubsonicController.getFolder,
|
||||
getGenreList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
if (hasFeature(apiClientProps.server, ServerFeature.BFR)) {
|
||||
const res = await ndApiClient(apiClientProps).getTagList({
|
||||
query: {
|
||||
_end: query.startIndex + (query.limit || 0),
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: tagListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
tag_name: 'genre',
|
||||
tag_value: query.searchTerm,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get genre list');
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.data.map((genre) =>
|
||||
ndNormalize.genre(
|
||||
{
|
||||
albumCount: genre.albumCount,
|
||||
id: genre.id,
|
||||
name: genre.tagValue,
|
||||
songCount: genre.songCount,
|
||||
},
|
||||
apiClientProps.server,
|
||||
),
|
||||
),
|
||||
startIndex: query.startIndex || 0,
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
}
|
||||
|
||||
const res = await ndApiClient(apiClientProps).getGenreList({
|
||||
query: {
|
||||
_end: query.startIndex + (query.limit || 0),
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: genreListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
name: query.searchTerm,
|
||||
},
|
||||
});
|
||||
@@ -371,11 +486,13 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.data.map((genre) => ndNormalize.genre(genre)),
|
||||
items: res.body.data.map((genre) => ndNormalize.genre(genre, apiClientProps.server)),
|
||||
startIndex: query.startIndex || 0,
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
},
|
||||
getImageUrl: SubsonicController.getImageUrl,
|
||||
getInternetRadioStations: SubsonicController.getInternetRadioStations,
|
||||
getLyrics: SubsonicController.getLyrics,
|
||||
getMusicFolderList: SubsonicController.getMusicFolderList,
|
||||
getPlaylistDetail: async (args) => {
|
||||
@@ -395,16 +512,6 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
},
|
||||
getPlaylistList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
const customQuery = query._custom?.navidrome;
|
||||
|
||||
// Smart playlists only became available in 0.48.0. Do not filter for previous versions
|
||||
if (
|
||||
customQuery &&
|
||||
customQuery.smart !== undefined &&
|
||||
!hasFeature(apiClientProps.server, ServerFeature.PLAYLISTS_SMART)
|
||||
) {
|
||||
customQuery.smart = undefined;
|
||||
}
|
||||
|
||||
const res = await ndApiClient(apiClientProps).getPlaylistList({
|
||||
query: {
|
||||
@@ -413,7 +520,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
|
||||
_start: query.startIndex,
|
||||
q: query.searchTerm,
|
||||
...customQuery,
|
||||
smart: query.excludeSmartPlaylists ? false : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -457,6 +564,32 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
},
|
||||
getPlayQueue: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
if (hasFeatureWithVersion(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE, 2)) {
|
||||
const res = await ndApiClient(apiClientProps).getQueue();
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get play queue');
|
||||
}
|
||||
|
||||
const { changedBy, current, items, position, updatedAt } = res.body.data;
|
||||
|
||||
const entries = items.map((song) => ndNormalize.song(song, apiClientProps.server));
|
||||
|
||||
return {
|
||||
changed: updatedAt,
|
||||
changedBy,
|
||||
currentIndex: current !== undefined ? current : 0,
|
||||
entry: entries,
|
||||
positionMs: position,
|
||||
username: apiClientProps.server?.username ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
return SubsonicController.getPlayQueue(args);
|
||||
},
|
||||
getRandomSongList: SubsonicController.getRandomSongList,
|
||||
getRoles: async ({ apiClientProps }) =>
|
||||
hasFeature(apiClientProps.server, ServerFeature.BFR) ? NAVIDROME_ROLES : [],
|
||||
@@ -478,11 +611,18 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
const subsonicArgs = await SubsonicController.getServerInfo(args);
|
||||
|
||||
const features = {
|
||||
...navidromeFeatures,
|
||||
...subsonicArgs.features,
|
||||
...navidromeFeatures,
|
||||
publicPlaylist: [1],
|
||||
[ServerFeature.MUSIC_FOLDER_MULTISELECT]: [1],
|
||||
};
|
||||
|
||||
if (subsonicArgs.features.serverPlayQueue && navidromeFeatures.serverPlayQueue) {
|
||||
features.serverPlayQueue = navidromeFeatures.serverPlayQueue.concat(
|
||||
subsonicArgs.features.serverPlayQueue,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
features,
|
||||
id: apiClientProps.serverId,
|
||||
@@ -504,42 +644,15 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.body.similarSongs?.song) {
|
||||
const similar = res.body.similarSongs.song.reduce<Song[]>((acc, song) => {
|
||||
if (song.id !== query.songId) {
|
||||
acc.push(ssNormalize.song(song, apiClientProps.server));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (similar.length > 0) {
|
||||
return similar;
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = await ndApiClient(apiClientProps).getSongList({
|
||||
query: {
|
||||
_end: 50,
|
||||
_order: 'ASC',
|
||||
_sort: NDSongListSort.RANDOM,
|
||||
_start: 0,
|
||||
[getArtistSongKey(apiClientProps.server)]: query.albumArtistIds,
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get similar songs');
|
||||
}
|
||||
|
||||
return fallback.body.data.reduce<Song[]>((acc, song) => {
|
||||
if (song.id !== query.songId) {
|
||||
acc.push(ndNormalize.song(song, apiClientProps.server));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
return (
|
||||
(res.body.similarSongs?.song || [])
|
||||
.filter((song) => song.id !== query.songId)
|
||||
.map((song) => ssNormalize.song(song, apiClientProps.server)) || []
|
||||
);
|
||||
},
|
||||
getSongDetail: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
@@ -568,9 +681,11 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
album_id: query.albumIds,
|
||||
genre_id: query.genreIds,
|
||||
[getArtistSongKey(apiClientProps.server)]: query.artistIds ?? query.albumArtistIds,
|
||||
library_id: getLibraryId(query.musicFolderId),
|
||||
starred: query.favorite,
|
||||
title: query.searchTerm,
|
||||
...query._custom?.navidrome,
|
||||
year: query.maxYear || query.minYear,
|
||||
...query._custom,
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
});
|
||||
@@ -580,9 +695,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.data.map((song) =>
|
||||
ndNormalize.song(song, apiClientProps.server, query.imageSize),
|
||||
),
|
||||
items: res.body.data.map((song) => ndNormalize.song(song, apiClientProps.server)),
|
||||
startIndex: query?.startIndex || 0,
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
@@ -592,48 +705,68 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getStreamUrl: SubsonicController.getStreamUrl,
|
||||
getStructuredLyrics: SubsonicController.getStructuredLyrics,
|
||||
getTags: async (args) => {
|
||||
getTagList: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
if (!hasFeature(apiClientProps.server, ServerFeature.TAGS)) {
|
||||
return { boolTags: undefined, enumTags: undefined };
|
||||
return { boolTags: undefined, enumTags: undefined, excluded: { album: [], song: [] } };
|
||||
}
|
||||
|
||||
const res = await ndApiClient(apiClientProps).getTags();
|
||||
const res = await ndApiClient(apiClientProps).getTagList({
|
||||
query: {},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('failed to get tags');
|
||||
}
|
||||
|
||||
const tagsToValues = new Map<string, string[]>();
|
||||
const tagsToValues = new Map<string, { id: string; name: string }[]>();
|
||||
|
||||
for (const tag of res.body.data) {
|
||||
if (!EXCLUDED_TAGS.has(tag.tagName)) {
|
||||
if (tagsToValues.has(tag.tagName)) {
|
||||
tagsToValues.get(tag.tagName)!.push(tag.tagValue);
|
||||
tagsToValues.get(tag.tagName)!.push({
|
||||
id: ID_TAGS.has(tag.tagName) ? tag.id : tag.tagValue,
|
||||
name: tag.tagValue,
|
||||
});
|
||||
} else {
|
||||
tagsToValues.set(tag.tagName, [tag.tagValue]);
|
||||
tagsToValues.set(tag.tagName, [
|
||||
{
|
||||
id: ID_TAGS.has(tag.tagName) ? tag.id : tag.tagValue,
|
||||
name: tag.tagValue,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const enumTags = Array.from(tagsToValues)
|
||||
.map((data) => ({
|
||||
name: data[0],
|
||||
options: data[1]
|
||||
.sort((a, b) =>
|
||||
a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()),
|
||||
)
|
||||
.map((option) => ({ id: option.id, name: option.name })),
|
||||
}))
|
||||
.sort((a, b) => a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()));
|
||||
|
||||
const excludedAlbumTags = Array.from(EXCLUDED_ALBUM_TAGS.values());
|
||||
const excludedSongTags = Array.from(EXCLUDED_SONG_TAGS.values());
|
||||
|
||||
return {
|
||||
boolTags: undefined,
|
||||
enumTags: Array.from(tagsToValues)
|
||||
.map((data) => ({
|
||||
name: data[0],
|
||||
options: data[1].sort((a, b) =>
|
||||
a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()),
|
||||
),
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()),
|
||||
),
|
||||
enumTags,
|
||||
excluded: {
|
||||
album: excludedAlbumTags,
|
||||
song: excludedSongTags,
|
||||
},
|
||||
};
|
||||
},
|
||||
getTopSongs: SubsonicController.getTopSongs,
|
||||
getTranscodingUrl: SubsonicController.getTranscodingUrl,
|
||||
getUserInfo: SubsonicController.getUserInfo,
|
||||
getUserList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -643,7 +776,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: userListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
...query._custom?.navidrome,
|
||||
...query._custom,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -692,6 +825,120 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
replacePlaylist: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
// 1. Fetch existing songs from the playlist without any sorts
|
||||
const existingSongsRes = await ndApiClient(apiClientProps as any).getPlaylistSongList({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
_end: -1,
|
||||
_order: 'ASC',
|
||||
_start: 0,
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSongsRes.status !== 200) {
|
||||
throw new Error('Failed to fetch existing playlist songs');
|
||||
}
|
||||
|
||||
const existingSongs = existingSongsRes.body.data.map((item) =>
|
||||
ndNormalize.song(item, apiClientProps.server),
|
||||
);
|
||||
|
||||
// 2. Get playlist detail to get the name
|
||||
const playlistDetailRes = await ndApiClient(apiClientProps).getPlaylistDetail({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (playlistDetailRes.status !== 200) {
|
||||
throw new Error('Failed to get playlist detail');
|
||||
}
|
||||
|
||||
const playlist = ndNormalize.playlist(playlistDetailRes.body.data, apiClientProps.server);
|
||||
|
||||
// 3. Make a backup of the playlist ids and their order, along with the id of the playlist and name
|
||||
const backup = {
|
||||
id: query.id,
|
||||
name: playlist.name,
|
||||
songIds: existingSongs.map((song) => song.id),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Store backup in IndexedDB using idb-keyval
|
||||
const backupKey = `playlist-backup-${query.id}`;
|
||||
await set(backupKey, backup);
|
||||
|
||||
// 4. Remove all songs from the playlist
|
||||
if (existingSongs.length > 0) {
|
||||
const existingPlaylistItemIds = existingSongs
|
||||
.map((song) => song.playlistItemId)
|
||||
.filter((id): id is string => id !== undefined && id !== null);
|
||||
|
||||
if (existingPlaylistItemIds.length > 0) {
|
||||
const removeRes = await ndApiClient(apiClientProps).removeFromPlaylist({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
id: existingPlaylistItemIds,
|
||||
},
|
||||
});
|
||||
|
||||
if (removeRes.status !== 200) {
|
||||
throw new Error('Failed to remove songs from playlist');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Add the new song ids to the playlist
|
||||
if (body.songId.length > 0) {
|
||||
const addRes = await ndApiClient(apiClientProps).addToPlaylist({
|
||||
body: {
|
||||
ids: body.songId,
|
||||
},
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (addRes.status !== 200) {
|
||||
throw new Error('Failed to add songs to playlist');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
savePlayQueue: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
// Prefer using Navidrome's API only in the situation where the OpenSubsonic extension is not present
|
||||
// OpenSubsonic extension is preferable as the credentials never expire
|
||||
if (
|
||||
hasFeatureWithVersion(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE, 2) &&
|
||||
!hasFeatureWithVersion(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE, 1)
|
||||
) {
|
||||
const res = await ndApiClient(apiClientProps).saveQueue({
|
||||
body: {
|
||||
current: query.currentIndex !== undefined ? query.currentIndex : undefined,
|
||||
ids: query.songs,
|
||||
position: query.positionMs,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to save play queue');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return SubsonicController.savePlayQueue(args);
|
||||
},
|
||||
scrobble: SubsonicController.scrobble,
|
||||
search: SubsonicController.search,
|
||||
setRating: SubsonicController.setRating,
|
||||
@@ -716,6 +963,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
id: res.body.data.id,
|
||||
};
|
||||
},
|
||||
updateInternetRadioStation: SubsonicController.updateInternetRadioStation,
|
||||
updatePlaylist: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
@@ -723,9 +971,11 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
body: {
|
||||
comment: body.comment || '',
|
||||
name: body.name,
|
||||
ownerId: body.ownerId,
|
||||
public: body?.public || false,
|
||||
rules: body._custom?.navidrome?.rules ? body._custom.navidrome.rules : undefined,
|
||||
sync: body._custom?.navidrome?.sync || undefined,
|
||||
rules: body.queryBuilderRules,
|
||||
sync: body.sync,
|
||||
...body._custom,
|
||||
},
|
||||
params: {
|
||||
id: query.id,
|
||||
|
||||
@@ -4,6 +4,8 @@ import type {
|
||||
AlbumDetailQuery,
|
||||
AlbumListQuery,
|
||||
ArtistListQuery,
|
||||
ArtistRadioQuery,
|
||||
FolderQuery,
|
||||
GenreListQuery,
|
||||
LyricSearchQuery,
|
||||
LyricsQuery,
|
||||
@@ -65,9 +67,24 @@ export const queryKeys: Record<
|
||||
return [serverId, 'albumArtists', 'count'] as const;
|
||||
},
|
||||
detail: (serverId: string, query?: AlbumArtistDetailQuery) => {
|
||||
if (query) return [serverId, 'albumArtists', 'detail', query] as const;
|
||||
if (query) {
|
||||
return [serverId, 'albumArtists', 'detail', query] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'albumArtists', 'detail'] as const;
|
||||
},
|
||||
infiniteList: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
return [serverId, 'albumArtists', 'infiniteList', filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'albumArtists', 'infiniteList', filter] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'albumArtists', 'infiniteList'] as const;
|
||||
},
|
||||
list: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
@@ -108,8 +125,34 @@ export const queryKeys: Record<
|
||||
|
||||
return [serverId, 'albums', 'count'] as const;
|
||||
},
|
||||
detail: (serverId: string, query?: AlbumDetailQuery) =>
|
||||
[serverId, 'albums', 'detail', query] as const,
|
||||
detail: (serverId: string, query?: AlbumDetailQuery) => {
|
||||
if (query) {
|
||||
return [serverId, 'albums', 'detail', query] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'albums', 'detail'] as const;
|
||||
},
|
||||
infiniteList: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
|
||||
if (query && pagination && artistId) {
|
||||
return [serverId, 'albums', 'infiniteList', artistId, filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query && pagination) {
|
||||
return [serverId, 'albums', 'infiniteList', filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query && artistId) {
|
||||
return [serverId, 'albums', 'infiniteList', artistId, filter] as const;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'albums', 'infiniteList', filter] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'albums', 'infiniteList'] as const;
|
||||
},
|
||||
list: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
|
||||
@@ -144,6 +187,31 @@ export const queryKeys: Record<
|
||||
[serverId, 'albums', 'songs', query] as const,
|
||||
},
|
||||
artists: {
|
||||
count: (serverId: string, query?: ArtistListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
|
||||
if (query && pagination) {
|
||||
return [serverId, 'artists', 'count', filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'artists', 'count', filter] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'artists', 'count'] as const;
|
||||
},
|
||||
infiniteList: (serverId: string, query?: ArtistListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
return [serverId, 'artists', 'infiniteList', filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'artists', 'infiniteList', filter] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'artists', 'infiniteList'] as const;
|
||||
},
|
||||
list: (serverId: string, query?: ArtistListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
@@ -158,7 +226,29 @@ export const queryKeys: Record<
|
||||
},
|
||||
root: (serverId: string) => [serverId, 'artists'] as const,
|
||||
},
|
||||
folders: {
|
||||
folder: (serverId: string, query?: FolderQuery) => {
|
||||
if (query) {
|
||||
return [serverId, 'folders', 'folder', query] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'folders', 'folder'] as const;
|
||||
},
|
||||
},
|
||||
genres: {
|
||||
count: (serverId: string, query?: GenreListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
|
||||
if (query && pagination) {
|
||||
return [serverId, 'genres', 'count', filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'genres', 'count', filter] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'genres', 'count'] as const;
|
||||
},
|
||||
list: (serverId: string, query?: GenreListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
@@ -176,7 +266,29 @@ export const queryKeys: Record<
|
||||
musicFolders: {
|
||||
list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const,
|
||||
},
|
||||
player: {
|
||||
fetch: (meta?: any) => {
|
||||
if (meta) {
|
||||
return ['player', 'fetch', meta] as const;
|
||||
}
|
||||
|
||||
return ['player', 'fetch'] as const;
|
||||
},
|
||||
},
|
||||
playlists: {
|
||||
count: (serverId: string, query?: PlaylistListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
|
||||
if (query && pagination) {
|
||||
return [serverId, 'playlists', 'count', filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'playlists', 'count', filter] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'playlists', 'count'] as const;
|
||||
},
|
||||
detail: (serverId: string, id?: string, query?: PlaylistDetailQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
@@ -204,10 +316,17 @@ export const queryKeys: Record<
|
||||
},
|
||||
root: (serverId: string) => [serverId, 'playlists'] as const,
|
||||
songList: (serverId: string, id?: string) => {
|
||||
if (id) return [serverId, 'playlists', id, 'songList'] as const;
|
||||
if (id) {
|
||||
return [serverId, 'playlists', 'songList', id] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'playlists', 'songList'] as const;
|
||||
},
|
||||
},
|
||||
radio: {
|
||||
list: (serverId: string) => [serverId, 'radio', 'list'] as const,
|
||||
root: (serverId: string) => [serverId, 'radio'] as const,
|
||||
},
|
||||
roles: {
|
||||
list: (serverId: string) => [serverId, 'roles'] as const,
|
||||
},
|
||||
@@ -222,6 +341,10 @@ export const queryKeys: Record<
|
||||
root: (serverId: string) => [serverId] as const,
|
||||
},
|
||||
songs: {
|
||||
artistRadio: (serverId: string, query?: ArtistRadioQuery) => {
|
||||
if (query) return [serverId, 'songs', 'artistRadio', query] as const;
|
||||
return [serverId, 'songs', 'artistRadio'] as const;
|
||||
},
|
||||
count: (serverId: string, query?: SongListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
@@ -235,7 +358,10 @@ export const queryKeys: Record<
|
||||
return [serverId, 'songs', 'count'] as const;
|
||||
},
|
||||
detail: (serverId: string, query?: SongDetailQuery) => {
|
||||
if (query) return [serverId, 'songs', 'detail', query] as const;
|
||||
if (query) {
|
||||
return [serverId, 'songs', 'detail', query] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'songs', 'detail'] as const;
|
||||
},
|
||||
list: (serverId: string, query?: SongListQuery) => {
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { initClient, initContract } from '@ts-rest/core';
|
||||
import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios';
|
||||
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios';
|
||||
import omitBy from 'lodash/omitBy';
|
||||
import qs from 'qs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ServerListItemWithCredential } from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const contract = c.router({
|
||||
authenticate: {
|
||||
method: 'GET',
|
||||
path: 'ping.view',
|
||||
path: 'getUser.view',
|
||||
query: ssType._parameters.authenticate,
|
||||
responses: {
|
||||
200: ssType._response.authenticate,
|
||||
@@ -28,6 +30,14 @@ export const contract = c.router({
|
||||
200: ssType._response.createFavorite,
|
||||
},
|
||||
},
|
||||
createInternetRadioStation: {
|
||||
method: 'GET',
|
||||
path: 'createInternetRadioStation.view',
|
||||
query: ssType._parameters.createInternetRadioStation,
|
||||
responses: {
|
||||
200: ssType._response.createInternetRadioStation,
|
||||
},
|
||||
},
|
||||
createPlaylist: {
|
||||
method: 'GET',
|
||||
path: 'createPlaylist.view',
|
||||
@@ -36,6 +46,14 @@ export const contract = c.router({
|
||||
200: ssType._response.createPlaylist,
|
||||
},
|
||||
},
|
||||
deleteInternetRadioStation: {
|
||||
method: 'GET',
|
||||
path: 'deleteInternetRadioStation.view',
|
||||
query: ssType._parameters.deleteInternetRadioStation,
|
||||
responses: {
|
||||
200: ssType._response.deleteInternetRadioStation,
|
||||
},
|
||||
},
|
||||
deletePlaylist: {
|
||||
method: 'GET',
|
||||
path: 'deletePlaylist.view',
|
||||
@@ -100,6 +118,29 @@ export const contract = c.router({
|
||||
200: ssType._response.getGenres,
|
||||
},
|
||||
},
|
||||
getIndexes: {
|
||||
method: 'GET',
|
||||
path: 'getIndexes.view',
|
||||
query: ssType._parameters.getIndexes,
|
||||
responses: {
|
||||
200: ssType._response.getIndexes,
|
||||
},
|
||||
},
|
||||
getInternetRadioStations: {
|
||||
method: 'GET',
|
||||
path: 'getInternetRadioStations.view',
|
||||
responses: {
|
||||
200: ssType._response.getInternetRadioStations,
|
||||
},
|
||||
},
|
||||
getMusicDirectory: {
|
||||
method: 'GET',
|
||||
path: 'getMusicDirectory.view',
|
||||
query: ssType._parameters.getMusicDirectory,
|
||||
responses: {
|
||||
200: ssType._response.getMusicDirectory,
|
||||
},
|
||||
},
|
||||
getMusicFolderList: {
|
||||
method: 'GET',
|
||||
path: 'getMusicFolders.view',
|
||||
@@ -123,6 +164,20 @@ export const contract = c.router({
|
||||
200: ssType._response.getPlaylists,
|
||||
},
|
||||
},
|
||||
getPlayQueue: {
|
||||
method: 'GET',
|
||||
path: 'getPlayQueue.view',
|
||||
responses: {
|
||||
200: ssType._response.playQueue,
|
||||
},
|
||||
},
|
||||
getPlayQueueByIndex: {
|
||||
method: 'GET',
|
||||
path: 'getPlayQueueByIndex.view',
|
||||
responses: {
|
||||
200: ssType._response.playQueueByIndex,
|
||||
},
|
||||
},
|
||||
getRandomSongList: {
|
||||
method: 'GET',
|
||||
path: 'getRandomSongs.view',
|
||||
@@ -146,6 +201,14 @@ export const contract = c.router({
|
||||
200: ssType._response.similarSongs,
|
||||
},
|
||||
},
|
||||
getSimilarSongs2: {
|
||||
method: 'GET',
|
||||
path: 'getSimilarSongs2',
|
||||
query: ssType._parameters.similarSongs2,
|
||||
responses: {
|
||||
200: ssType._response.similarSongs2,
|
||||
},
|
||||
},
|
||||
getSong: {
|
||||
method: 'GET',
|
||||
path: 'getSong.view',
|
||||
@@ -186,6 +249,14 @@ export const contract = c.router({
|
||||
200: ssType._response.topSongsList,
|
||||
},
|
||||
},
|
||||
getUser: {
|
||||
method: 'GET',
|
||||
path: 'getUser.view',
|
||||
query: ssType._parameters.user,
|
||||
responses: {
|
||||
200: ssType._response.user,
|
||||
},
|
||||
},
|
||||
ping: {
|
||||
method: 'GET',
|
||||
path: 'ping.view',
|
||||
@@ -201,6 +272,22 @@ export const contract = c.router({
|
||||
200: ssType._response.removeFavorite,
|
||||
},
|
||||
},
|
||||
savePlayQueue: {
|
||||
method: 'GET',
|
||||
path: 'savePlayQueue.view',
|
||||
query: ssType._parameters.saveQueue,
|
||||
responses: {
|
||||
200: ssType._response.saveQueue,
|
||||
},
|
||||
},
|
||||
savePlayQueueByIndex: {
|
||||
method: 'GET',
|
||||
path: 'savePlayQueueByIndex.view',
|
||||
query: ssType._parameters.savePlayQueueByIndex,
|
||||
responses: {
|
||||
200: ssType._response.saveQueue,
|
||||
},
|
||||
},
|
||||
scrobble: {
|
||||
method: 'GET',
|
||||
path: 'scrobble.view',
|
||||
@@ -225,6 +312,14 @@ export const contract = c.router({
|
||||
200: ssType._response.setRating,
|
||||
},
|
||||
},
|
||||
updateInternetRadioStation: {
|
||||
method: 'GET',
|
||||
path: 'updateInternetRadioStation.view',
|
||||
query: ssType._parameters.updateInternetRadioStation,
|
||||
responses: {
|
||||
200: ssType._response.updateInternetRadioStation,
|
||||
},
|
||||
},
|
||||
updatePlaylist: {
|
||||
method: 'GET',
|
||||
path: 'updatePlaylist.view',
|
||||
@@ -296,7 +391,7 @@ export const ssApiClient = (args: {
|
||||
const { server, signal, silent, url } = args;
|
||||
|
||||
return initClient(contract, {
|
||||
api: async ({ body, headers, method, path }) => {
|
||||
api: async ({ headers, method, path }) => {
|
||||
let baseUrl: string | undefined;
|
||||
const authParams: Record<string, any> = {};
|
||||
|
||||
@@ -318,25 +413,36 @@ export const ssApiClient = (args: {
|
||||
baseUrl = url;
|
||||
}
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
headers,
|
||||
signal,
|
||||
// In cases where we have a fallback, don't notify the error
|
||||
transformResponse: silent ? silentlyTransformResponse : undefined,
|
||||
url: `${baseUrl}/${api}`,
|
||||
};
|
||||
|
||||
const data = {
|
||||
c: 'Feishin',
|
||||
f: 'json',
|
||||
v: '1.13.0',
|
||||
...authParams,
|
||||
...params,
|
||||
};
|
||||
|
||||
if (hasFeature(server, ServerFeature.OS_FORM_POST)) {
|
||||
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
request.method = 'POST';
|
||||
request.data = qs.stringify(data, { arrayFormat: 'repeat' });
|
||||
} else {
|
||||
request.method = method;
|
||||
request.params = data;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await axiosClient.request<
|
||||
z.infer<typeof ssType._response.baseResponse>
|
||||
>({
|
||||
data: body,
|
||||
headers,
|
||||
method: method as Method,
|
||||
params: {
|
||||
c: 'Feishin',
|
||||
f: 'json',
|
||||
v: '1.13.0',
|
||||
...authParams,
|
||||
...params,
|
||||
},
|
||||
signal,
|
||||
// In cases where we have a fallback, don't notify the error
|
||||
transformResponse: silent ? silentlyTransformResponse : undefined,
|
||||
url: `${baseUrl}/${api}`,
|
||||
});
|
||||
const result =
|
||||
await axiosClient.request<z.infer<typeof ssType._response.baseResponse>>(
|
||||
request,
|
||||
);
|
||||
|
||||
return {
|
||||
body: result.data['subsonic-response'],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ServerInferResponses } from '@ts-rest/core';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { set } from 'idb-keyval';
|
||||
import filter from 'lodash/filter';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import md5 from 'md5';
|
||||
@@ -14,19 +15,19 @@ import {
|
||||
ssType,
|
||||
SubsonicExtensions,
|
||||
} from '/@/shared/api/subsonic/subsonic-types';
|
||||
import { hasFeature, sortAlbumArtistList, sortAlbumList, sortSongList } from '/@/shared/api/utils';
|
||||
import {
|
||||
AlbumListSort,
|
||||
GenreListSort,
|
||||
InternalControllerEndpoint,
|
||||
LibraryItem,
|
||||
PlaylistListSort,
|
||||
ServerType,
|
||||
Song,
|
||||
sortAlbumArtistList,
|
||||
sortAlbumList,
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
sortSongList,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeatures } from '/@/shared/types/features-types';
|
||||
import { ServerFeature, ServerFeatures } from '/@/shared/types/features-types';
|
||||
|
||||
const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefined> = {
|
||||
[AlbumListSort.ALBUM_ARTIST]: AlbumListSortType.ALPHABETICAL_BY_ARTIST,
|
||||
@@ -48,9 +49,41 @@ const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefin
|
||||
};
|
||||
|
||||
const MAX_SUBSONIC_ITEMS = 500;
|
||||
// A trick to skip ahead 10x
|
||||
const SUBSONIC_FAST_BATCH_SIZE = MAX_SUBSONIC_ITEMS * 10;
|
||||
|
||||
function sortAndPaginate<T>(
|
||||
items: T[],
|
||||
options: {
|
||||
limit?: number;
|
||||
sortBy?: any;
|
||||
sortFn?: (items: T[], sortBy: any, sortOrder: SortOrder) => T[];
|
||||
sortOrder?: SortOrder;
|
||||
startIndex?: number;
|
||||
},
|
||||
): {
|
||||
items: T[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
} {
|
||||
let sortedItems = items;
|
||||
|
||||
if (options.sortFn && options.sortBy) {
|
||||
const sortOrder = options.sortOrder || SortOrder.ASC;
|
||||
sortedItems = options.sortFn(items, options.sortBy, sortOrder);
|
||||
}
|
||||
|
||||
const totalCount = sortedItems.length;
|
||||
const startIndex = options.startIndex || 0;
|
||||
const limit = options.limit || totalCount;
|
||||
const paginatedItems = sortedItems.slice(startIndex, startIndex + limit);
|
||||
|
||||
return {
|
||||
items: paginatedItems,
|
||||
startIndex: startIndex,
|
||||
totalRecordCount: totalCount,
|
||||
};
|
||||
}
|
||||
|
||||
export const SubsonicController: InternalControllerEndpoint = {
|
||||
addToPlaylist: async ({ apiClientProps, body, query }) => {
|
||||
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
||||
@@ -99,6 +132,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
query: {
|
||||
c: 'Feishin',
|
||||
f: 'json',
|
||||
username: body.username,
|
||||
v: '1.13.0',
|
||||
...credentialParams,
|
||||
},
|
||||
@@ -110,7 +144,8 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return {
|
||||
credential,
|
||||
userId: null,
|
||||
isAdmin: Boolean(resp.body.user.adminRole),
|
||||
userId: resp.body.user.username,
|
||||
username: body.username,
|
||||
};
|
||||
},
|
||||
@@ -120,7 +155,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
const res = await ssApiClient(apiClientProps).createFavorite({
|
||||
query: {
|
||||
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
|
||||
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
|
||||
artistId:
|
||||
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
|
||||
? query.id
|
||||
: undefined,
|
||||
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
||||
},
|
||||
});
|
||||
@@ -131,6 +169,23 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
createInternetRadioStation: async (args) => {
|
||||
const { apiClientProps, body } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).createInternetRadioStation({
|
||||
query: {
|
||||
homepageUrl: body.homepageUrl,
|
||||
name: body.name,
|
||||
streamUrl: body.streamUrl,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to create internet radio station');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
createPlaylist: async ({ apiClientProps, body }) => {
|
||||
const res = await ssApiClient(apiClientProps).createPlaylist({
|
||||
query: {
|
||||
@@ -153,7 +208,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
const res = await ssApiClient(apiClientProps).removeFavorite({
|
||||
query: {
|
||||
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
|
||||
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
|
||||
artistId:
|
||||
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
|
||||
? query.id
|
||||
: undefined,
|
||||
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
||||
},
|
||||
});
|
||||
@@ -164,6 +222,21 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
deleteInternetRadioStation: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).deleteInternetRadioStation({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to delete internet radio station');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
deletePlaylist: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -206,11 +279,11 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
...ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||
...ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
||||
similarArtists:
|
||||
artistInfo?.similarArtist?.map((artist) =>
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
) || null,
|
||||
};
|
||||
},
|
||||
@@ -219,7 +292,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getArtists({
|
||||
query: {
|
||||
musicFolderId: query.musicFolderId,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -230,7 +303,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
const artists = (res.body.artists?.index || []).flatMap((index) => index.artist);
|
||||
|
||||
let results = artists.map((artist) =>
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
);
|
||||
|
||||
if (query.searchTerm) {
|
||||
@@ -248,7 +321,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return {
|
||||
items: results,
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: results?.length || 0,
|
||||
totalRecordCount: artists.length,
|
||||
};
|
||||
},
|
||||
getAlbumArtistListCount: (args) =>
|
||||
@@ -281,6 +354,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: query.startIndex,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: 0,
|
||||
songOffset: 0,
|
||||
@@ -340,7 +414,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
if (query.favorite) {
|
||||
const res = await ssApiClient(apiClientProps).getStarred({
|
||||
query: {
|
||||
musicFolderId: query.musicFolderId,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -348,19 +422,21 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get album list');
|
||||
}
|
||||
|
||||
const results =
|
||||
const allResults =
|
||||
res.body.starred?.album?.map((album) =>
|
||||
ssNormalize.album(album, apiClientProps.server),
|
||||
) || [];
|
||||
|
||||
return {
|
||||
items: sortAlbumList(results, query.sortBy, query.sortOrder),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.starred?.album?.length || 0,
|
||||
};
|
||||
return sortAndPaginate(allResults, {
|
||||
limit: query.limit,
|
||||
sortBy: query.sortBy,
|
||||
sortFn: sortAlbumList,
|
||||
sortOrder: query.sortOrder,
|
||||
startIndex: query.startIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (query.genres?.length) {
|
||||
if (query.genreIds?.length) {
|
||||
type = AlbumListSortType.BY_GENRE;
|
||||
}
|
||||
|
||||
@@ -397,8 +473,8 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
const res = await ssApiClient(apiClientProps).getAlbumList2({
|
||||
query: {
|
||||
fromYear,
|
||||
genre: query.genres?.length ? query.genres[0] : undefined,
|
||||
musicFolderId: query.musicFolderId,
|
||||
genre: query.genreIds?.length ? query.genreIds[0] : undefined,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
offset: query.startIndex,
|
||||
size: query.limit,
|
||||
toYear,
|
||||
@@ -413,7 +489,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return {
|
||||
items:
|
||||
res.body.albumList2.album?.map((album) =>
|
||||
ssNormalize.album(album, apiClientProps.server, 300),
|
||||
ssNormalize.album(album, apiClientProps.server),
|
||||
) || [],
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: null,
|
||||
@@ -434,6 +510,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: startIndex,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: 0,
|
||||
songOffset: 0,
|
||||
@@ -485,7 +562,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
if (query.favorite) {
|
||||
const res = await ssApiClient(apiClientProps).getStarred({
|
||||
query: {
|
||||
musicFolderId: query.musicFolderId,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -502,7 +579,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
let startIndex = 0;
|
||||
let totalRecordCount = 0;
|
||||
|
||||
if (query.genres?.length) {
|
||||
if (query.genreIds?.length) {
|
||||
type = AlbumListSortType.BY_GENRE;
|
||||
}
|
||||
|
||||
@@ -530,8 +607,8 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
const res = await ssApiClient(apiClientProps).getAlbumList2({
|
||||
query: {
|
||||
fromYear,
|
||||
genre: query.genres?.length ? query.genres[0] : undefined,
|
||||
musicFolderId: query.musicFolderId,
|
||||
genre: query.genreIds?.length ? query.genreIds[0] : undefined,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
offset: startIndex,
|
||||
size: MAX_SUBSONIC_ITEMS,
|
||||
toYear,
|
||||
@@ -567,7 +644,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getArtists({
|
||||
query: {
|
||||
musicFolderId: query.musicFolderId,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -583,7 +660,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
let results = artists.map((artist) =>
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
);
|
||||
|
||||
if (query.searchTerm) {
|
||||
@@ -594,21 +671,41 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
results = searchResults;
|
||||
}
|
||||
|
||||
if (query.sortBy) {
|
||||
results = sortAlbumArtistList(results, query.sortBy, query.sortOrder);
|
||||
}
|
||||
|
||||
return {
|
||||
items: results,
|
||||
return sortAndPaginate(results, {
|
||||
limit: query.limit,
|
||||
sortBy: query.sortBy,
|
||||
sortFn: query.sortBy ? sortAlbumArtistList : undefined,
|
||||
sortOrder: query.sortOrder,
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: results?.length || 0,
|
||||
};
|
||||
});
|
||||
},
|
||||
getArtistListCount: async (args) =>
|
||||
SubsonicController.getArtistList({
|
||||
...args,
|
||||
query: { ...args.query, startIndex: 0 },
|
||||
}).then((res) => res!.totalRecordCount!),
|
||||
getArtistRadio: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getSimilarSongs2({
|
||||
query: {
|
||||
count: query.count,
|
||||
id: query.artistId,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get artist radio songs');
|
||||
}
|
||||
|
||||
if (!res.body.similarSongs2?.song) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return res.body.similarSongs2.song.map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
);
|
||||
},
|
||||
getDownloadUrl: (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -620,8 +717,108 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
'&c=Feishin'
|
||||
);
|
||||
},
|
||||
getFolder: async ({ apiClientProps, query }) => {
|
||||
const sortOrder = (query.sortOrder?.toLowerCase() ?? 'asc') as 'asc' | 'desc';
|
||||
|
||||
const isRootFolderId = /^\d+$/.test(query.id);
|
||||
|
||||
if (isRootFolderId) {
|
||||
const res = await ssApiClient(apiClientProps).getIndexes({
|
||||
query: {
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Failed to get folder list: ${JSON.stringify(res.body)}`);
|
||||
}
|
||||
|
||||
let items =
|
||||
res.body.indexes?.index?.flatMap((idx) =>
|
||||
idx.artist.map((artist) => ({
|
||||
artist: artist.name,
|
||||
id: artist.id.toString(),
|
||||
isDir: true,
|
||||
title: artist.name,
|
||||
})),
|
||||
) || [];
|
||||
|
||||
if (query.searchTerm) {
|
||||
items = filter(items, (item) => {
|
||||
return item.title.toLowerCase().includes(query.searchTerm!.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
let folders = items.map((item) => ssNormalize.folder(item, apiClientProps.server));
|
||||
|
||||
folders = orderBy(folders, [(v) => v.name.toLowerCase()], [sortOrder]);
|
||||
|
||||
return {
|
||||
_itemType: LibraryItem.FOLDER,
|
||||
_serverId: apiClientProps.server?.id || 'unknown',
|
||||
_serverType: ServerType.SUBSONIC,
|
||||
children: {
|
||||
folders,
|
||||
songs: [],
|
||||
},
|
||||
id: query.id,
|
||||
name: '~',
|
||||
parentId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const directoryRes = await ssApiClient(apiClientProps).getMusicDirectory({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (directoryRes.status !== 200) {
|
||||
throw new Error('Failed to get folder');
|
||||
}
|
||||
|
||||
const folder = ssNormalize.folder(directoryRes.body.directory, apiClientProps.server);
|
||||
|
||||
let filteredFolders = folder.children?.folders || [];
|
||||
let filteredSongs = folder.children?.songs || [];
|
||||
|
||||
if (query.searchTerm) {
|
||||
const searchTermLower = query.searchTerm.toLowerCase();
|
||||
filteredFolders = filter(filteredFolders, (f) =>
|
||||
f.name.toLowerCase().includes(searchTermLower),
|
||||
);
|
||||
filteredSongs = filter(filteredSongs, (s) => {
|
||||
const name = s.name?.toLowerCase() || '';
|
||||
const album = s.album?.toLowerCase() || '';
|
||||
const artist = s.artistName?.toLowerCase() || '';
|
||||
return (
|
||||
name.includes(searchTermLower) ||
|
||||
album.includes(searchTermLower) ||
|
||||
artist.includes(searchTermLower)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
filteredFolders = orderBy(filteredFolders, [(v) => v.name.toLowerCase()], [sortOrder]);
|
||||
|
||||
if (filteredSongs.length > 0) {
|
||||
filteredSongs = sortSongList(
|
||||
filteredSongs,
|
||||
query.sortBy || SongListSort.NAME,
|
||||
query.sortOrder || SortOrder.ASC,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...folder,
|
||||
children: {
|
||||
folders: filteredFolders,
|
||||
songs: filteredSongs,
|
||||
},
|
||||
};
|
||||
},
|
||||
getGenreList: async ({ apiClientProps, query }) => {
|
||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||
const sortOrder = (query.sortOrder?.toLowerCase() ?? 'asc') as 'asc' | 'desc';
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getGenres({});
|
||||
|
||||
@@ -647,13 +844,47 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
break;
|
||||
}
|
||||
|
||||
const genres = results.map(ssNormalize.genre);
|
||||
const genres = results.map((genre) => ssNormalize.genre(genre, apiClientProps.server));
|
||||
|
||||
return {
|
||||
items: genres,
|
||||
startIndex: 0,
|
||||
totalRecordCount: genres.length,
|
||||
};
|
||||
return sortAndPaginate(genres, {
|
||||
limit: query.limit,
|
||||
startIndex: query.startIndex,
|
||||
});
|
||||
},
|
||||
getImageUrl: ({ apiClientProps: { server }, query }) => {
|
||||
const { id, size } = query;
|
||||
const imageSize = size;
|
||||
|
||||
if (!server?.url || !server?.credential) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for default placeholder image ID
|
||||
if (id.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
`${server.url}/rest/getCoverArt.view` +
|
||||
`?id=${id}` +
|
||||
`&${server.credential}` +
|
||||
'&v=1.13.0' +
|
||||
'&c=Feishin' +
|
||||
(imageSize ? `&size=${imageSize}` : '')
|
||||
);
|
||||
},
|
||||
getInternetRadioStations: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getInternetRadioStations();
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get internet radio stations');
|
||||
}
|
||||
|
||||
const stations = res.body.internetRadioStations?.internetRadioStation || [];
|
||||
|
||||
return stations.map((station) => ssNormalize.internetRadioStation(station));
|
||||
},
|
||||
getMusicFolderList: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
@@ -673,6 +904,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
totalRecordCount: res.body.musicFolders.musicFolder.length,
|
||||
};
|
||||
},
|
||||
|
||||
getPlaylistDetail: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -730,11 +962,14 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
items: results.map((playlist) => ssNormalize.playlist(playlist, apiClientProps.server)),
|
||||
startIndex: 0,
|
||||
totalRecordCount: results.length,
|
||||
};
|
||||
const playlists = results.map((playlist) =>
|
||||
ssNormalize.playlist(playlist, apiClientProps.server),
|
||||
);
|
||||
|
||||
return sortAndPaginate(playlists, {
|
||||
limit: query.limit,
|
||||
startIndex: query.startIndex,
|
||||
});
|
||||
},
|
||||
getPlaylistListCount: async ({ apiClientProps, query }) => {
|
||||
const res = await ssApiClient(apiClientProps).getPlaylists({});
|
||||
@@ -776,6 +1011,44 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
totalRecordCount: items.length,
|
||||
};
|
||||
},
|
||||
getPlayQueue: async ({ apiClientProps }) => {
|
||||
if (hasFeature(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE)) {
|
||||
const res = await ssApiClient(apiClientProps).getPlayQueueByIndex();
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get random songs');
|
||||
}
|
||||
|
||||
const { changed, changedBy, currentIndex, entry, position, username } =
|
||||
res.body.playQueueByIndex;
|
||||
|
||||
return {
|
||||
changed,
|
||||
changedBy,
|
||||
currentIndex: currentIndex ?? 0,
|
||||
entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
|
||||
positionMs: position ?? 0,
|
||||
username,
|
||||
};
|
||||
} else {
|
||||
const res = await ssApiClient(apiClientProps).getPlayQueue();
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get random songs');
|
||||
}
|
||||
|
||||
const { changed, changedBy, current, entry, position, username } = res.body.playQueue;
|
||||
|
||||
return {
|
||||
changed,
|
||||
changedBy,
|
||||
currentIndex: current ? entry.findIndex((item) => item.id === current) : 0,
|
||||
entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
|
||||
positionMs: position ?? 0,
|
||||
username,
|
||||
};
|
||||
}
|
||||
},
|
||||
getRandomSongList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -783,7 +1056,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
query: {
|
||||
fromYear: query.minYear,
|
||||
genre: query.genre,
|
||||
musicFolderId: query.musicFolderId,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
size: query.limit,
|
||||
toYear: query.maxYear,
|
||||
},
|
||||
@@ -794,11 +1067,14 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
const results = res.body.randomSongs?.song || [];
|
||||
const normalizedResults = results.map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
);
|
||||
|
||||
return {
|
||||
items: results.map((song) => ssNormalize.song(song, apiClientProps.server)),
|
||||
items: normalizedResults,
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.randomSongs?.song?.length || 0,
|
||||
totalRecordCount: normalizedResults.length,
|
||||
};
|
||||
},
|
||||
getRoles: async (args) => {
|
||||
@@ -859,6 +1135,14 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
features.lyricsMultipleStructured = [1];
|
||||
}
|
||||
|
||||
if (subsonicFeatures[SubsonicExtensions.FORM_POST]) {
|
||||
features.osFormPost = [1];
|
||||
}
|
||||
|
||||
if (subsonicFeatures[SubsonicExtensions.INDEX_BASED_QUEUE]) {
|
||||
features.serverPlayQueue = [1];
|
||||
}
|
||||
|
||||
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion };
|
||||
},
|
||||
getSimilarSongs: async (args) => {
|
||||
@@ -913,6 +1197,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: query.limit,
|
||||
songOffset: query.startIndex,
|
||||
@@ -938,7 +1223,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
query: {
|
||||
count: query.limit,
|
||||
genre: query.genreIds[0],
|
||||
musicFolderId: query.musicFolderId,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
offset: query.startIndex,
|
||||
},
|
||||
});
|
||||
@@ -959,7 +1244,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
if (query.favorite) {
|
||||
const res = await ssApiClient(apiClientProps).getStarred({
|
||||
query: {
|
||||
musicFolderId: query.musicFolderId,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -967,16 +1252,18 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get song list');
|
||||
}
|
||||
|
||||
const results =
|
||||
const allResults =
|
||||
(res.body.starred?.song || []).map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
) || [];
|
||||
|
||||
return {
|
||||
items: sortSongList(results, query.sortBy, query.sortOrder),
|
||||
startIndex: 0,
|
||||
totalRecordCount: (res.body.starred?.song || []).length || 0,
|
||||
};
|
||||
return sortAndPaginate(allResults, {
|
||||
limit: query.limit,
|
||||
sortBy: query.sortBy,
|
||||
sortFn: sortSongList,
|
||||
sortOrder: query.sortOrder,
|
||||
startIndex: query.startIndex,
|
||||
});
|
||||
}
|
||||
|
||||
const artistIds = query.albumArtistIds || query.artistIds;
|
||||
@@ -1043,7 +1330,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: results.map((song) => ssNormalize.song(song, apiClientProps.server)),
|
||||
items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
|
||||
startIndex: 0,
|
||||
totalRecordCount: results.length,
|
||||
};
|
||||
@@ -1055,6 +1342,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: query.limit,
|
||||
songOffset: query.startIndex,
|
||||
@@ -1095,6 +1383,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: MAX_SUBSONIC_ITEMS,
|
||||
songOffset: startIndex,
|
||||
@@ -1127,7 +1416,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
query: {
|
||||
count: 1,
|
||||
genre: query.genreIds[0],
|
||||
musicFolderId: query.musicFolderId,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
offset: sectionIndex,
|
||||
},
|
||||
});
|
||||
@@ -1152,7 +1441,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
query: {
|
||||
count: MAX_SUBSONIC_ITEMS,
|
||||
genre: query.genreIds[0],
|
||||
musicFolderId: query.musicFolderId,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
offset: startIndex,
|
||||
},
|
||||
});
|
||||
@@ -1175,7 +1464,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
if (query.favorite) {
|
||||
const res = await ssApiClient(apiClientProps).getStarred({
|
||||
query: {
|
||||
musicFolderId: query.musicFolderId,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1198,6 +1487,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: 1,
|
||||
songOffset: sectionIndex,
|
||||
@@ -1226,6 +1516,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: 0,
|
||||
artistCount: 0,
|
||||
artistOffset: 0,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.searchTerm || '',
|
||||
songCount: MAX_SUBSONIC_ITEMS,
|
||||
songOffset: startIndex,
|
||||
@@ -1246,6 +1537,21 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return totalRecordCount;
|
||||
},
|
||||
getStreamUrl: ({ apiClientProps: { server }, query }) => {
|
||||
const { bitrate, format, id, transcode } = query;
|
||||
let url = `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=Feishin&${server?.credential}`;
|
||||
|
||||
if (transcode) {
|
||||
if (format) {
|
||||
url += `&format=${format}`;
|
||||
}
|
||||
if (bitrate !== undefined) {
|
||||
url += `&maxBitRate=${bitrate}`;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
getStructuredLyrics: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -1311,17 +1617,24 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||
};
|
||||
},
|
||||
getTranscodingUrl: (args) => {
|
||||
const { base, bitrate, format } = args.query;
|
||||
let url = base;
|
||||
if (format) {
|
||||
url += `&format=${format}`;
|
||||
}
|
||||
if (bitrate !== undefined) {
|
||||
url += `&maxBitRate=${bitrate}`;
|
||||
getUserInfo: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getUser({
|
||||
query: {
|
||||
username: query.username,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get user info');
|
||||
}
|
||||
|
||||
return url;
|
||||
return {
|
||||
id: res.body.user.username,
|
||||
isAdmin: Boolean(res.body.user.adminRole),
|
||||
name: res.body.user.username,
|
||||
};
|
||||
},
|
||||
removeFromPlaylist: async ({ apiClientProps, query }) => {
|
||||
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
||||
@@ -1337,6 +1650,117 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
replacePlaylist: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
// 1. Fetch existing songs from the playlist
|
||||
const existingSongsRes = await ssApiClient(apiClientProps).getPlaylist({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSongsRes.status !== 200) {
|
||||
throw new Error('Failed to fetch existing playlist songs');
|
||||
}
|
||||
|
||||
const existingSongs =
|
||||
existingSongsRes.body.playlist.entry?.map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
) || [];
|
||||
|
||||
// 2. Get playlist detail to get the name
|
||||
const playlistDetailRes = await ssApiClient(apiClientProps).getPlaylist({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (playlistDetailRes.status !== 200) {
|
||||
throw new Error('Failed to get playlist detail');
|
||||
}
|
||||
|
||||
const playlist = ssNormalize.playlist(
|
||||
playlistDetailRes.body.playlist,
|
||||
apiClientProps.server,
|
||||
);
|
||||
|
||||
// 3. Make a backup of the playlist ids and their order, along with the id of the playlist and name
|
||||
const backup = {
|
||||
id: query.id,
|
||||
name: playlist.name,
|
||||
songIds: existingSongs.map((song) => song.id),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Store backup in IndexedDB using idb-keyval
|
||||
const backupKey = `playlist-backup-${query.id}`;
|
||||
await set(backupKey, backup);
|
||||
|
||||
// 4. Remove all songs from the playlist (Subsonic uses indices, not IDs)
|
||||
if (existingSongs.length > 0) {
|
||||
// Get indices of all songs (0-based)
|
||||
// Remove in reverse order to avoid index shifting issues
|
||||
const songIndices = existingSongs.map((_, index) => index).reverse();
|
||||
|
||||
const removeRes = await ssApiClient(apiClientProps).updatePlaylist({
|
||||
query: {
|
||||
playlistId: query.id,
|
||||
songIndexToRemove: songIndices.map((index) => index.toString()),
|
||||
},
|
||||
});
|
||||
|
||||
if (removeRes.status !== 200) {
|
||||
throw new Error('Failed to remove songs from playlist');
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Add the new song ids to the playlist
|
||||
if (body.songId.length > 0) {
|
||||
const addRes = await ssApiClient(apiClientProps).updatePlaylist({
|
||||
query: {
|
||||
playlistId: query.id,
|
||||
songIdToAdd: body.songId,
|
||||
},
|
||||
});
|
||||
|
||||
if (addRes.status !== 200) {
|
||||
throw new Error('Failed to add songs to playlist');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
savePlayQueue: async ({ apiClientProps, query }) => {
|
||||
if (hasFeature(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE)) {
|
||||
const res = await ssApiClient(apiClientProps).savePlayQueueByIndex({
|
||||
query: {
|
||||
currentIndex: query.currentIndex !== undefined ? query.currentIndex : undefined,
|
||||
id: query.songs,
|
||||
position: query.positionMs,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to save play queue');
|
||||
}
|
||||
} else {
|
||||
const res = await ssApiClient(apiClientProps).savePlayQueue({
|
||||
query: {
|
||||
current:
|
||||
query.currentIndex !== undefined && query.currentIndex < query.songs.length
|
||||
? query.songs[query.currentIndex]
|
||||
: undefined,
|
||||
id: query.songs,
|
||||
position: query.positionMs,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to save play queue');
|
||||
}
|
||||
}
|
||||
},
|
||||
scrobble: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -1353,7 +1777,6 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
search: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -1363,6 +1786,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
albumOffset: query.albumStartIndex,
|
||||
artistCount: query.albumArtistLimit,
|
||||
artistOffset: query.albumArtistStartIndex,
|
||||
musicFolderId: getLibraryId(query.musicFolderId),
|
||||
query: query.query,
|
||||
songCount: query.songLimit,
|
||||
songOffset: query.songStartIndex,
|
||||
@@ -1388,7 +1812,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
setRating: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const itemIds = query.item.map((item) => item.id);
|
||||
const itemIds = query.id;
|
||||
|
||||
for (const id of itemIds) {
|
||||
await ssApiClient(apiClientProps).setRating({
|
||||
@@ -1401,6 +1825,24 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
updateInternetRadioStation: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).updateInternetRadioStation({
|
||||
query: {
|
||||
homepageUrl: body.homepageUrl,
|
||||
id: query.id,
|
||||
name: body.name,
|
||||
streamUrl: body.streamUrl,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to update internet radio station');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
updatePlaylist: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
@@ -1420,3 +1862,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
function getLibraryId(musicFolderId?: string | string[]) {
|
||||
return Array.isArray(musicFolderId) ? musicFolderId[0] : musicFolderId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ServerListItemWithCredential } from '/@/shared/types/domain-types';
|
||||
|
||||
export const mergeMusicFolderId = <T extends { musicFolderId?: string | string[] }>(
|
||||
query: T,
|
||||
server: null | ServerListItemWithCredential,
|
||||
): T => {
|
||||
if (
|
||||
!server ||
|
||||
!server.musicFolderId ||
|
||||
server.musicFolderId.length === 0 ||
|
||||
query.musicFolderId
|
||||
) {
|
||||
return query;
|
||||
}
|
||||
|
||||
// Only merge if server matches and musicFolderId is not already in query
|
||||
const musicFolderId =
|
||||
server.musicFolderId.length === 1 ? server.musicFolderId[0] : server.musicFolderId;
|
||||
|
||||
return {
|
||||
...query,
|
||||
musicFolderId,
|
||||
};
|
||||
};
|
||||
+21
-136
@@ -1,66 +1,38 @@
|
||||
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
|
||||
import { ModuleRegistry } from '@ag-grid-community/core';
|
||||
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
|
||||
/* eslint-disable perfectionist/sort-imports */
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
import '/styles/overlayscrollbars.css';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import isElectron from 'is-electron';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
|
||||
import '/@/shared/styles/global.css';
|
||||
|
||||
import '@ag-grid-community/styles/ag-grid.css';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
|
||||
import '/styles/overlayscrollbars.css';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
|
||||
import { PlayQueueHandlerContext } from '/@/renderer/features/player/context/play-queue-handler-context';
|
||||
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
||||
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
||||
import { IsUpdatedDialog } from '/@/renderer/is-updated-dialog';
|
||||
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
||||
import { ReleaseNotesModal } from './release-notes-modal';
|
||||
import { AppRouter } from '/@/renderer/router/app-router';
|
||||
import {
|
||||
PlayerState,
|
||||
useCssSettings,
|
||||
useHotkeySettings,
|
||||
usePlaybackSettings,
|
||||
usePlayerStore,
|
||||
useQueueControls,
|
||||
useRemoteSettings,
|
||||
useSettingsStore,
|
||||
} from '/@/renderer/store';
|
||||
import { useCssSettings, useHotkeySettings, useSettingsStore } from '/@/renderer/store';
|
||||
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
||||
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
||||
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { PlaybackType, PlayerStatus, WebAudio } from '/@/shared/types/types';
|
||||
import { WebAudio } from '/@/shared/types/types';
|
||||
import '/@/shared/styles/global.css';
|
||||
import { PlayerProvider } from '/@/renderer/features/player/context/player-context';
|
||||
import { AudioPlayers } from '/@/renderer/features/player/components/audio-players';
|
||||
|
||||
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
const remote = isElectron() ? window.api.remote : null;
|
||||
const utils = isElectron() ? window.api.utils : null;
|
||||
|
||||
export const App = () => {
|
||||
const { mode, theme } = useAppTheme();
|
||||
const language = useSettingsStore((store) => store.general.language);
|
||||
|
||||
const { content, enabled } = useCssSettings();
|
||||
const { type: playbackType } = usePlaybackSettings();
|
||||
const { bindings } = useHotkeySettings();
|
||||
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
||||
const { clearQueue, restoreQueue } = useQueueControls();
|
||||
const remoteSettings = useRemoteSettings();
|
||||
const cssRef = useRef<HTMLStyleElement | null>(null);
|
||||
useDiscordRpc();
|
||||
useServerVersion();
|
||||
|
||||
useSyncSettingsToMain();
|
||||
|
||||
const [webAudio, setWebAudio] = useState<WebAudio>();
|
||||
|
||||
@@ -84,104 +56,16 @@ export const App = () => {
|
||||
return () => {};
|
||||
}, [content, enabled]);
|
||||
|
||||
const providerValue = useMemo(() => {
|
||||
return { handlePlayQueueAdd };
|
||||
}, [handlePlayQueueAdd]);
|
||||
|
||||
const webAudioProvider = useMemo(() => {
|
||||
return { setWebAudio, webAudio };
|
||||
}, [webAudio]);
|
||||
|
||||
// Start the mpv instance on startup
|
||||
useEffect(() => {
|
||||
const initializeMpv = async () => {
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
|
||||
|
||||
mpvPlayer?.stop();
|
||||
|
||||
if (!isRunning) {
|
||||
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
||||
const properties: Record<string, any> = {
|
||||
speed: usePlayerStore.getState().speed,
|
||||
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
||||
};
|
||||
|
||||
await mpvPlayer?.initialize({
|
||||
extraParameters,
|
||||
properties,
|
||||
});
|
||||
|
||||
mpvPlayer?.volume(properties.volume);
|
||||
}
|
||||
}
|
||||
|
||||
utils?.restoreQueue();
|
||||
};
|
||||
|
||||
if (isElectron()) {
|
||||
initializeMpv();
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearQueue();
|
||||
mpvPlayer?.stop();
|
||||
mpvPlayer?.cleanup();
|
||||
};
|
||||
}, [clearQueue, playbackType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
ipc?.send('set-global-shortcuts', bindings);
|
||||
}
|
||||
}, [bindings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (utils) {
|
||||
utils.onSaveQueue(() => {
|
||||
const { current, queue } = usePlayerStore.getState();
|
||||
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
|
||||
current: {
|
||||
...current,
|
||||
status: PlayerStatus.PAUSED,
|
||||
},
|
||||
queue,
|
||||
};
|
||||
utils.saveQueue(stateToSave);
|
||||
});
|
||||
|
||||
utils.onRestoreQueue((_event: any, data) => {
|
||||
const playerData = restoreQueue(data);
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
setQueue(playerData, true);
|
||||
}
|
||||
updateSong(playerData.current.song);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('renderer-restore-queue');
|
||||
ipc?.removeAllListeners('renderer-save-queue');
|
||||
};
|
||||
}, [playbackType, restoreQueue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (remote) {
|
||||
remote
|
||||
?.updateSetting(
|
||||
remoteSettings.enabled,
|
||||
remoteSettings.port,
|
||||
remoteSettings.username,
|
||||
remoteSettings.password,
|
||||
)
|
||||
.catch((error) => {
|
||||
toast.warn({ message: error, title: 'Failed to enable remote' });
|
||||
});
|
||||
}
|
||||
// We only want to fire this once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (language) {
|
||||
i18n.changeLanguage(language);
|
||||
@@ -200,12 +84,13 @@ export const App = () => {
|
||||
}}
|
||||
zIndex={50000}
|
||||
/>
|
||||
<PlayQueueHandlerContext.Provider value={providerValue}>
|
||||
<WebAudioContext.Provider value={webAudioProvider}>
|
||||
<WebAudioContext.Provider value={webAudioProvider}>
|
||||
<PlayerProvider>
|
||||
<AudioPlayers />
|
||||
<AppRouter />
|
||||
</WebAudioContext.Provider>
|
||||
</PlayQueueHandlerContext.Provider>
|
||||
<IsUpdatedDialog />
|
||||
</PlayerProvider>
|
||||
</WebAudioContext.Provider>
|
||||
<ReleaseNotesModal />
|
||||
</MantineProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,484 +0,0 @@
|
||||
import type { Song } from '/@/shared/types/domain-types';
|
||||
import type { CrossfadeStyle } from '/@/shared/types/types';
|
||||
import type { ReactPlayerProps } from 'react-player';
|
||||
|
||||
import isElectron from 'is-electron';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import ReactPlayer from 'react-player/lazy';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import {
|
||||
crossfadeHandler,
|
||||
gaplessHandler,
|
||||
} from '/@/renderer/components/audio-player/utils/list-handlers';
|
||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||
import { TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { PlaybackStyle, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export type AudioPlayerProgress = {
|
||||
loaded: number;
|
||||
loadedSeconds: number;
|
||||
played: number;
|
||||
playedSeconds: number;
|
||||
};
|
||||
|
||||
interface AudioPlayerProps extends ReactPlayerProps {
|
||||
autoNext: () => void;
|
||||
crossfadeDuration: number;
|
||||
crossfadeStyle: CrossfadeStyle;
|
||||
currentPlayer: 1 | 2;
|
||||
muted: boolean;
|
||||
playbackStyle: PlaybackStyle;
|
||||
player1?: Song;
|
||||
player2?: Song;
|
||||
status: PlayerStatus;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
const getDuration = (ref: any) => {
|
||||
return ref.current?.player?.player?.player?.duration;
|
||||
};
|
||||
|
||||
// Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393
|
||||
// This is used so that the player will always have an <audio> element. This means that
|
||||
// player1Source and player2Source are connected BEFORE the user presses play for
|
||||
// the first time. This workaround is important for Safari, which seems to require the
|
||||
// source to be connected PRIOR to resuming audio context
|
||||
const EMPTY_SOURCE =
|
||||
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
|
||||
|
||||
const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song): null | string => {
|
||||
const prior = useRef(['', '']);
|
||||
|
||||
return useMemo(() => {
|
||||
if (song?.serverId) {
|
||||
// If we are the current track, we do not want a transcoding
|
||||
// reconfiguration to force a restart.
|
||||
if (current && prior.current[0] === song.uniqueId) {
|
||||
return prior.current[1];
|
||||
}
|
||||
|
||||
if (!transcode.enabled) {
|
||||
// transcoding disabled; save the result
|
||||
prior.current = [song.uniqueId, song.streamUrl];
|
||||
return song.streamUrl;
|
||||
}
|
||||
|
||||
const result = api.controller.getTranscodingUrl({
|
||||
apiClientProps: {
|
||||
serverId: song.serverId,
|
||||
},
|
||||
query: {
|
||||
base: song.streamUrl,
|
||||
...transcode,
|
||||
},
|
||||
})!;
|
||||
|
||||
// transcoding enabled; save the updated result
|
||||
prior.current = [song.uniqueId, result];
|
||||
return result;
|
||||
}
|
||||
|
||||
// no track; clear result
|
||||
prior.current = ['', ''];
|
||||
return null;
|
||||
}, [current, song?.uniqueId, song?.serverId, song?.streamUrl, transcode]);
|
||||
};
|
||||
|
||||
export interface AudioPlayerRef {
|
||||
player1: null | ReactPlayer;
|
||||
player2: null | ReactPlayer;
|
||||
}
|
||||
|
||||
export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props, ref) => {
|
||||
const {
|
||||
autoNext,
|
||||
crossfadeDuration,
|
||||
crossfadeStyle,
|
||||
currentPlayer,
|
||||
muted,
|
||||
playbackStyle,
|
||||
player1,
|
||||
player2,
|
||||
status,
|
||||
volume,
|
||||
} = props;
|
||||
|
||||
const player1Ref = useRef<ReactPlayer>(null);
|
||||
const player2Ref = useRef<ReactPlayer>(null);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
|
||||
const playback = useSettingsStore((state) => state.playback.mpvProperties);
|
||||
const shouldUseWebAudio = useSettingsStore((state) => state.playback.webAudio);
|
||||
const preservesPitch = useSettingsStore((state) => state.playback.preservePitch);
|
||||
const { resetSampleRate } = useSettingsStoreActions();
|
||||
const playbackSpeed = useSpeed();
|
||||
const { transcode } = usePlaybackSettings();
|
||||
|
||||
const stream1 = useSongUrl(transcode, currentPlayer === 1, player1);
|
||||
const stream2 = useSongUrl(transcode, currentPlayer === 2, player2);
|
||||
|
||||
const { setWebAudio, webAudio } = useWebAudio();
|
||||
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(null);
|
||||
const [player2Source, setPlayer2Source] = useState<MediaElementAudioSourceNode | null>(null);
|
||||
|
||||
const calculateReplayGain = useCallback(
|
||||
(song: Song): number => {
|
||||
if (playback.replayGainMode === 'no') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
let gain: number | undefined;
|
||||
let peak: number | undefined;
|
||||
|
||||
if (playback.replayGainMode === 'track') {
|
||||
gain = song.gain?.track ?? song.gain?.album;
|
||||
peak = song.peak?.track ?? song.peak?.album;
|
||||
} else {
|
||||
gain = song.gain?.album ?? song.gain?.track;
|
||||
peak = song.peak?.album ?? song.peak?.track;
|
||||
}
|
||||
|
||||
if (gain === undefined) {
|
||||
gain = playback.replayGainFallbackDB;
|
||||
|
||||
if (!gain) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (peak === undefined) {
|
||||
peak = 1;
|
||||
}
|
||||
|
||||
const preAmp = playback.replayGainPreampDB ?? 0;
|
||||
|
||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification§ion=19
|
||||
// Normalized to max gain
|
||||
const expectedGain = 10 ** ((gain + preAmp) / 20);
|
||||
|
||||
if (playback.replayGainClip) {
|
||||
return Math.min(expectedGain, 1 / peak);
|
||||
}
|
||||
return expectedGain;
|
||||
},
|
||||
[
|
||||
playback.replayGainClip,
|
||||
playback.replayGainFallbackDB,
|
||||
playback.replayGainMode,
|
||||
playback.replayGainPreampDB,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldUseWebAudio && 'AudioContext' in window) {
|
||||
let context: AudioContext;
|
||||
|
||||
try {
|
||||
context = new AudioContext({
|
||||
latencyHint: 'playback',
|
||||
sampleRate: playback.audioSampleRateHz || undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
// In practice, this should never be hit because the UI should validate
|
||||
// the range. However, the actual supported range is not guaranteed
|
||||
toast.error({ message: (error as Error).message });
|
||||
context = new AudioContext({ latencyHint: 'playback' });
|
||||
resetSampleRate();
|
||||
}
|
||||
|
||||
const gain = context.createGain();
|
||||
gain.connect(context.destination);
|
||||
|
||||
setWebAudio!({ context, gain });
|
||||
|
||||
return () => {
|
||||
return context.close();
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
// Intentionally ignore the sample rate dependency, as it makes things really messy
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
get player1() {
|
||||
return player1Ref?.current;
|
||||
},
|
||||
get player2() {
|
||||
return player2Ref?.current;
|
||||
},
|
||||
}));
|
||||
|
||||
const handleOnEnded = () => {
|
||||
autoNext();
|
||||
setIsTransitioning(false);
|
||||
};
|
||||
|
||||
const handleOnError = (playerRef: React.RefObject<ReactPlayer>) => {
|
||||
return ({ target }: ErrorEvent) => {
|
||||
const { current: player } = playerRef;
|
||||
if (!player || !(target instanceof Audio)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = target;
|
||||
|
||||
console.log('Playback error occurred:', error);
|
||||
|
||||
if (
|
||||
error?.code !== MediaError.MEDIA_ERR_DECODE &&
|
||||
error?.code !== MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleOnEnded();
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
if (currentPlayer === 1) {
|
||||
// calling play() is not necessarily a safe option (https://developer.chrome.com/blog/play-request-was-interrupted)
|
||||
// In practice, this failure is only likely to happen when using the 0-second wav:
|
||||
// play() + play() in rapid succession will cause problems as the frist one ends the track.
|
||||
const internalPlayer = player1Ref.current?.getInternalPlayer();
|
||||
if (internalPlayer) {
|
||||
internalPlayer.preservesPitch = preservesPitch;
|
||||
internalPlayer.play().catch(() => {});
|
||||
}
|
||||
} else {
|
||||
const internalPlayer = player2Ref.current?.getInternalPlayer();
|
||||
if (internalPlayer) {
|
||||
internalPlayer.preservesPitch = preservesPitch;
|
||||
internalPlayer.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
player1Ref.current?.getInternalPlayer()?.pause();
|
||||
player2Ref.current?.getInternalPlayer()?.pause();
|
||||
}
|
||||
}, [currentPlayer, status, preservesPitch]);
|
||||
|
||||
const handleCrossfade1 = useCallback(
|
||||
(e: AudioPlayerProgress) => {
|
||||
return crossfadeHandler({
|
||||
currentPlayer,
|
||||
currentPlayerRef: player1Ref,
|
||||
currentTime: e.playedSeconds,
|
||||
duration: getDuration(player1Ref),
|
||||
fadeDuration: crossfadeDuration,
|
||||
fadeType: crossfadeStyle,
|
||||
isTransitioning,
|
||||
nextPlayerRef: player2Ref,
|
||||
player: 1,
|
||||
setIsTransitioning,
|
||||
volume,
|
||||
});
|
||||
},
|
||||
[crossfadeDuration, crossfadeStyle, currentPlayer, isTransitioning, volume],
|
||||
);
|
||||
|
||||
const handleCrossfade2 = useCallback(
|
||||
(e: AudioPlayerProgress) => {
|
||||
return crossfadeHandler({
|
||||
currentPlayer,
|
||||
currentPlayerRef: player2Ref,
|
||||
currentTime: e.playedSeconds,
|
||||
duration: getDuration(player2Ref),
|
||||
fadeDuration: crossfadeDuration,
|
||||
fadeType: crossfadeStyle,
|
||||
isTransitioning,
|
||||
nextPlayerRef: player1Ref,
|
||||
player: 2,
|
||||
setIsTransitioning,
|
||||
volume,
|
||||
});
|
||||
},
|
||||
[crossfadeDuration, crossfadeStyle, currentPlayer, isTransitioning, volume],
|
||||
);
|
||||
|
||||
const handleGapless1 = useCallback(
|
||||
(e: AudioPlayerProgress) => {
|
||||
return gaplessHandler({
|
||||
currentTime: e.playedSeconds,
|
||||
duration: getDuration(player1Ref),
|
||||
isFlac: player1?.container === 'flac',
|
||||
isTransitioning,
|
||||
nextPlayerRef: player2Ref,
|
||||
setIsTransitioning,
|
||||
});
|
||||
},
|
||||
[isTransitioning, player1?.container],
|
||||
);
|
||||
|
||||
const handleGapless2 = useCallback(
|
||||
(e: AudioPlayerProgress) => {
|
||||
return gaplessHandler({
|
||||
currentTime: e.playedSeconds,
|
||||
duration: getDuration(player2Ref),
|
||||
isFlac: player2?.container === 'flac',
|
||||
isTransitioning,
|
||||
nextPlayerRef: player1Ref,
|
||||
setIsTransitioning,
|
||||
});
|
||||
},
|
||||
[isTransitioning, player2?.container],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Not standard, just used in chromium-based browsers. See
|
||||
// https://developer.chrome.com/blog/audiocontext-setsinkid/.
|
||||
// If the isElectron() check is every removed, fix this.
|
||||
if (isElectron() && webAudio && 'setSinkId' in webAudio.context && audioDeviceId) {
|
||||
const setSink = async () => {
|
||||
try {
|
||||
if (webAudio.context.state !== 'closed') {
|
||||
await (webAudio.context as any).setSinkId(audioDeviceId);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error({ message: `Error setting sink: ${(error as Error).message}` });
|
||||
}
|
||||
};
|
||||
|
||||
setSink();
|
||||
}
|
||||
}, [audioDeviceId, webAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webAudio) return;
|
||||
|
||||
const sources = [player1Source ? player1 : null, player2Source ? player2 : null];
|
||||
const current = sources[currentPlayer - 1];
|
||||
|
||||
// Set the current replaygain
|
||||
if (current) {
|
||||
const newVolume = calculateReplayGain(current) * volume;
|
||||
webAudio.gain.gain.setValueAtTime(Math.max(0, newVolume), 0);
|
||||
}
|
||||
|
||||
// Set the next track replaygain right before the end of this track
|
||||
// Attempt to prevent pop-in for web audio.
|
||||
const next = sources[3 - currentPlayer];
|
||||
if (next && current) {
|
||||
const newVolume = calculateReplayGain(next) * volume;
|
||||
webAudio.gain.gain.setValueAtTime(
|
||||
Math.max(0, newVolume),
|
||||
Math.max(0, (current.duration - 1) / 1000),
|
||||
);
|
||||
}
|
||||
}, [
|
||||
calculateReplayGain,
|
||||
currentPlayer,
|
||||
player1,
|
||||
player1Source,
|
||||
player2,
|
||||
player2Source,
|
||||
volume,
|
||||
webAudio,
|
||||
]);
|
||||
|
||||
const handlePlayer1Start = useCallback(
|
||||
async (player: ReactPlayer) => {
|
||||
if (!webAudio) return;
|
||||
if (player1Source) {
|
||||
// This should fire once, only if the source is real (meaning we
|
||||
// saw the dummy source) and the context is not ready
|
||||
if (webAudio.context.state !== 'running') {
|
||||
await webAudio.context.resume();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
|
||||
if (internal) {
|
||||
const { context, gain } = webAudio;
|
||||
const source = context.createMediaElementSource(internal);
|
||||
source.connect(gain);
|
||||
setPlayer1Source(source);
|
||||
}
|
||||
},
|
||||
[player1Source, webAudio],
|
||||
);
|
||||
|
||||
const handlePlayer2Start = useCallback(
|
||||
async (player: ReactPlayer) => {
|
||||
if (!webAudio) return;
|
||||
if (player2Source) {
|
||||
if (webAudio.context.state !== 'running') {
|
||||
await webAudio.context.resume();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
|
||||
if (internal) {
|
||||
const { context, gain } = webAudio;
|
||||
const source = context.createMediaElementSource(internal);
|
||||
source.connect(gain);
|
||||
setPlayer2Source(source);
|
||||
}
|
||||
},
|
||||
[player2Source, webAudio],
|
||||
);
|
||||
|
||||
// Bugfix for Safari: rather than use the `<audio>` volume (which doesn't work),
|
||||
// use the GainNode to scale the volume. In this case, for compatibility with
|
||||
// other browsers, set the `<audio>` volume to 1
|
||||
return (
|
||||
<>
|
||||
<ReactPlayer
|
||||
config={{
|
||||
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
|
||||
}}
|
||||
height={0}
|
||||
muted={muted}
|
||||
// If there is no stream url, we do not need to handle when the audio finishes
|
||||
onEnded={stream1 ? handleOnEnded : undefined}
|
||||
onError={handleOnError(player1Ref)}
|
||||
onProgress={
|
||||
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
|
||||
}
|
||||
onReady={handlePlayer1Start}
|
||||
playbackRate={playbackSpeed}
|
||||
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
|
||||
progressInterval={isTransitioning ? 10 : 250}
|
||||
ref={player1Ref}
|
||||
url={stream1 || EMPTY_SOURCE}
|
||||
volume={webAudio ? 1 : volume}
|
||||
width={0}
|
||||
/>
|
||||
<ReactPlayer
|
||||
config={{
|
||||
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
|
||||
}}
|
||||
height={0}
|
||||
muted={muted}
|
||||
onEnded={stream2 ? handleOnEnded : undefined}
|
||||
onError={handleOnError(player2Ref)}
|
||||
onProgress={
|
||||
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
|
||||
}
|
||||
onReady={handlePlayer2Start}
|
||||
playbackRate={playbackSpeed}
|
||||
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
|
||||
progressInterval={isTransitioning ? 10 : 250}
|
||||
ref={player2Ref}
|
||||
url={stream2 || EMPTY_SOURCE}
|
||||
volume={webAudio ? 1 : volume}
|
||||
width={0}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
.play-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: rgb(255 255 255);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: scale 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: rgb(0 0 0);
|
||||
stroke: rgb(0 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: scale 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-card-controls-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
width: 100%;
|
||||
height: calc(100% / 3);
|
||||
}
|
||||
|
||||
.bottom-controls {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.favorite-wrapper {
|
||||
svg {
|
||||
fill: var(--theme-colors-primary-filled);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import type { PlayQueueAddOptions } from '/@/shared/types/types';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import styles from './card-controls.module.css';
|
||||
|
||||
import {
|
||||
ALBUM_CONTEXT_MENU_ITEMS,
|
||||
ARTIST_CONTEXT_MENU_ITEMS,
|
||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export const CardControls = ({
|
||||
handlePlayQueueAdd,
|
||||
itemData,
|
||||
itemType,
|
||||
}: {
|
||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||
itemData: any;
|
||||
itemType: LibraryItem;
|
||||
}) => {
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
const handlePlay = (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
id: [itemData.id],
|
||||
type: itemType,
|
||||
},
|
||||
playType: playType || playButtonBehavior,
|
||||
});
|
||||
};
|
||||
|
||||
const handleContextMenu = useHandleGeneralContextMenu(
|
||||
itemType,
|
||||
itemType === LibraryItem.ALBUM ? ALBUM_CONTEXT_MENU_ITEMS : ARTIST_CONTEXT_MENU_ITEMS,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.gridCardControlsContainer}>
|
||||
<div className={styles.bottomControls}>
|
||||
<button className={styles.playButton} onClick={handlePlay}>
|
||||
<Icon icon="mediaPlay" />
|
||||
</button>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
className={styles.secondaryButton}
|
||||
disabled
|
||||
p={5}
|
||||
style={{ svg: { fill: 'white !important' } }}
|
||||
variant="subtle"
|
||||
>
|
||||
<div className={itemData?.isFavorite ? styles.favoriteWrapper : ''}>
|
||||
{itemData?.isFavorite ? (
|
||||
<Icon icon="favorite" />
|
||||
) : (
|
||||
<Icon icon="favorite" />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
<ActionIcon
|
||||
className={styles.secondaryButton}
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleContextMenu(e, [itemData]);
|
||||
}}
|
||||
p={5}
|
||||
style={{ svg: { fill: 'white !important' } }}
|
||||
variant="subtle"
|
||||
>
|
||||
<Icon icon="ellipsisHorizontal" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
.row {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 22px;
|
||||
padding: 0 0.2rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--theme-colors-foreground);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.row.secondary {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import formatDuration from 'format-duration';
|
||||
import React from 'react';
|
||||
import { TFunction, useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import styles from './card-rows.module.css';
|
||||
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Artist,
|
||||
ExplicitStatus,
|
||||
Playlist,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { CardRow } from '/@/shared/types/types';
|
||||
|
||||
interface CardRowsProps {
|
||||
data: any;
|
||||
rows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
|
||||
}
|
||||
|
||||
export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{rows.map((row, index: number) => {
|
||||
if (row.arrayProperty && row.route) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.row, {
|
||||
[styles.secondary]: index > 0,
|
||||
})}
|
||||
key={`row-${row.property}-${index}`}
|
||||
>
|
||||
{data[row.property].map((item: any, itemIndex: number) => (
|
||||
<React.Fragment key={`${data.id}-${item.id}`}>
|
||||
{itemIndex > 0 && (
|
||||
<Text
|
||||
isMuted
|
||||
isNoSelect
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0 2px 0 1px',
|
||||
}}
|
||||
>
|
||||
,
|
||||
</Text>
|
||||
)}{' '}
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
isMuted={index > 0}
|
||||
isNoSelect
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
to={generatePath(
|
||||
row.route!.route,
|
||||
row.route!.slugs?.reduce((acc, slug) => {
|
||||
return {
|
||||
...acc,
|
||||
[slug.slugProperty]:
|
||||
data[row.property][itemIndex][
|
||||
slug.idProperty
|
||||
],
|
||||
};
|
||||
}, {}),
|
||||
)}
|
||||
>
|
||||
{row.arrayProperty &&
|
||||
(row.format
|
||||
? row.format(item, t)
|
||||
: item[row.arrayProperty])}
|
||||
</Text>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (row.arrayProperty) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.row, {
|
||||
[styles.secondary]: index > 0,
|
||||
})}
|
||||
key={`row-${row.property}`}
|
||||
>
|
||||
{data[row.property].map((item: any) => (
|
||||
<Text
|
||||
isMuted={index > 0}
|
||||
isNoSelect
|
||||
key={`${data.id}-${item.id}`}
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
>
|
||||
{row.arrayProperty &&
|
||||
(row.format
|
||||
? row.format(item, t)
|
||||
: item[row.arrayProperty])}
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.row, {
|
||||
[styles.secondary]: index > 0,
|
||||
})}
|
||||
key={`row-${row.property}`}
|
||||
>
|
||||
{row.route ? (
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
isNoSelect
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
overflow="hidden"
|
||||
to={generatePath(
|
||||
row.route.route,
|
||||
row.route.slugs?.reduce((acc, slug) => {
|
||||
return {
|
||||
...acc,
|
||||
[slug.slugProperty]: data[slug.idProperty],
|
||||
};
|
||||
}, {}),
|
||||
)}
|
||||
>
|
||||
{data && (row.format ? row.format(data, t) : data[row.property])}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
isMuted={index > 0}
|
||||
isNoSelect
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
>
|
||||
{data && (row.format ? row.format(data, t) : data[row.property])}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ALBUM_CARD_ROWS: { [key: string]: CardRow<Album> } = {
|
||||
albumArtists: {
|
||||
arrayProperty: 'name',
|
||||
property: 'albumArtists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
artists: {
|
||||
arrayProperty: 'name',
|
||||
property: 'artists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
format: (song) => formatDateAbsolute(song.createdAt),
|
||||
property: 'createdAt',
|
||||
},
|
||||
duration: {
|
||||
format: (album) => (album.duration === null ? null : formatDuration(album.duration)),
|
||||
property: 'duration',
|
||||
},
|
||||
explicitStatus: {
|
||||
format: (album, t: TFunction) =>
|
||||
album.explicitStatus === ExplicitStatus.EXPLICIT
|
||||
? t('common.explicit', { postProcess: 'sentenceCase' })
|
||||
: album.explicitStatus === ExplicitStatus.CLEAN
|
||||
? t('common.clean', { postProcess: 'sentenceCase' })
|
||||
: null,
|
||||
property: 'explicitStatus',
|
||||
},
|
||||
lastPlayedAt: {
|
||||
format: (album) => formatDateRelative(album.lastPlayedAt),
|
||||
property: 'lastPlayedAt',
|
||||
},
|
||||
name: {
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||
},
|
||||
},
|
||||
playCount: {
|
||||
property: 'playCount',
|
||||
},
|
||||
rating: {
|
||||
format: (album) => formatRating(album),
|
||||
property: 'userRating',
|
||||
},
|
||||
releaseDate: {
|
||||
property: 'releaseDate',
|
||||
},
|
||||
releaseYear: {
|
||||
property: 'releaseYear',
|
||||
},
|
||||
songCount: {
|
||||
property: 'songCount',
|
||||
},
|
||||
};
|
||||
|
||||
export const SONG_CARD_ROWS: { [key: string]: CardRow<Song> } = {
|
||||
album: {
|
||||
property: 'album',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'albumId', slugProperty: 'albumId' }],
|
||||
},
|
||||
},
|
||||
albumArtists: {
|
||||
arrayProperty: 'name',
|
||||
property: 'albumArtists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
artists: {
|
||||
arrayProperty: 'name',
|
||||
property: 'artists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
format: (song) => formatDateAbsolute(song.createdAt),
|
||||
property: 'createdAt',
|
||||
},
|
||||
duration: {
|
||||
format: (song) => (song.duration === null ? null : formatDuration(song.duration)),
|
||||
property: 'duration',
|
||||
},
|
||||
explicitStatus: {
|
||||
format: (song, t: TFunction) =>
|
||||
song.explicitStatus === ExplicitStatus.EXPLICIT
|
||||
? t('common.explicit', { postProcess: 'sentenceCase' })
|
||||
: song.explicitStatus === ExplicitStatus.CLEAN
|
||||
? t('common.clean', { postProcess: 'sentenceCase' })
|
||||
: null,
|
||||
property: 'explicitStatus',
|
||||
},
|
||||
lastPlayedAt: {
|
||||
format: (song) => formatDateRelative(song.lastPlayedAt),
|
||||
property: 'lastPlayedAt',
|
||||
},
|
||||
name: {
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'albumId', slugProperty: 'albumId' }],
|
||||
},
|
||||
},
|
||||
playCount: {
|
||||
property: 'playCount',
|
||||
},
|
||||
rating: {
|
||||
format: (song) => formatRating(song),
|
||||
property: 'userRating',
|
||||
},
|
||||
releaseDate: {
|
||||
property: 'releaseDate',
|
||||
},
|
||||
releaseYear: {
|
||||
property: 'releaseYear',
|
||||
},
|
||||
};
|
||||
|
||||
export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow<AlbumArtist> } = {
|
||||
albumCount: {
|
||||
property: 'albumCount',
|
||||
},
|
||||
duration: {
|
||||
format: (artist) => (artist.duration === null ? null : formatDuration(artist.duration)),
|
||||
property: 'duration',
|
||||
},
|
||||
genres: {
|
||||
property: 'genres',
|
||||
},
|
||||
lastPlayedAt: {
|
||||
format: (artist) => formatDateRelative(artist.lastPlayedAt),
|
||||
property: 'lastPlayedAt',
|
||||
},
|
||||
name: {
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
playCount: {
|
||||
property: 'playCount',
|
||||
},
|
||||
rating: {
|
||||
format: (artist) => formatRating(artist),
|
||||
property: 'userRating',
|
||||
},
|
||||
songCount: {
|
||||
property: 'songCount',
|
||||
},
|
||||
};
|
||||
|
||||
export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow<Playlist> } = {
|
||||
duration: {
|
||||
format: (playlist) =>
|
||||
playlist.duration === null ? null : formatDuration(playlist.duration),
|
||||
property: 'duration',
|
||||
},
|
||||
name: {
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
|
||||
},
|
||||
},
|
||||
nameFull: {
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
property: 'owner',
|
||||
},
|
||||
public: {
|
||||
property: 'public',
|
||||
},
|
||||
songCount: {
|
||||
property: 'songCount',
|
||||
},
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
|
||||
&:global(.card-controls) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
aspect-ratio: 1/1;
|
||||
overflow: hidden;
|
||||
background: var(--theme-card-default-bg);
|
||||
border-radius: var(--theme-card-poster-radius);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
content: '';
|
||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:global(.card-controls) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100% !important;
|
||||
max-height: 100%;
|
||||
border: 0;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: var(--theme-image-fit);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user