Compare commits

...

119 Commits

Author SHA1 Message Date
Kendall Garner 41c2a7da69 make artist stats for navidrome api sane 2025-03-09 19:21:16 -07:00
jeffvli 592376316f Update to v0.12.3 2025-03-09 17:06:40 -07:00
Kendall Garner c6d7dc0b32 prepare bfr changes (#882)
* prepare bfr changes

* contributors to subsonic/navidrome

* show performer roles

* Add BFR smart playlist fields

* Fix upload-artifact action to handle v4

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
2025-03-09 16:55:27 -07:00
Kendall Garner 571aacbaa0 Merge branch 'development' of github.com:jeffvli/feishin into development 2025-02-27 16:43:21 -08:00
Kendall Garner ef194424e3 support css variables in sanitize 2025-02-27 16:42:02 -08:00
jeffvli 233c24cea6 Update to v0.12.2 2025-01-24 23:19:55 -08:00
jeffvli b48b0b0d11 Fix page header overlay on fullscreen player (#867) 2025-01-24 17:37:58 -08:00
jeffvli 65fe42d30c Add new languages to config 2025-01-24 16:47:49 -08:00
jeffvli 90ae7130f6 Fix invalid args on nd normalize 2025-01-24 16:44:02 -08:00
jeffvli d866dd211a Bump electron from v31 -> v33 2025-01-24 16:44:02 -08:00
Hosted Weblate 81446561e0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 47.4% (312 of 657 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 41.0% (270 of 657 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 30.2% (199 of 657 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 20.2% (133 of 657 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: klodrik <klodrik@zoominn.no>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/nb_NO/
Translation: feishin/Translation
2025-01-15 22:35:53 +01:00
Hosted Weblate 4ef7213cf5 Translated using Weblate (Finnish)
Currently translated at 59.8% (393 of 657 strings)

Translated using Weblate (Finnish)

Currently translated at 31.6% (208 of 657 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Lauri Koo <late91@gmail.com>
Co-authored-by: jonoafi <joona@jonottaa.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fi/
Translation: feishin/Translation
2025-01-15 22:35:53 +01:00
Hosted Weblate b639d7bfab Translated using Weblate (Persian)
Currently translated at 64.6% (425 of 657 strings)

Translated using Weblate (Persian)

Currently translated at 62.7% (412 of 657 strings)

Translated using Weblate (Persian)

Currently translated at 39.2% (258 of 657 strings)

Co-authored-by: Hadi <xhopeter@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fa/
Translation: feishin/Translation
2025-01-15 22:35:53 +01:00
Hosted Weblate 35e0af5164 Translated using Weblate (French)
Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Dylan MONTIGAUD <dylanmontigaud17@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2025-01-15 22:35:53 +01:00
Hosted Weblate 86cca7fca5 Translated using Weblate (Indonesian)
Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Indonesian)

Currently translated at 6.0% (40 of 657 strings)

Added translation using Weblate (Indonesian)

Co-authored-by: Fadilah Riczky <friczky@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/id/
Translation: feishin/Translation
2025-01-15 22:35:53 +01:00
Hosted Weblate 132ebb6b52 Translated using Weblate (Hungarian)
Currently translated at 31.6% (208 of 657 strings)

Added translation using Weblate (Hungarian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: WilliamNT <hwbendeguz@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/hu/
Translation: feishin/Translation
2025-01-15 22:35:53 +01:00
Hosted Weblate e23906582a Translated using Weblate (Tamil)
Currently translated at 100.0% (657 of 657 strings)

Added translation using Weblate (Tamil)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ta/
Translation: feishin/Translation
2025-01-15 22:35:53 +01:00
jeffvli e57232f89c fix array parameter parsing for subsonic client 2024-12-31 05:56:34 -08:00
ももぴ f0978365d4 fix: fetch album art via Last.fm should use album artist name (#855) 2024-12-19 20:31:44 -08:00
Penelope Gwen / Pogmommy ae65922253 decoded plaintext credentials before params get re-encoded in ssApiClient (#862) 2024-12-19 20:31:07 -08:00
jeffvli ca58551b94 Make client param on SS/ND queries consistent 2024-12-19 17:15:07 -08:00
jeffvli be0ebac362 Update to v0.12.1 2024-11-20 10:50:04 -08:00
Mitja Ševerkar 8eb8290fc4 Fix URL encoding on Subsonic (#850)
* Revert "Encode credential for subsonic stream/coverart (#841)"

This reverts commit 8ec4551b46.

* Properly URL encode credentials on Subsonic

Previous commit (8ec4551b46) has been reverted, as it has encoded even equal signs (=), and and signs (&), which should not have been encoded. Nextcloud Music has subsequently failed to receive separate username and password and has therefore failed whilst authenticating the user.

Example of URL beforehand:
https://cloud.example.com/index.php/apps/music/subsonic/rest/stream.view?id=track-4936&v=1.13.0&c=feishin_&u%3Dtest-test%40example.com%26p%3Dpassword

Example of URL now:
https://cloud.example.com/index.php/apps/music/subsonic/rest/stream.view?id=track-4936&v=1.13.0&c=feishin_&u=test-test%40example.com&p=password
2024-11-19 19:00:53 -08:00
jeffvli fac1d3fb62 Update to v0.12.0 2024-11-18 20:43:41 -08:00
Hosted Weblate 93fbe1f49a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-11-19 04:37:48 +00:00
Hosted Weblate 59f17a4faa Translated using Weblate (Korean)
Currently translated at 35.0% (230 of 657 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Korean)

Currently translated at 26.5% (174 of 655 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: tgp0625 <tgp0625@naver.com>
Co-authored-by: ᄒᄋ <prohack1109@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ko/
Translation: feishin/Translation
2024-11-19 04:37:47 +00:00
Hosted Weblate d9e41720c8 Translated using Weblate (Spanish)
Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2024-11-19 04:37:46 +00:00
Hosted Weblate 8452780602 Translated using Weblate (Czech)
Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2024-11-19 04:37:46 +00:00
jeffvli 96b5b660fb Add maintenance notice 2024-11-18 20:37:33 -08:00
Martin 610138c05c README.md: Colorize YAML block, fix macOS capitalization and remove deprecated compose version (#847) 2024-11-18 20:22:13 -08:00
jeffvli 6a619240fa Handle potential undefined value on CardRows (#834) 2024-11-18 20:17:33 -08:00
jeffvli b65c972da1 Handle negative values on gain calculation (#834) 2024-11-18 20:16:20 -08:00
jeffvli 8ec4551b46 Encode credential for subsonic stream/coverart (#841) 2024-11-13 17:54:54 -08:00
Jack Merrill 21f4a78dd7 feat: Discord Rich Presence album art via Last.fm (#341) (#817)
* feat: Discord Rich Presence album art via Last.fm

* fix: securely fetch album art
2024-10-31 12:09:17 -07:00
sel10ut 61d7e7c390 fix(jellyfin): return "Appears On" section to artist page (#812)
Exclude 'AlbumArtistIds' when querying "Appears On" items, which,
if put together with 'ContributingArtistIds', returns an empty list.
2024-10-31 11:33:10 -07:00
astrid 993841ddbf fix devEngines (#801) 2024-10-31 11:31:58 -07:00
jeffvli 98b8409592 Update description to include subsonic servers 2024-10-15 03:22:21 -07:00
jeffvli d3480a86c3 Fix release date parsing to use UTC (#794) 2024-10-15 03:15:59 -07:00
jeffvli 3a63ee4b95 Include all playlist types in Jellyfin playlist fetch 2024-10-15 03:04:38 -07:00
jeffvli 876376d65f Update to v0.11.1 2024-10-14 20:26:21 -07:00
Hosted Weblate 215abf615d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: ENDzZ <godzmichael@outlook.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-10-15 04:37:52 +02:00
Hosted Weblate afad2843c6 Translated using Weblate (Spanish)
Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2024-10-15 04:37:52 +02:00
Hosted Weblate 958ab1f31f Translated using Weblate (Czech)
Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2024-10-15 04:37:51 +02:00
Hosted Weblate 0ca325aac2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 31.1% (204 of 655 strings)

Co-authored-by: Rafael Vieira <rafaelvieiras@pm.me>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/
Translation: feishin/Translation
2024-10-15 04:37:50 +02:00
jeffvli 12b66e5fa0 Convert subsonic coverart property to string (#795) 2024-10-14 19:36:51 -07:00
jeffvli 7e78478fbe Fix combined title cell controls blocking links 2024-10-14 00:38:28 -07:00
jeffvli f783a6360e Update to v0.11.0 2024-10-09 20:04:42 -07:00
Hosted Weblate 8eb6c6a213 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (653 of 653 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-10-10 03:28:31 +02:00
Hosted Weblate ea5f0268cb Translated using Weblate (Serbian)
Currently translated at 79.0% (516 of 653 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Reportiv <reportiv@gmx.de>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/sr/
Translation: feishin/Translation
2024-10-10 03:28:31 +02:00
Hosted Weblate 427272f8c8 Translated using Weblate (French)
Currently translated at 100.0% (653 of 653 strings)

Translated using Weblate (French)

Currently translated at 100.0% (653 of 653 strings)

Co-authored-by: Benjamin <iipython@proton.me>
Co-authored-by: D.M <dylan.montigaud@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2024-10-10 03:28:31 +02:00
Hosted Weblate 40d09404b3 Translated using Weblate (Spanish)
Currently translated at 100.0% (653 of 653 strings)

Co-authored-by: Benjamin <iipython@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2024-10-10 03:28:31 +02:00
Hosted Weblate f3ee198833 Translated using Weblate (Czech)
Currently translated at 100.0% (653 of 653 strings)

Co-authored-by: Benjamin <iipython@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2024-10-10 03:28:31 +02:00
Hosted Weblate 5416d6e596 Translated using Weblate (Russian)
Currently translated at 95.7% (625 of 653 strings)

Co-authored-by: Benjamin <iipython@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ru/
Translation: feishin/Translation
2024-10-10 03:28:31 +02:00
Hosted Weblate a0639cbd27 Translated using Weblate (English)
Currently translated at 100.0% (653 of 653 strings)

Co-authored-by: Benjamin <iipython@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/en/
Translation: feishin/Translation
2024-10-10 03:28:31 +02:00
Hosted Weblate 790961f29a Translated using Weblate (German)
Currently translated at 87.2% (570 of 653 strings)

Co-authored-by: Achim Walz <achim@aalso-walz.de>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/de/
Translation: feishin/Translation
2024-10-10 03:28:31 +02:00
jeffvli 18027d4292 Remove current song list index animation (#783) 2024-10-09 18:27:48 -07:00
jeffvli a8b3944c66 Set row play button to switch to song on queue lists 2024-10-09 18:20:04 -07:00
Trevor a00385e78f Add "Move to next" button to queue (#781) 2024-10-09 18:00:25 -07:00
Egor 5e628d96c7 Some fixes to #772 (Add play button to song table) (#784)
* Add play button to song table album cover, like it is in grid

* Fix: play button caused error for albums and artists tables

* Fix: play button caused error for some other tables
2024-10-09 17:40:30 -07:00
Egor ad34d8553e Add play button to song table album cover, like it is in grid (#772)
* Add play button to song table album cover, like it is in grid

* Fix: play button caused error for albums and artists tables
2024-10-03 19:22:51 -07:00
Kendall Garner a89b6640a9 horizontal scroll 2024-10-01 18:15:18 -07:00
Kendall Garner b3b810c62c funkwhale bodge 2024-10-01 17:21:28 -07:00
Kendall Garner ecef9bea5e fix speed state 2024-09-29 16:16:33 -07:00
Hosted Weblate c7214fc7ce Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (652 of 653 strings)

Co-authored-by: ENDzZ <godzmichael@outlook.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-09-29 06:44:18 +02:00
Hosted Weblate 84bcfb6eb9 Translated using Weblate (Spanish)
Currently translated at 99.8% (652 of 653 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2024-09-29 06:44:17 +02:00
Hosted Weblate 0ca7a0efc9 Translated using Weblate (Czech)
Currently translated at 100.0% (653 of 653 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2024-09-29 06:44:17 +02:00
Torsten Curdt cb76436a2a point help menu items to feishin instead of just electron (#767) 2024-09-28 21:44:13 -07:00
jeffvli 88a951e2e7 Update to v0.10.1 2024-09-28 21:39:30 -07:00
jeffvli 6f1b78c2d6 Fix Subsonic servertype lock from docker configuration 2024-09-28 21:37:09 -07:00
jeffvli 107074b240 Fix subsonic album list sort options 2024-09-28 21:35:32 -07:00
Kendall Garner 6e8ca7e035 fix navidrome getPlaylistSOngList end 2024-09-28 21:22:14 -07:00
jeffvli 3c99a662e8 Fix album detail header track count 2024-09-26 21:23:48 -07:00
jeffvli fc8110ca79 Fix layout shift on lyrics container hover 2024-09-26 21:21:54 -07:00
jeffvli 715f800788 Add getStructuredLyrics to navidrome controller 2024-09-26 12:13:29 -07:00
jeffvli 244aee45cd Handle potential null response on genre results from Navidrome 2024-09-26 12:13:03 -07:00
jeffvli c96f5b207d Handle subsonic endpoints that potentially return optional response when no items 2024-09-26 11:14:06 -07:00
Hosted Weblate 0e8b2aed72 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (642 of 642 strings)

Co-authored-by: ENDzZ <godzmichael@outlook.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-09-26 06:57:33 +02:00
Hosted Weblate f2accd63fd Translated using Weblate (French)
Currently translated at 96.5% (620 of 642 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jucgshu <brewal.bouvet@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2024-09-26 06:57:33 +02:00
Hosted Weblate c024e975fb Translated using Weblate (Spanish)
Currently translated at 100.0% (642 of 642 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2024-09-26 06:57:33 +02:00
Hosted Weblate a3f725b0ef Translated using Weblate (Czech)
Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (642 of 642 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2024-09-26 06:57:33 +02:00
Hosted Weblate f137f487aa Translated using Weblate (English)
Currently translated at 98.1% (641 of 653 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/en/
Translation: feishin/Translation
2024-09-26 06:57:33 +02:00
jeffvli 8e59514524 Update to v.10.0 2024-09-25 21:57:19 -07:00
Mikhail Tsarev 7bcfe30a8e Improved translations for English and Russian versions. (#760)
* First version of Russian translation

* Improvements

---------

Co-authored-by: Suoslex <mtsarev06@gmail.com>
2024-09-25 21:42:41 -07:00
Kendall Garner 8cddbef701 Subsonic 2, general rework (#758) 2024-09-25 21:23:08 -07:00
Xudong Zhou 31492fa9ef Lyrics Translation and Romaji (Fulfill #732) [Translation Part] (#747) 2024-09-23 20:25:17 -07:00
Jeff e3946a9413 Update Navidrome list sort mappings for ND v0.53.2 (#754)
* Update navidrome list sort mappings

* Rename ownerName to owner_name

* Remove deprecated client-side sort
2024-09-22 18:37:13 -07:00
Jamie King 28c12496f1 Removed references to "ElectronReact" in MacOS menu bar (#756) 2024-09-20 13:26:28 +00:00
jeffvli f8c2ff735b Replace "SortName" for "Name" in Jellyfin song sort 2024-09-19 20:39:47 -07:00
Kendall Garner 22e4974191 refactor navidrome-types 2024-09-18 20:43:35 -07:00
Kendall Garner 6eecc3c0fd handle sanitized sort and filter post-fact 2024-09-18 18:00:25 -07:00
Kendall Garner b628b684ae require limit to specified (nonzero) for shuffle all 2024-09-18 07:31:58 -07:00
Kendall Garner 4c49e403ab make headers optional? 2024-09-16 19:57:59 -07:00
Kendall Garner 730683fe25 different date formats based off of metadata 2024-09-16 17:33:06 -07:00
Kendall Garner 96b4f8dd89 update album play count 2024-09-15 21:48:32 -07:00
Kendall Garner f82889e5ec npm audit fix 2024-09-15 20:05:48 -07:00
Benjamin 8d8826a9b7 use utc for absolute date formatting (#743)
* use utc for date formatting

* add seperate utc function and call that instead

* swap date format to be a constant

* make dateadded use non-utc
2024-09-13 01:35:57 +00:00
Kendall Garner 660c9744bf clear queue when shuffle now 2024-09-11 20:36:46 -07:00
Kendall Garner 8221af9a8f break by newline for comment 2024-09-11 07:41:15 -07:00
jeffvli 93f2573847 Update to v0.9.0 2024-09-10 22:37:48 -07:00
Kendall Garner 03d97c6b1e use unique id for paginated playlist 2024-09-10 22:37:24 -07:00
Hosted Weblate 0b86cb51d3 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-09-10 04:01:31 +02:00
Hosted Weblate ee54b8219b Translated using Weblate (French)
Currently translated at 96.8% (610 of 630 strings)

Co-authored-by: Evan <evan_g@orange.fr>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2024-09-10 04:01:31 +02:00
Hosted Weblate 25b593aadd Translated using Weblate (Spanish)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (632 of 632 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (631 of 632 strings)

Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2024-09-10 04:01:31 +02:00
Hosted Weblate 5253e32b67 Translated using Weblate (Czech)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (632 of 632 strings)

Translated using Weblate (Czech)

Currently translated at 99.6% (630 of 632 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2024-09-10 04:01:31 +02:00
Hosted Weblate b0b558c90a Translated using Weblate (German)
Currently translated at 85.0% (536 of 630 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Schroti <schrotihd@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/de/
Translation: feishin/Translation
2024-09-10 04:01:31 +02:00
Kendall Garner f11a53c1a4 fix suspense 2024-09-09 19:01:07 -07:00
Kendall Garner e2a05f4204 add track normalization for jellyfin as well 2024-09-09 07:15:26 -07:00
Kendall Garner fcc010eb54 move transcoding placeholder 2024-09-08 22:05:44 -07:00
Kendall Garner 1b41a5a674 enable disabling tray 2024-09-08 20:55:07 -07:00
Kendall Garner 74aa88e082 add web visualizer (#314)
* add web visualizer

* fallback to simple model

* less samples, hopefully more efficient

* Use audiomotion analyzer

- Note: fixed to 4.1.1 because 4.2.0 uses esm which breaks in the current workflow...

* revert publish changes

* r2

* don't massively change package.json

* lazy
2024-09-09 01:25:01 +00:00
Kendall Garner fbac33ceba add shuffle context menu item 2024-09-07 21:31:01 -07:00
Kendall Garner 42ba5a531c use feishin switch instead of default 2024-09-05 18:08:37 -07:00
Kendall Garner 257e1e2cd9 ... 2024-09-05 07:06:37 -07:00
Kendall Garner 3025e84c58 remove height everywhere for jellyfin images 2024-09-04 22:30:50 -07:00
Kendall Garner 4a111d9cf2 don't make artist clickable if no id 2024-09-04 20:01:45 -07:00
Kendall Garner e6bd8deb0c use unique id for places that may have duplicates 2024-09-04 19:34:07 -07:00
jeffvli 6b0c57998b Update to v0.8.1 2024-09-03 21:53:19 -07:00
jeffvli 6587e9cac8 Fix invalid DOM prop on playerbar 2024-09-03 21:51:50 -07:00
jeffvli 2e3c69e61c Fix song index skip when viewing synchronized lyrics 2024-09-03 21:51:18 -07:00
156 changed files with 10101 additions and 4416 deletions
+2 -4
View File
@@ -1,5 +1,5 @@
name: Feature request
description: Request a feature to be added to Feishin 🎉
name: Feature request - NOT ACCEPTING NEW FEATURE REQUESTS
description: Feature requests are currently closed. The application is actively being rewritten https://github.com/audioling/audioling.
labels: ['enhancement']
body:
- type: textarea
@@ -18,5 +18,3 @@ body:
options:
- label: 'Yes'
required: false
validations:
required: false
+52 -17
View File
@@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
os: [macos-latest]
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Checkout git repo
@@ -27,34 +27,69 @@ jobs:
run: |
npm install --legacy-peer-deps
- name: Build releases
- name: Build for Windows
if: ${{ matrix.os == 'windows-latest' }}
uses: nick-invision/retry@v2.8.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
npm run postinstall
npm run build
npm run package:pr
on_retry_command: npm cache clean --force
npm run package:pr:windows
- uses: actions/upload-artifact@v3
- name: Build for Linux
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: nick-invision/retry@v2.8.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
npm run package:pr:linux
- name: Build for MacOS
if: ${{ matrix.os == 'macos-latest' }}
uses: nick-invision/retry@v2.8.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
npm run package:pr:macos
- name: Zip Windows Binaries
if: ${{ matrix.os == 'windows-latest' }}
shell: pwsh
run: |
Compress-Archive -Path "release/build/*.exe" -DestinationPath "release/build/windows-binaries.zip" -Force
- name: Zip Linux Binaries
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
zip -r release/build/linux-binaries.zip release/build/*.{AppImage,deb,rpm}
- name: Zip MacOS Binaries
if: ${{ matrix.os == 'macos-latest' }}
run: |
zip -r release/build/macos-binaries.zip release/build/*.dmg
- name: Upload Windows Binaries
if: ${{ matrix.os == 'windows-latest' }}
uses: actions/upload-artifact@v4
with:
name: windows-binaries
path: |
release/build/*.exe
path: release/build/windows-binaries.zip
- uses: actions/upload-artifact@v3
- name: Upload Linux Binaries
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-artifact@v4
with:
name: linux-binaries
path: |
release/build/*.AppImage
release/build/*.deb
release/build/*.rpm
path: release/build/linux-binaries.zip
- uses: actions/upload-artifact@v3
- name: Upload MacOS Binaries
if: ${{ matrix.os == 'macos-latest' }}
uses: actions/upload-artifact@v4
with:
name: macos-binaries
path: |
release/build/*.dmg
path: release/build/macos-binaries.zip
+43 -23
View File
@@ -27,6 +27,16 @@
</a>
</p>
---
## MAINTENANCE NOTICE
Feishin is currently undergoing a major rewrite. New feature requests will not be accepted. The rewrite is being actively developed at the [audioling](https://github.com/audioling/audioling) repository.
Follow the repository or join the discord/matrix server for updates.
---
Rewrite of [Sonixd](https://github.com/jeffvli/sonixd).
## Features
@@ -49,7 +59,7 @@ Rewrite of [Sonixd](https://github.com/jeffvli/sonixd).
Download the [latest desktop client](https://github.com/jeffvli/feishin/releases). The desktop client is the recommended way to use Feishin. It supports both the MPV and web player backends, as well as includes built-in fetching for lyrics.
#### MacOS Notes
#### macOS Notes
If you're using a device running macOS 12 (Monterey) or higher, [check here](https://github.com/jeffvli/feishin/issues/104#issuecomment-1553914730) for instructions on how to remove the app from quarantine.
@@ -71,27 +81,27 @@ docker run --name feishin -p 9180:9180 feishin
```
#### Docker Compose
To install via Docker Compose use the following snippit. This also works on Portainer.
```
version: '3'
services:
feishin:
container_name: feishin
image: 'ghcr.io/jeffvli/feishin:latest'
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
- PUID=1000
- PGID=1000
- UMASK=002
- TZ=America/Los_Angeles
ports:
- 9180:9180
restart: unless-stopped
```
To install via Docker Compose use the following snippit. This also works on Portainer.
```yaml
services:
feishin:
container_name: feishin
image: 'ghcr.io/jeffvli/feishin:latest'
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
- PUID=1000
- PGID=1000
- UMASK=002
- TZ=America/Los_Angeles
ports:
- 9180:9180
restart: unless-stopped
```
### Configuration
@@ -118,8 +128,16 @@ Feishin supports any music server that implements a [Navidrome](https://www.navi
- [Navidrome](https://github.com/navidrome/navidrome)
- [Jellyfin](https://github.com/jellyfin/jellyfin)
- [Funkwhale](https://funkwhale.audio/) - TBD
- Subsonic-compatible servers - TBD
- Subsonic-compatible servers
- [Airsonic-Advanced](https://github.com/airsonic-advanced/airsonic-advanced)
- [Ampache](https://ampache.org)
- [Astiga](https://asti.ga/)
- [Funkwhale](https://www.funkwhale.audio/)
- [Gonic](https://github.com/sentriz/gonic)
- [LMS](https://github.com/epoupon/lms)
- [Nextcloud Music](https://apps.nextcloud.com/apps/music)
- [Supysonic](https://github.com/spl0k/supysonic)
- More (?)
### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux
@@ -130,6 +148,8 @@ chmod 4755 chrome-sandbox
sudo chown root:root chrome-sandbox
```
Ubunutu 24.04 specifically introduced breaking changes that affect how namespaces work. Please see https://discourse.ubuntu.com/t/ubuntu-24-04-lts-noble-numbat-release-notes/39890#:~:text=security%20improvements%20 for possible fixes.
## Development
Built and tested using Node `v16.15.0`.
+191 -115
View File
@@ -1,12 +1,12 @@
{
"name": "feishin",
"version": "0.8.0",
"version": "0.12.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "feishin",
"version": "0.8.0",
"version": "0.12.2",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {
@@ -28,6 +28,7 @@
"@tanstack/react-query-persist-client": "^4.32.1",
"@ts-rest/core": "^3.23.0",
"@xhayper/discord-rpc": "^1.0.24",
"audiomotion-analyzer": "^4.5.0",
"auto-text-size": "^0.2.3",
"axios": "^1.6.0",
"clsx": "^2.0.0",
@@ -107,7 +108,7 @@
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"detect-port": "^1.3.0",
"electron": "^31.2.0",
"electron": "^33.3.1",
"electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0",
"electron-notarize": "^1.2.1",
@@ -6740,6 +6741,16 @@
"node": ">=10.12.0"
}
},
"node_modules/audiomotion-analyzer": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/audiomotion-analyzer/-/audiomotion-analyzer-4.5.0.tgz",
"integrity": "sha512-qnmB8TSbrxYkTbFgsQeeym0Z/suQx4c0jFg9Yh5+gaPw6J4AFLdfFpagdnDbtNEsj6K7BntgsC3bkdut5rxozg==",
"license": "AGPL-3.0-or-later",
"funding": {
"type": "Ko-fi",
"url": "https://ko-fi.com/hvianna"
}
},
"node_modules/auto-text-size": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/auto-text-size/-/auto-text-size-0.2.3.tgz",
@@ -7041,10 +7052,11 @@
}
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@@ -7054,7 +7066,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -7069,6 +7081,7 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -7078,6 +7091,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
@@ -7087,6 +7101,7 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -7096,6 +7111,7 @@
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
@@ -7107,7 +7123,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/bonjour-service": {
"version": "1.0.11",
@@ -8508,6 +8525,7 @@
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -9371,6 +9389,7 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
@@ -9745,8 +9764,9 @@
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
"dev": true
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"dev": true,
"license": "MIT"
},
"node_modules/ejs": {
"version": "3.1.10",
@@ -9764,9 +9784,9 @@
}
},
"node_modules/electron": {
"version": "31.2.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-31.2.0.tgz",
"integrity": "sha512-5w+kjOsGiTXytPSErBPNp/3znnuEMKc42RD41MqRoQkiYaR8x/Le2+qWk1cL60UwE/67oeKnOHnnol8xEuldGg==",
"version": "33.3.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-33.3.2.tgz",
"integrity": "sha512-2pWr0frM9UrZGX1d7eoFdMROw10h2vXIWJmXdjwlKnSWWUm18GCrEOUeDUr+IMgz5EjO7JM7FQDHDMApeMgHyg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -10206,10 +10226,11 @@
}
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -11873,8 +11894,9 @@
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -11961,37 +11983,38 @@
"license": "Apache-2.0"
},
"node_modules/express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"dev": true,
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.2",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@@ -12282,13 +12305,14 @@
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@@ -12304,6 +12328,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
@@ -12311,14 +12336,16 @@
"node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/finalhandler/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -12478,8 +12505,9 @@
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -13357,6 +13385,7 @@
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
@@ -13373,6 +13402,7 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -13382,6 +13412,7 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -16584,6 +16615,7 @@
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -16705,10 +16737,14 @@
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
"dev": true
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
@@ -17443,6 +17479,7 @@
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
@@ -17881,10 +17918,11 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
"dev": true
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"dev": true,
"license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
@@ -18888,12 +18926,13 @@
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.4"
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
@@ -18992,6 +19031,7 @@
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
@@ -19007,6 +19047,7 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -19016,6 +19057,7 @@
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
@@ -20123,10 +20165,11 @@
"optional": true
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@@ -20151,6 +20194,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
@@ -20158,14 +20202,26 @@
"node_modules/send/node_modules/debug/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/send/node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -20175,6 +20231,7 @@
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"license": "MIT",
"bin": {
"mime": "cli.js"
},
@@ -20186,13 +20243,15 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/send/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -20227,10 +20286,11 @@
}
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
}
@@ -20296,15 +20356,16 @@
"dev": true
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
@@ -20363,7 +20424,8 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true
"dev": true,
"license": "ISC"
},
"node_modules/shallow-clone": {
"version": "3.0.1",
@@ -21972,6 +22034,7 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6"
}
@@ -22363,6 +22426,7 @@
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
@@ -22621,8 +22685,9 @@
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -28672,6 +28737,11 @@
"resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
"integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="
},
"audiomotion-analyzer": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/audiomotion-analyzer/-/audiomotion-analyzer-4.5.0.tgz",
"integrity": "sha512-qnmB8TSbrxYkTbFgsQeeym0Z/suQx4c0jFg9Yh5+gaPw6J4AFLdfFpagdnDbtNEsj6K7BntgsC3bkdut5rxozg=="
},
"auto-text-size": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/auto-text-size/-/auto-text-size-0.2.3.tgz",
@@ -28899,9 +28969,9 @@
}
},
"body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dev": true,
"requires": {
"bytes": "3.1.2",
@@ -28912,7 +28982,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -30881,7 +30951,7 @@
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"dev": true
},
"ejs": {
@@ -30894,9 +30964,9 @@
}
},
"electron": {
"version": "31.2.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-31.2.0.tgz",
"integrity": "sha512-5w+kjOsGiTXytPSErBPNp/3znnuEMKc42RD41MqRoQkiYaR8x/Le2+qWk1cL60UwE/67oeKnOHnnol8xEuldGg==",
"version": "33.3.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-33.3.2.tgz",
"integrity": "sha512-2pWr0frM9UrZGX1d7eoFdMROw10h2vXIWJmXdjwlKnSWWUm18GCrEOUeDUr+IMgz5EjO7JM7FQDHDMApeMgHyg==",
"dev": true,
"requires": {
"@electron/get": "^2.0.0",
@@ -31251,9 +31321,9 @@
"dev": true
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true
},
"encoding": {
@@ -32409,7 +32479,7 @@
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"dev": true
},
"eventemitter3": {
@@ -32474,37 +32544,37 @@
"dev": true
},
"express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"dev": true,
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.2",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@@ -32730,13 +32800,13 @@
}
},
"finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dev": true,
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@@ -32756,7 +32826,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"statuses": {
@@ -32863,7 +32933,7 @@
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"dev": true
},
"fs-extra": {
@@ -35822,9 +35892,9 @@
}
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"dev": true
},
"merge-stream": {
@@ -36668,9 +36738,9 @@
}
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"dev": true
},
"path-type": {
@@ -37330,12 +37400,12 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dev": true,
"requires": {
"side-channel": "^1.0.4"
"side-channel": "^1.0.6"
}
},
"querystringify": {
@@ -38195,9 +38265,9 @@
"optional": true
},
"send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dev": true,
"requires": {
"debug": "2.6.9",
@@ -38227,7 +38297,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
}
}
@@ -38238,6 +38308,12 @@
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"dev": true
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -38278,9 +38354,9 @@
}
},
"serialize-javascript": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
@@ -38343,15 +38419,15 @@
}
},
"serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dev": true,
"requires": {
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
"send": "0.19.0"
}
},
"set-blocking": {
@@ -40061,7 +40137,7 @@
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"dev": true
},
"unzip-crx-3": {
+16 -4
View File
@@ -2,7 +2,7 @@
"name": "feishin",
"productName": "Feishin",
"description": "Feishin music server",
"version": "0.8.0",
"version": "0.12.3",
"scripts": {
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
@@ -16,6 +16,9 @@
"lint:styles": "npx stylelint **/*.tsx --fix",
"package": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
"package:pr": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win --mac --linux",
"package:pr:macos": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --mac",
"package:pr:windows": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win",
"package:pr:linux": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --linux",
"package:dev": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --dir",
"postinstall": "node --import tsx .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
"start": "node --import tsx ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
@@ -231,7 +234,7 @@
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"detect-port": "^1.3.0",
"electron": "^31.2.0",
"electron": "^33.3.1",
"electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0",
"electron-notarize": "^1.2.1",
@@ -309,6 +312,7 @@
"@tanstack/react-query-persist-client": "^4.32.1",
"@ts-rest/core": "^3.23.0",
"@xhayper/discord-rpc": "^1.0.24",
"audiomotion-analyzer": "^4.5.0",
"auto-text-size": "^0.2.3",
"axios": "^1.6.0",
"clsx": "^2.0.0",
@@ -359,8 +363,16 @@
"styled-components": "^6"
},
"devEngines": {
"node": ">=18.x",
"npm": ">=7.x"
"runtime": {
"name": "node",
"version": ">=18.x",
"onFail": "error"
},
"packageManager": {
"name": "npm",
"version": ">=7.x",
"onFail": "error"
}
},
"browserslist": [],
"electronmon": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "feishin",
"version": "0.8.0",
"version": "0.12.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "feishin",
"version": "0.8.0",
"version": "0.12.2",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "feishin",
"version": "0.8.0",
"version": "0.12.3",
"description": "",
"main": "./dist/main/main.js",
"author": {
+24
View File
@@ -19,6 +19,10 @@ import nl from './locales/nl.json';
import zhHant from './locales/zh-Hant.json';
import fa from './locales/fa.json';
import ko from './locales/ko.json';
import ta from './locales/ta.json';
import id from './locales/id.json';
import fi from './locales/fi.json';
import hu from './locales/hu.json';
const resources = {
en: { translation: en },
@@ -39,6 +43,10 @@ const resources = {
cs: { translation: cs },
nl: { translation: nl },
'nb-NO': { translation: nbNO },
ta: { translation: ta },
id: { translation: id },
fi: { translation: fi },
hu: { translation: hu },
};
export const languages = [
@@ -62,6 +70,18 @@ export const languages = [
label: 'Français',
value: 'fr',
},
{
label: 'Bahasa Indonesia',
value: 'id',
},
{
label: 'Suomeksi',
value: 'fi',
},
{
label: 'Magyar',
value: 'hu',
},
{
label: 'Italiano',
value: 'it',
@@ -106,6 +126,10 @@ export const languages = [
label: 'Svenska',
value: 'sv',
},
{
label: 'Tamil',
value: 'ta',
},
{
label: '简体中文',
value: 'zh-Hans',
+46 -15
View File
@@ -11,7 +11,7 @@
"skip_back": "přeskočit dozadu",
"favorite": "oblíbené",
"next": "další",
"shuffle": "náhodně",
"shuffle": "přehrát náhodně",
"playbackFetchNoResults": "nenalezeny žádné skladby",
"playbackFetchInProgress": "načítání skladeb…",
"addNext": "přidat další",
@@ -29,7 +29,8 @@
"addLast": "přidat poslední",
"mute": "ztlumit",
"skip_forward": "přeskočit dopředu",
"playSimilarSongs": "přehrát podobné skladby"
"playSimilarSongs": "přehrát podobné skladby",
"viewQueue": "zobrazit frontu"
},
"setting": {
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
@@ -221,14 +222,14 @@
"volumeWidth": "šířka posuvníku hlasitosti",
"volumeWidth_description": "horizontální velikost posuvníku hlasitosti",
"discordListening": "zobrazit stav jako „Poslouchá“",
"discordListening_description": "zobrazit stav jako „Poslouchá“ namísto „Hraje“. tato funkce v současné době není kompatibilní s lištou s časem",
"discordListening_description": "zobrazit stav jako „Poslouchá“ namísto „Hraje“",
"contextMenu": "nastavení kontextové nabídky (kliknutí pravým)",
"contextMenu_description": "umožňuje skrýt položky, které se zobrazí v nabídce po kliknutí pravým tlačítkem myši na položku. položky, které nejsou zaškrtnuté, se skryjí",
"customCssEnable": "povolit vlastní css",
"customCssEnable_description": "povolti psaní vlastního css.",
"customCssEnable": "povolit vlastní CSS",
"customCssEnable_description": "povolit vlastní CSS.",
"customCssNotice": "Varování: i když provádíme určitou sanitizaci (zakázáním url() a content:), může používání CSS stále představovat riziko změnami rozhraní.",
"customCss_description": "vlastní css obsah. Upozornění: vlastnosti content a vzdálené url jsou zakázané. Níže je zobrazen náhled vašeho obsahu. Další pole, která jste nenastavili, jsou přítomna z důvodu sanitizace.",
"customCss": "vlastní css",
"customCss_description": "vlastní CSS obsah. Upozornění: vlastnosti content a vzdálené url jsou zakázané. Níže je zobrazen náhled vašeho obsahu. Další pole, která jste nenastavili, jsou přítomna z důvodu sanitizace.",
"customCss": "vlastní CSS",
"webAudio": "použít webový zvuk",
"webAudio_description": "použít webový zvuk. tím povolíte pokročilé funkce jako replaygain. zakažte, pokud se objeví problémy",
"transcodeNote": "projeví se po 1 (web) - 2 (mpv) skladbách",
@@ -245,7 +246,18 @@
"playerbarOpenDrawer": "lišta přehrávače jako přepínač celé obrazovky",
"playerbarOpenDrawer_description": "umožňuje kliknutí na lištu přehrávače pro otevření celoobrazovkového přehrávače",
"artistConfiguration": "nastavení stránky umělce alba",
"artistConfiguration_description": "nastavit, které položky na stránce umělce alba budou zobrazeny a v jakém pořadí"
"artistConfiguration_description": "nastavit, které položky na stránce umělce alba budou zobrazeny a v jakém pořadí",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"trayEnabled": "zobrazit v oznamovací oblasti",
"trayEnabled_description": "zobrazit/skrýt ikonu/nabídku v oznamovací oblasti. pokud je zakázáno, vypne také minimalizaci/ukončení do oznamovací oblasti",
"translationApiProvider": "poskytovatel api překladů",
"translationApiProvider_description": "poskytovatel api pro překlady",
"translationApiKey": "klíč api překladů",
"translationApiKey_description": "klíč api pro překlady (podporuje pouze koncový bod globální služby)",
"translationTargetLanguage": "cílový jazyk překladu",
"translationTargetLanguage_description": "cílový jazyk pro překlad",
"lastfmApiKey": "klíč API {{lastfm}}",
"lastfmApiKey_description": "klíč API pro {{lastfm}}. vyžadováno pro obaly alb"
},
"action": {
"editPlaylist": "upravit $t(entity.playlist_one)",
@@ -268,7 +280,8 @@
"openIn": {
"lastfm": "Otevřít v Last.fm",
"musicbrainz": "Otevřít v MusicBrainz"
}
},
"moveToNext": "přesunout na další"
},
"common": {
"backward": "zpátky",
@@ -361,7 +374,8 @@
"share": "sdílet",
"codec": "kodek",
"trackPeak": "vrchol skladby",
"preview": "náhled"
"preview": "náhled",
"translation": "překlad"
},
"table": {
"config": {
@@ -377,7 +391,8 @@
"autoFitColumns": "automaticky přizpůsobit sloupce",
"size": "$t(common.size)",
"itemGap": "mezera mezi položkami (px)",
"itemSize": "velikost položek (px)"
"itemSize": "velikost položek (px)",
"followCurrentSong": "následovat aktuální skladbu"
},
"label": {
"releaseDate": "datum vydání",
@@ -534,11 +549,14 @@
"useImageAspectRatio": "použít poměr stran obrázku",
"lyricGap": "mezera textů",
"dynamicImageBlur": "velikost rozostření obrázku",
"dynamicIsImage": "povolit obrázek na pozadí"
"dynamicIsImage": "povolit obrázek na pozadí",
"lyricOffset": "posunutí textů (ms)"
},
"upNext": "další",
"lyrics": "texty",
"related": "související"
"related": "související",
"visualizer": "vizualizér",
"noLyrics": "nenalezeny žádné texty"
},
"appMenu": {
"selectServer": "vybrat server",
@@ -572,7 +590,9 @@
"showDetails": "získat informace",
"shareItem": "sdílet položku",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "stáhnout"
"download": "stáhnout",
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"home": {
"mostPlayed": "nejpřehrávanější",
@@ -640,6 +660,14 @@
},
"playlist": {
"reorder": "změna pořadí povolena pouze při řazení podle id"
},
"manageServers": {
"url": "URL",
"username": "uživatelské jméno",
"editServerDetailsTooltip": "upravit podrobnosti o serveru",
"removeServer": "odstranit server",
"serverDetails": "podrobnosti o serveru",
"title": "správa serverů"
}
},
"form": {
@@ -751,6 +779,9 @@
"trackWithCount_other": "{{count}} skladeb",
"play_one": "{{count}} přehrání",
"play_few": "{{count}} přehrání",
"play_other": "{{count}} přehrání"
"play_other": "{{count}} přehrání",
"song_one": "píseň",
"song_few": "písničky",
"song_other": "písní"
}
}
+73 -20
View File
@@ -66,7 +66,7 @@
"cancel": "Abbrechen",
"forceRestartRequired": "Neustarten um die Änderungen zu übernehmen... Schließe die Benachrichtigung zum Neustarten",
"setting": "Einstellungen",
"setting_one": "",
"setting_one": "Einstellung",
"setting_other": "Einstellungen",
"version": "Version",
"title": "Titel",
@@ -106,7 +106,14 @@
"preview": "Vorschau",
"reload": "Neu Laden",
"mbid": "MusicBrainz ID",
"close": "schliessen"
"close": "schliessen",
"share": "Teilen",
"translation": "Übersetzung",
"trackGain": "Track-Pegelverstärkung",
"trackPeak": "Track-Spitzenpegel",
"codec": "Codec",
"albumPeak": "Album-Spitzenpegel",
"albumGain": "Album-Pegelverstärkung"
},
"error": {
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
@@ -178,13 +185,13 @@
},
"form": {
"deletePlaylist": {
"title": "Lösche $t(entity.playlist_one)",
"title": "$t(entity.playlist_one) löschen",
"success": "$t(entity.playlist_one) erfolgreich gelöscht",
"input_confirm": "Geben Sie zur Bestätigung den Namen von $t(entity.playlist_one) ein"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"title": "Erstellen $t(entity.playlist_one)",
"title": "$t(entity.playlist_one) erstellen",
"input_public": "öffentlich",
"success": "$t(entity.playlist_one) erfolgreich erstellt",
"input_name": "$t(common.name)",
@@ -218,7 +225,8 @@
"input_optionMatchAny": "Treffer Einige"
},
"editPlaylist": {
"title": "Bearbeite $t(entity.playlist_one)"
"title": "Bearbeite $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) erfolgreich aktualisiert"
},
"lyricSearch": {
"title": "Songtext Suche",
@@ -226,7 +234,10 @@
"input_artist": "$t(entity.artist_one)"
},
"shareItem": {
"description": "Beschreibung"
"description": "Beschreibung",
"setExpiration": "Ablaufdatum setzen",
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
"allowDownloading": "Herunterladen zulassen"
}
},
"entity": {
@@ -260,7 +271,11 @@
"genreWithCount_other": "{{count}} Genres",
"trackWithCount_one": "{{count}} Track",
"trackWithCount_other": "{{count}} Tracks",
"smartPlaylist": "Smart $t(entity.playlist_one)"
"smartPlaylist": "Smart $t(entity.playlist_one)",
"play_one": "{{count}} Wiedergabe",
"play_other": "{{count}} Wiedergaben",
"song_one": "Lied",
"song_other": "Lieder"
},
"table": {
"config": {
@@ -341,11 +356,13 @@
"unsynchronized": "nicht synchronisiert",
"lyricAlignment": "Songtext-Ausrichtung",
"useImageAspectRatio": "Bildseitenverhältnis verwenden",
"lyricGap": "Songtext-Lücke"
"lyricGap": "Songtext-Lücke",
"dynamicIsImage": "Hintergrundbild aktivieren"
},
"upNext": "als nächstes",
"lyrics": "Songtexte",
"related": "Ähnliche"
"related": "Ähnliche",
"noLyrics": "Keine Liedtexte gefunden"
},
"appMenu": {
"selectServer": "Server auswählen",
@@ -368,18 +385,19 @@
},
"albumDetail": {
"moreFromArtist": "Mehr von diesem $t(entity.artist_one)",
"moreFromGeneric": "Mehr von {{item}}"
"moreFromGeneric": "Mehr von {{item}}",
"released": "erschienen"
},
"globalSearch": {
"commands": {
"serverCommands": "Serverbefehle",
"goToPage": "Gehe zur Seite",
"searchFor": "Suche nach {{query}}"
"searchFor": "Nach {{query}} suchen"
},
"title": "Befehle"
},
"contextMenu": {
"numberSelected": "{{count}} Ausgewählte",
"numberSelected": "{{count}} ausgewählt",
"addToPlaylist": "$t(action.addToPlaylist)",
"addToFavorites": "$t(action.addToFavorites)",
"setRating": "$t(action.setRating)",
@@ -394,7 +412,10 @@
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"removeFromQueue": "$t(action.removeFromQueue)"
"removeFromQueue": "$t(action.removeFromQueue)",
"playShuffled": "$t(player.shuffle)",
"download": "Download",
"playSimilarSongs": "$t(player.playSimilarSongs)"
},
"sidebar": {
"nowPlaying": "läuft gerade",
@@ -407,28 +428,59 @@
"settings": "$t(common.setting_other)",
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)"
"albumArtists": "$t(entity.albumArtist_other)",
"shared": "$t(entity.playlist_other) geteilt"
},
"setting": {
"playbackTab": "Wiedergabe",
"generalTab": "allgemein",
"generalTab": "Allgemein",
"hotkeysTab": "Kurzbefehle",
"windowTab": "Fenster"
"windowTab": "Fenster",
"advanced": "Erweitert"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
"title": "$t(entity.genre_other)",
"showTracks": "$t(entity.genre_one) $t(entity.track_other) anzeigen",
"showAlbums": "$t(entity.genre_one) $t(entity.album_other) anzeigen"
},
"trackList": {
"title": "$t(entity.track_other)"
"title": "$t(entity.track_other)",
"artistTracks": "Tracks von {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
"title": "$t(entity.album_other)",
"artistAlbums": "Alben von {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
},
"albumArtistDetail": {
"about": "Über {{artist}}",
"appearsOn": "erscheint auf",
"recentReleases": "Kürzliche Veröffentlichungen",
"viewDiscography": "Diskographie ansehen",
"viewAllTracks": "Alle $t(entity.track_other) ansehen",
"topSongsFrom": "Toplieder von {{title}}",
"viewAll": "Alles ansehen",
"topSongs": "Toplieder"
},
"manageServers": {
"title": "Servers verwalten",
"editServerDetailsTooltip": "Serverdetails editieren",
"removeServer": "Server entfernen",
"url": "URL",
"serverDetails": "Serverdetails",
"username": "Benutzername"
},
"itemDetail": {
"copyPath": "Pfad in Zwischenablage kopieren",
"copiedPath": "Pfad erfolgreich kopiert",
"openFile": "Track im Dateiexplorer anzeigen"
}
},
"player": {
@@ -460,7 +512,8 @@
"pause": "Pause",
"unfavorite": "Aus Favoriten entfernen",
"skip_forward": "Vorspulen",
"skip": "Überspringen"
"skip": "Überspringen",
"playSimilarSongs": "Ähnliche Lieder abspielen"
},
"setting": {
"audioDevice_description": "Wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer).",
+34 -4
View File
@@ -8,6 +8,7 @@
"deselectAll": "deselect all",
"editPlaylist": "edit $t(entity.playlist_one)",
"goToPage": "go to page",
"moveToNext": "move to next",
"moveToBottom": "move to bottom",
"moveToTop": "move to top",
"refresh": "$t(common.refresh)",
@@ -109,6 +110,7 @@
"trackNumber": "track",
"trackGain": "track gain",
"trackPeak": "track peak",
"translation": "translation",
"unknown": "unknown",
"version": "version",
"year": "year",
@@ -146,6 +148,8 @@
"smartPlaylist": "smart $t(entity.playlist_one)",
"track_one": "track",
"track_other": "tracks",
"song_one": "song",
"song_other": "songs",
"trackWithCount_one": "{{count}} track",
"trackWithCount_other": "{{count}} tracks"
},
@@ -314,6 +318,14 @@
"settings": "$t(common.setting_other)",
"version": "version {{version}}"
},
"manageServers": {
"title": "manage servers",
"serverDetails": "server details",
"url": "URL",
"username": "username",
"editServerDetailsTooltip": "edit server details",
"removeServer": "remove server"
},
"contextMenu": {
"addFavorite": "$t(action.addToFavorites)",
"addLast": "$t(player.addLast)",
@@ -324,6 +336,7 @@
"deletePlaylist": "$t(action.deletePlaylist)",
"deselectAll": "$t(action.deselectAll)",
"download": "download",
"moveToNext": "$t(action.moveToNext)",
"moveToBottom": "$t(action.moveToBottom)",
"moveToTop": "$t(action.moveToTop)",
"numberSelected": "{{count}} selected",
@@ -333,6 +346,7 @@
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromQueue": "$t(action.removeFromQueue)",
"setRating": "$t(action.setRating)",
"playShuffled": "$t(player.shuffle)",
"shareItem": "share item",
"showDetails": "get info"
},
@@ -343,6 +357,7 @@
"dynamicIsImage": "enable background image",
"followCurrentLyric": "follow current lyric",
"lyricAlignment": "lyric alignment",
"lyricOffset": "lyrics offset (ms)",
"lyricGap": "lyric gap",
"lyricSize": "lyric size",
"opacity": "opacity",
@@ -354,7 +369,9 @@
},
"lyrics": "lyrics",
"related": "related",
"upNext": "up next"
"upNext": "up next",
"visualizer": "visualizer",
"noLyrics": "no lyrics found"
},
"genreList": {
"showAlbums": "show $t(entity.genre_one) $t(entity.album_other)",
@@ -438,7 +455,7 @@
"repeat_off": "repeat disabled",
"repeat_one": "repeat one",
"repeat_other": "",
"shuffle": "shuffle",
"shuffle": "play shuffled",
"shuffle_off": "shuffle disabled",
"skip": "skip",
"skip_back": "skip backwards",
@@ -446,7 +463,8 @@
"stop": "stop",
"toggleFullscreenPlayer": "toggle fullscreen player",
"unfavorite": "unfavorite",
"pause": "pause"
"pause": "pause",
"viewQueue": "view queue"
},
"setting": {
"accentColor": "accent color",
@@ -492,7 +510,7 @@
"discordIdleStatus": "show rich presence idle status",
"discordIdleStatus_description": "when enabled, update status while player is idle",
"discordListening": "show status as listening",
"discordListening_description": "show status as listening instead of playing. note that this currently breaks timer bar",
"discordListening_description": "show status as listening instead of playing",
"discordRichPresence": "{{discord}} rich presence",
"discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}} ",
"discordUpdateInterval": "{{discord}} rich presence update interval",
@@ -564,6 +582,8 @@
"imageAspectRatio_description": "if enabled, cover art will be shown using their native aspect ratio. for art that is not 1:1, the remaining space will be empty",
"language": "language",
"language_description": "sets the language for the application ($t(common.restartRequired))",
"lastfmApiKey": "{{lastfm}} API key",
"lastfmApiKey_description": "the API key for {{lastfm}}. required for cover art",
"lyricFetch": "fetch lyrics from the internet",
"lyricFetch_description": "fetch lyrics from various internet sources",
"lyricFetchProvider": "providers to fetch lyrics from",
@@ -591,6 +611,7 @@
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerAlbumArtResolution": "player album art resolution",
"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",
"playerbarOpenDrawer": "playerbar fullscreen toggle",
@@ -651,6 +672,14 @@
"transcodeBitrate_description": "selects the bitrate to transcode. 0 means let the server pick",
"transcodeFormat": "format to transcode",
"transcodeFormat_description": "selects the format to transcode. leave empty to let the server decide",
"translationApiProvider": "translation api provider",
"translationApiProvider_description": "api provider for translation",
"translationApiKey": "translation api key",
"translationApiKey_description": "api key for translation (Support global service endpoint only)",
"translationTargetLanguage": "translation target language",
"translationTargetLanguage_description": "target language for translation",
"trayEnabled": "show tray",
"trayEnabled_description": "show/hide tray icon/menu. if disabled, also disables minimize/exit to tray",
"useSystemTheme": "use system theme",
"useSystemTheme_description": "follow the system-defined light or dark preference",
"volumeWheelStep": "volume wheel step",
@@ -694,6 +723,7 @@
"config": {
"general": {
"autoFitColumns": "auto fit columns",
"followCurrentSong": "follow current song",
"displayType": "display type",
"gap": "$t(common.gap)",
"itemGap": "item gap (px)",
+42 -11
View File
@@ -11,7 +11,7 @@
"skip_back": "retroceder",
"favorite": "favorito",
"next": "siguiente",
"shuffle": "mezclar",
"shuffle": "Reproducir aleatoriamente",
"playbackFetchNoResults": "ninguna canción encontrada",
"playbackFetchInProgress": "cargando canciones…",
"addNext": "añadir siguiente",
@@ -29,7 +29,8 @@
"mute": "silencio",
"skip_forward": "saltar hacia delante",
"pause": "pausa",
"playSimilarSongs": "Reproducir canciones similares"
"playSimilarSongs": "Reproducir canciones similares",
"viewQueue": "ver cola"
},
"setting": {
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
@@ -220,7 +221,7 @@
"doubleClickBehavior_description": "si es true, se pondrán en cola todas las pistas que coincidan en una búsqueda de pistas. De lo contrario, solo se pondrá en cola la pista seleccionada",
"volumeWidth": "Ancho del deslizador de volumen",
"volumeWidth_description": "La anchura del deslizador de volumen",
"discordListening_description": "Muestra el estado como escuchando en lugar de reproduciendo. Ten en cuenta que esto actualmente rompe la barra de tiempo",
"discordListening_description": "mostrar el estado como escuchando en lugar de jugando",
"discordListening": "Mostrar estado como escuchando",
"contextMenu": "Configuración del menú de contexto (clic derecho)",
"contextMenu_description": "Te permite esconder elementos que son mostrados en el menú cuando haces clic derecho en un elemento. Los elementos que no estén seleccionados serán escondidos",
@@ -245,7 +246,18 @@
"playerbarOpenDrawer": "Cambiar la barra del reproductor a pantalla completa",
"playerbarOpenDrawer_description": "Permitir hacer clic en la barra del reproductor para abrir el reproductor en pantalla completa",
"artistConfiguration": "Configuración de la página del artista del álbum",
"artistConfiguration_description": "Configurar qué elementos se muestran y en qué orden en la página del artista del álbum"
"artistConfiguration_description": "Configurar qué elementos se muestran y en qué orden en la página del artista del álbum",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"trayEnabled": "Mostrar en el área de notificación",
"trayEnabled_description": "mostrar/ocultar el icono/menú del área de notificación. si está deshabilitado, también deshabilita minimizar/salir a la bandeja",
"translationApiProvider": "Proveedor de API de traducción",
"translationApiProvider_description": "Proveedor de API para traducción",
"translationApiKey": "clave api de traducción",
"translationApiKey_description": "Clave API para la traducción (solo para el punto final del servicio global)",
"translationTargetLanguage": "idioma final de la traducción",
"translationTargetLanguage_description": "lengua de destino de la traducción",
"lastfmApiKey_description": "la clave API para {{lastfm}}. Requerida para la portada",
"lastfmApiKey": "Clave API para {{lastfm}}"
},
"action": {
"editPlaylist": "editar $t(entity.playlist_one)",
@@ -268,7 +280,8 @@
"openIn": {
"lastfm": "Abrir en Last.fm",
"musicbrainz": "Abrir en MusicBrainz"
}
},
"moveToNext": "pasar al siguiente"
},
"common": {
"backward": "hacia atrás",
@@ -361,7 +374,8 @@
"reload": "Recargar",
"share": "Compartir",
"trackGain": "Ganancia de pista",
"preview": "Vista previa"
"preview": "Vista previa",
"translation": "traducción"
},
"error": {
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
@@ -478,7 +492,9 @@
"shareItem": "Compartir elemento",
"showDetails": "Obtener información",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "descargar"
"download": "descargar",
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"home": {
"mostPlayed": "más reproducidos",
@@ -502,10 +518,13 @@
"showLyricMatch": "mostrar coincidencia de letras",
"lyricGap": "desfase de letra",
"dynamicImageBlur": "tamaño de desenfoque de imagen",
"dynamicIsImage": "habilitar imagen de fondo"
"dynamicIsImage": "habilitar imagen de fondo",
"lyricOffset": "compensación de letras (ms)"
},
"lyrics": "letras",
"related": "relacionado"
"related": "relacionado",
"visualizer": "visualizador",
"noLyrics": "sin letras"
},
"albumDetail": {
"moreFromArtist": "más de este $t(entity.artist_one)",
@@ -566,6 +585,14 @@
},
"playlist": {
"reorder": "la reordenación solo se activa al ordenar por id"
},
"manageServers": {
"removeServer": "eliminar servidor",
"title": "administrar servidores",
"serverDetails": "detalles del servidor",
"username": "nombre de usuario",
"editServerDetailsTooltip": "editar detalles del servidor",
"url": "URL"
}
},
"form": {
@@ -693,7 +720,8 @@
"size": "$t(common.size)",
"displayType": "tipo de visualización",
"itemGap": "espacio entre elementos (px)",
"itemSize": "tamaño del elemento (px)"
"itemSize": "tamaño del elemento (px)",
"followCurrentSong": "seguir la canción actual"
},
"view": {
"card": "tarjeta",
@@ -751,6 +779,9 @@
"trackWithCount_other": "{{count}} pistas",
"play_one": "Reproducir {{count}}",
"play_many": "Reproducir {{count}}",
"play_other": "Reproducir {{count}}"
"play_other": "Reproducir {{count}}",
"song_one": "canción",
"song_many": "canciones",
"song_other": "canciones"
}
}
+270 -27
View File
@@ -8,10 +8,29 @@
"skip_back": "برو عقب",
"shuffle": "شافل",
"repeat_off": "تکرار غیرفعال",
"pause": "pause",
"pause": "ایست",
"unfavorite": "حذف از موردعلاقه‌ها",
"shuffle_off": "شافل غیرفعال",
"skip_forward": "برو جلو"
"skip_forward": "برو جلو",
"queue_moveToTop": "جابجا کردن انتخاب شده به پایین",
"queue_clear": "خالی کردن صف",
"queue_remove": "حذف انتخاب شده",
"addLast": "افزودن به پایان",
"next": "پسین",
"play": "پخش",
"playbackSpeed": "تندی پخش",
"playRandom": "پخش تصادفی",
"previous": "پیشین",
"mute": "بی‌صدا کردن",
"playbackFetchCancel": "دارد طول می‌کشد... برای لفو کردن اعلان را ببندید",
"playbackFetchInProgress": "بارگذاری قطعه‌ها…",
"queue_moveToBottom": "جابجا کردن انتخاب شده به بالا",
"addNext": "افزودن به پسین",
"favorite": "مورد علاقه",
"playSimilarSongs": "پخش آهنگ‌های همگون",
"playbackFetchNoResults": "هیچ آهنگی پیدا نشد",
"viewQueue": "دیدن صف",
"muted": "بی‌صدا"
},
"action": {
"editPlaylist": "ویرایش $t(entity.playlist_one)",
@@ -30,7 +49,12 @@
"moveToBottom": "انتقال به پایین",
"setRating": "تعیین امتیاز",
"toggleSmartPlaylistEditor": "تغییر $t(entity.smartPlaylist) ویرایشگر",
"removeFromFavorites": "حذف از $t(entity.favorite_other)"
"removeFromFavorites": "حذف از $t(entity.favorite_other)",
"openIn": {
"lastfm": "باز کردن در Last.fm",
"musicbrainz": "باز کردن در MusicBranz"
},
"moveToNext": "جابجا کردن به بعدی"
},
"setting": {
"hotkey_skipBackward": "برو عقب",
@@ -72,7 +96,7 @@
"mpvExecutablePath": "مسیر اجرای MPV",
"audioDevice": "دستگاه صوتی",
"hotkey_rate2": "امتیاز ۲ ستاره",
"playButtonBehavior_description": "رفتار پیش‌فرض دکمهٔ پخش را هنگامی که آهنگی به صف اضافه می‌شود معین می‌کند",
"playButtonBehavior_description": "رفتار پیش‌فرض دکمهٔ پخش را هنگامی که آهنگی به صف افزوده می‌شود را معین می‌کند",
"exitToTray": "خروج به tray",
"hotkey_rate4": "امتیاز ۴ ستاره",
"enableRemote": "فعال کردن کنترل از راه دور سرویس‌دهنده",
@@ -93,7 +117,27 @@
"customFontPath_description": "مسیر قلم سفارشی را برای استفاده در اپلیکیشن مشخص کنید",
"gaplessAudio_optionWeak": "ضعیف (توصیه شده)",
"hotkey_playbackStop": "توقف",
"font_description": "قلم مورد استفادهٔ اپلیکیشن را معین می‌کند"
"font_description": "قلم مورد استفادهٔ اپلیکیشن را معین می‌کند",
"accentColor_description": "رنگ شاخص را برای نرم‌افزار مشخص می‌کند",
"applicationHotkeys": "کلیدهای میان‌بر نرم‌افزار",
"accentColor": "رنگ شاخص",
"albumBackgroundBlur": "اندازه‌ی مبهمی نگاره‌ی پس‌زمینه‌ی آلبوم",
"albumBackgroundBlur_description": "مقدار مبهمی‌ای که روی نگاره‌ی پس‌زمینه‌ی آلبوم اعمال می‌شود را تنظیم می‌کند",
"albumBackground": "نگاره‌ی پس‌زمینه‌ی آلبوم",
"albumBackground_description": "یک نگاره‌ی پس‌زمینه برای صفحات آلبوم دارای نگار آلبوم هستند، می‌افزاید",
"artistConfiguration": "پیکربندی صفحه‌ی هنرمند آلبوم",
"applicationHotkeys_description": "پیکربندی کلیدهای میان‌بر نرم‌افزار. برای تنظیم یک کلید میان‌بر عمومی مربع چک را فعال کنید (فقط پخش‌کننده‌ی میزکار)",
"clearCache": "پاک‌سازی کَش مرورگر",
"clearQueryCache": "پاک‌سازی کَش فیشین",
"clearCacheSuccess": "با موفقیت کَش پاک شد",
"artistConfiguration_description": "پیکربندی اینکه چه آیتمی‌هایی و در چه ترتیبی در صفحه‌ی هنرمند آلبوم نمایش داده شوند",
"buttonSize": "اندازه‌ی دکمه‌ی پخش نوار",
"contextMenu": "پیکربندی فهرست زمینه (کلیک راست)",
"buttonSize_description": "اندازه‌ی دکمه‌های پخش نوار",
"audioExclusiveMode_description": "حالت اختصاصی خروجی را فعال می‌کند. در این حالت، سامانه معمولاً قفل است و فقط mpv می‌تواند خروجی صدا دهد",
"clearQueryCache_description": "یک 'پاک‌سازی نرم' از فیشین. این فهرست‌های پخش و فراداده‌ی قطعه‌ها را تازه می‌کند و متن شعرهای ذخیره شده را بازنشانی می‌کند. پیکربندی‌ها، اعتبارنامه‌های سرویس‌دهنده و نگاره‌های کَش شده حفظ می‌شوند",
"clearCache_description": "یک 'پاک‌سازی سخت' فیشین. افزون بر پاک‌سازی کَش فیشین، کَش مرورگر هم تهی می‌شود (نگاره‌های ذخیره شده و باقی دارایی‌ها). اعتبارنامه‌ها و پیکربندی‌ها حفظ می‌شوند",
"contextMenu_description": "به شما اجازه می‌دهد که آیتم‌های نمایش داده شده در فهرستی که وقتی روی یک آیتم کلیک راست می‌کنید پدیدار می‌شود، را پنهان کنید. آیتم‌هایی که منتخب نیستند پنهان می‌شوند"
},
"common": {
"backward": "به عقب",
@@ -162,17 +206,29 @@
"menu": "منو",
"restartRequired": "راه‌اندازی دوباره لازم است",
"previousSong": "$t(entity.track_one) پیشین",
"noResultsFromQuery": "جست و جو نتیجه‌ای نداشت",
"noResultsFromQuery": "جست‌وجو نتیجه‌ای نداشت",
"quit": "خروج",
"expand": "گسترش",
"search": "جست و جو",
"search": "جست‌وجو",
"saveAs": "ذخیره کن با اسم",
"disc": "دیسک",
"yes": "بله",
"random": "تصادفی",
"size": "حجم",
"biography": "زندگی‌نامه",
"note": "توجه"
"note": "توجه",
"albumGain": "گین آلبوم",
"close": "بستن",
"albumPeak": "اوج آلبوم",
"mbid": "شناسه‌ی MusicBrainz",
"reload": "بارگذاری مجدد",
"setting": "پیکربندی",
"trackGain": "گین قطعه",
"trackPeak": "اوج قطعه",
"translation": "ترجمه",
"preview": "پیش‌نمایش",
"share": "اشتراک‌گذاری",
"codec": "کدک"
},
"error": {
"remotePortWarning": "برای تعیین port تازه، سرویس دهنده را دوباره راه‌اندازی کنید",
@@ -188,7 +244,15 @@
"serverNotSelectedError": "سرویس‌دهنده‌ای انتخاب نشده",
"remoteDisableError": "هنگام $t(common.disable) سرویس دهنده خطایی رخ داد",
"mpvRequired": "وجود MPV ضروری است",
"audioDeviceFetchError": "هنگام دسترسی به دستگاه صوتی خطایی رخ داد"
"audioDeviceFetchError": "هنگام دسترسی به دستگاه صوتی خطایی رخ داد",
"localFontAccessDenied": "دسترسی به فونت‌های محلی پذیرفته نشد",
"loginRateError": "تلاش‌های بسیار برای ورود انجام داده‌اید،‌لطفاً بعد از چند ثانیه دوباره امتحان کنید",
"networkError": "خطای شبکه رخ داد",
"badAlbum": "شما این صفحه را می‌بینید چون‌که این آهنگ قسمتی از یک آلبوم نیست. شما احتمالا این مسأله را به این خاطر می‌بینید که آهنگی در پوشه‌ی سطح بالای آهنگ‌هایتان دارید. جلی‌فین فقط قطعه‌هایی را گروه‌بندی می‌کند که در یک پوشه قرار دارند.",
"invalidServer": "سرویس‌دهنده‌ی نامعتبر",
"openError": "نمی‌توان پرونده را باز کرد",
"endpointNotImplementedError": "نقطه‌ی پایان {{endpoint}} برای {{serverType}} قرار داده نشده است",
"systemFontError": "خطایی هنگام تلاش برای دریافت فونت‌های سیستم رخ داد"
},
"filter": {
"mostPlayed": "بیشتر پخش شده",
@@ -201,13 +265,13 @@
"owner": "$t(common.owner)",
"title": "عنوان",
"rating": "امتیاز",
"search": "جست و جو",
"search": "جست‌وجو",
"bitrate": "بیت‌ریت",
"genre": "$t(entity.genre_one)",
"recentlyAdded": "به تازگی اضافه شده",
"recentlyAdded": "به تازگی افزوده شده",
"note": "توجه",
"name": "نام",
"dateAdded": "تاریخ اضافه شدن",
"dateAdded": "تاریخ افزوده شدن",
"releaseDate": "تاریخ انتشار",
"albumCount": "$t(entity.album_other) عدد",
"path": "مسیر",
@@ -230,7 +294,9 @@
"fromYear": "از سال",
"criticRating": "امتیاز منتقدین",
"album": "$t(entity.album_one)",
"trackNumber": "قطعه"
"trackNumber": "قطعه",
"communityRating": "رتبه بندی جامعه",
"isCompilation": "مخلوط است"
},
"form": {
"deletePlaylist": {
@@ -252,38 +318,61 @@
"input_url": "نشانی",
"input_password": "رمز عبور",
"input_name": "نام سرویس‌دهنده",
"success": "سرویس‌دهنده اضافه شد",
"success": "سرویس‌دهنده افزوده شد",
"input_savePassword": "ذخیرهٔ رمز",
"error_savePassword": "هنگام ذخیره رمز خطایی رخ داد"
"error_savePassword": "هنگام ذخیره رمز خطایی رخ داد",
"ignoreCors": "نادیده گرفتن هسته‌ها ($t(common.restartRequired))",
"input_legacyAuthentication": "فعال‌سازی احراز هویت سنتی",
"ignoreSsl": "نادیده گرفتن ssl ($t(common.restartRequired))"
},
"addToPlaylist": {
"success": "$t(entity.song_other) به {{numOfPlaylists}}$t(entity.playlist_other) اضافه شد",
"success": "$t(entity.song_other) به {{numOfPlaylists}}$t(entity.playlist_other) افزوده شد",
"title": "افزودن به $t(entity.playlist_one)",
"input_playlists": "$t(entity.playlist_other)"
"input_playlists": "$t(entity.playlist_other)",
"input_skipDuplicates": "پرش از تکراری‌ها"
},
"lyricSearch": {
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)"
"input_artist": "$t(entity.artist_one)",
"title": "جست‌وجو در متن شعر"
},
"editPlaylist": {
"title": "ویرایش $t(entity.playlist_one)"
"title": "ویرایش $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) با موفقیت بروزرسانی شد",
"publicJellyfinNote": "جلی‌فین به دلیلی این‌که فهرست پخش عمومی‌ست یا خصوصی را فاش نمی‌کند. اگر می‌خواهید این عمومی باقی بماند، لطفاٌ ورودی پیش‌رو را منتخب داشته باشید"
},
"queryEditor": {
"input_optionMatchAny": "همخوانی داشتن هر کدام",
"input_optionMatchAll": "همخوانی داشتن همه"
},
"shareItem": {
"expireInvalid": "انقضا باید در آینده باشد",
"description": "بازنمود",
"setExpiration": "تنظیم انقضا",
"success": "پیوند اشتراک‌گذاری در کلیپ‌بورد کپی شد (یا اینجا را کلیک کنید تا باز شود)",
"allowDownloading": "اجازه دادن بارگیری",
"createFailed": "ناکامی در ساخت پیوند اشتراک‌گذاری (آیا اشتراک‌گذاری فعال است؟)"
},
"updateServer": {
"success": "سرویس‌دهنده با موفقیت بروزرسانی شد",
"title": "بروزرسانی سرویس‌دهنده"
}
},
"entity": {
"genre_one": "ژانر",
"genre_other": "ژانر",
"genre_other": "ژانرها",
"playlistWithCount_one": "{{count}} فهرست پخش",
"playlistWithCount_other": "{{count}} فهرست پخش",
"playlist_one": "فهرست پخش",
"playlist_other": "فهرست پخش",
"playlist_other": "فهرست‌های پخش",
"artist_one": "هنرمند",
"artist_other": "هنرمند",
"artist_other": "هنرمندان",
"folderWithCount_one": "{{count}} پوشه",
"folderWithCount_other": "{{count}} پوشه",
"albumArtist_one": "هنرمند آلبوم",
"albumArtist_other": "هنرمند آلبوم",
"albumArtist_other": "هنرمندان آلبوم",
"track_one": "قطعه",
"track_other": "قطعه",
"track_other": "قطعه‌ها",
"albumArtistCount_one": "{{count}} هنرمند آلبوم",
"albumArtistCount_other": "{{count}} هنرمند آلبوم",
"albumWithCount_one": "{{count}} آلبوم",
@@ -293,13 +382,167 @@
"artistWithCount_one": "{{count}} هنرمند",
"artistWithCount_other": "{{count}} هنرمند",
"folder_one": "پوشه",
"folder_other": "پوشه",
"folder_other": "پوشه‌ها",
"smartPlaylist": "$t(entity.playlist_one) هوشمند",
"album_one": "آلبوم",
"album_other": "آلبوم",
"album_other": "آلبوم‌ها",
"genreWithCount_one": "{{count}} ژانر",
"genreWithCount_other": "{{count}} ژانر",
"trackWithCount_one": "{{count}} قطعه",
"trackWithCount_other": "{{count}} قطعه"
"trackWithCount_other": "{{count}} قطعه",
"play_one": "{{count}} بار پخش",
"play_other": "{{count}} بار پخش",
"song_one": "آهنگ",
"song_other": "آهنگ‌ها"
},
"page": {
"albumList": {
"title": "$t(entity.album_other)",
"artistAlbums": "آلبوم‌های {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
},
"appMenu": {
"settings": "$t(common.setting_other)",
"selectServer": "گزینش سرویس‌دهنده",
"expandSidebar": "گسترش نوار کناری",
"collapseSidebar": "فروکش نوار کناری",
"goBack": "بازگشت",
"openBrowserDevtools": "باز کردن ابزارهای توسعه مرورگر",
"quit": "$t(common.quit)",
"goForward": "پیش رفتن",
"manageServers": "مدیریت سرویس‌دهنده‌ها",
"version": "نسخه‌ی {{version}}"
},
"albumArtistDetail": {
"appearsOn": "مشاهده می‌شود در",
"about": "درباره‌ی {{artist}}",
"recentReleases": "عرضه‌های اخیر",
"viewAllTracks": "نمایش همه‌ی $t(entity.track_other)",
"topSongsFrom": "قطعه‌های برتر از {{title}}",
"viewAll": "نمایش همه",
"viewDiscography": "نمایش کاتالوگ",
"relatedArtists": "$t(entity.artist_other) مربوطه",
"topSongs": "قطعه‌های برتر"
},
"contextMenu": {
"addFavorite": "$t(action.addToFavorites)",
"addLast": "$t(player.addLast)",
"addNext": "$t(player.addNext)",
"addToFavorites": "$t(action.addToFavorites)",
"numberSelected": "{{count}} تا انتخاب شده",
"play": "$t(player.play)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"deselectAll": "$t(action.deselectAll)",
"download": "بارگیری",
"shareItem": "اشتراک‌گذاری آیتم",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"showDetails": "دریافت داده",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"removeFromQueue": "$t(action.removeFromQueue)",
"playShuffled": "$t(player.shuffle)",
"addToPlaylist": "$t(action.addToPlaylist)",
"createPlaylist": "$t(action.createPlaylist)",
"moveToBottom": "$t(action.moveToBottom)",
"moveToTop": "$t(action.moveToTop)",
"setRating": "$t(action.setRating)",
"deletePlaylist": "$t(action.deletePlaylist)",
"moveToNext": "$t(action.moveToNext)"
},
"fullscreenPlayer": {
"related": "موارد مربوطه",
"visualizer": "تجسم یافته",
"config": {
"dynamicImageBlur": "اندازه مبهمی نگاره",
"dynamicIsImage": "فعال‌سازی نگاره به عنوان پس‌زمینه",
"lyricOffset": "انحراف متن شعر (میلی‌ثانیه)",
"unsynchronized": "همگام نشده",
"dynamicBackground": "پس‌زمینه پویا",
"followCurrentLyric": "دنبال کردن متن شعر کنونی",
"lyricAlignment": "هم‌ترازی متن شعر",
"lyricGap": "فاصله‌ی متن شعر",
"showLyricProvider": "نمایش فراهم‌گر متن شعر",
"useImageAspectRatio": "استفاده از نسبت نمای نگاره",
"lyricSize": "اندازه‌ی متن شعر",
"opacity": "شفافی",
"showLyricMatch": "نمایش همخوانی متن شعر",
"synchronized": "همگام شده"
},
"noLyrics": "هیچ متن شعری پیدا نشد",
"lyrics": "متن شعرها",
"upNext": "در ادامه"
},
"home": {
"mostPlayed": "بیشترین پخش‌شده‌ها",
"title": "$t(common.home)",
"explore": "در کتاب‌خانه‌ی خود کاوش کنید",
"newlyAdded": "عرضه‌های تازه افزوده شده",
"recentlyPlayed": "تازه پخش شده‌ها"
},
"playlist": {
"reorder": "مرتب کردن دوباره زمانی فقط زمانی فعال شود که مرتب‌سازی بر اساس شناسه است"
},
"setting": {
"advanced": "پیشرفته",
"windowTab": "پنجره",
"generalTab": "همگانی",
"hotkeysTab": "کلیدهای میان‌بر",
"playbackTab": "پخش"
},
"sidebar": {
"genres": "$t(entity.genre_other)",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"albumArtists": "$t(entity.albumArtist_other)",
"albums": "$t(entity.album_other)",
"folders": "$t(entity.folder_other)",
"artists": "$t(entity.artist_other)",
"home": "$t(common.home)",
"nowPlaying": "پخش کنونی",
"tracks": "$t(entity.track_other)",
"settings": "$t(common.setting_other)",
"shared": "$t(entity.playlist_other) اشتراک‌گذاری شده"
},
"albumDetail": {
"moreFromArtist": "موارد بیشتر از این $t(entity.artist_one)",
"moreFromGeneric": "موارد بیشتر از {{item}}",
"released": "عرضه شده"
},
"manageServers": {
"title": "مدیریت سرویس‌دهنده‌ها",
"url": "آدرس",
"serverDetails": "ریزگان سرویس‌دهنده",
"removeServer": "حذف سرویس‌دهنده",
"username": "نام کاربری",
"editServerDetailsTooltip": "ویرایش ریزگان سرویس‌دهنده"
},
"genreList": {
"showAlbums": "نمایش $t(entity.genre_one) $t(entity.album_other)",
"title": "$t(entity.genre_other)",
"showTracks": "نمایش $t(entity.genre_one) $t(entity.track_other)"
},
"globalSearch": {
"commands": {
"goToPage": "رفتن به صفحه‌ی",
"searchFor": "جست‌و‌جو برای {{query}}",
"serverCommands": "فرمان‌های سرویس‌دهنده"
},
"title": "فرمان‌ها"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"trackList": {
"title": "$t(entity.track_other)",
"artistTracks": "قطعه‌های {{artist}}",
"genreTracks": "$t(entity.track_other) \"{{genre}}\""
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"itemDetail": {
"copyPath": "کپی کردن مسیر در کلیپ‌بورد",
"copiedPath": "مسیر با موفقیت کپی شد",
"openFile": "نمایش قطعه در مدیر پرونده"
}
}
}
+364 -10
View File
@@ -38,7 +38,7 @@
"name": "nimi",
"no": "ei",
"none": "ei mitään",
"noResultsFromQuery": "kysely palautti ei tuloksia",
"noResultsFromQuery": "kysely ei tuottanut tuloksia",
"note": "huomautus",
"ok": "ok",
"owner": "omistaja",
@@ -83,13 +83,18 @@
"rating": "arvostelu",
"refresh": "virkistä",
"reset": "nollaa",
"playerMustBePaused": "soitin täytyy olla pysäytetty"
"playerMustBePaused": "soittimen täytyy olla pysäytetty",
"translation": "käännös",
"albumGain": "albumin vahvistus (gain)",
"albumPeak": "albumin huippu (peak)",
"trackGain": "raidan vahvistus (gain)",
"trackPeak": "kappaleen huippu (peak)"
},
"entity": {
"album_one": "albumi",
"album_other": "albumia",
"album_other": "albumit",
"albumArtist_one": "albumi artisti",
"albumArtist_other": "albumi artistia",
"albumArtist_other": "albumi artistit",
"artistWithCount_one": "{{count}} artisti",
"artistWithCount_other": "{{count}} artistia",
"playlist_one": "soittolista",
@@ -116,12 +121,16 @@
"track_one": "raita",
"track_other": "raitaa",
"trackWithCount_one": "{{count}} raita",
"trackWithCount_other": "{{count}} raitaa"
"trackWithCount_other": "{{count}} raitaa",
"play_one": "{{count}} toista",
"play_other": "{{count}} toistaa",
"song_one": "kappale",
"song_other": "kappaleet"
},
"action": {
"clearQueue": "tyhjennä jono",
"createPlaylist": "luo $t(entity.playlist_one)",
"deselectAll": "poista valinta kaikista",
"deselectAll": "poista kaikkien valinta",
"editPlaylist": "muokkaa $t(entity.playlist_one)",
"removeFromQueue": "poista jonosta",
"viewPlaylists": "katsele $t(entity.playlist_other)",
@@ -130,15 +139,360 @@
"musicbrainz": "Avaa MusicBrainz:ssä"
},
"goToPage": "mene sivulle",
"moveToBottom": "siirry pohjalle",
"moveToBottom": "siirry alas",
"moveToTop": "siirry ylös",
"addToFavorites": "lisää $t(entity.favorite_other)",
"addToPlaylist": "lisää $t(entity.playlist_one)",
"addToFavorites": "lisää kohteeseen $t(entity.favorite_other)",
"addToPlaylist": "lisää kohteeseen $t(entity.playlist_one)",
"refresh": "$t(common.refresh)",
"removeFromFavorites": "poista kohteesta $t(entity.favorite_other)",
"toggleSmartPlaylistEditor": "kytke $t(entity.smartPlaylist) editori",
"deletePlaylist": "poista $t(entity.playlist_one)",
"removeFromPlaylist": "poista kohteesta $t(entity.playlist_one)",
"setRating": "aseta arvostelu"
"setRating": "aseta arvostelu",
"moveToNext": "siirry seuraavaan"
},
"error": {
"remoteEnableError": "virhe tapahtui yrittäessä $t(common.enable) etäpalvelinta",
"remotePortError": "virhe tapahtui etäpalvelimen porttia määrittäessä",
"serverNotSelectedError": "palvelinta ei ole valittu",
"remoteDisableError": "virhe tapahtui yrittäessä $t(common.disable) etäpalvelinta",
"serverRequired": "palvelin vaadittu",
"systemFontError": "virhe tapahtui yrittäessä hakea järjestelmän fontteja",
"sessionExpiredError": "istuntosi on vanhentunut",
"genericError": "tapahtui virhe",
"invalidServer": "virheellinen palvelin",
"audioDeviceFetchError": "äänentoistolaitteita haettaessa tapahtui virhe",
"authenticationFailed": "tunnistautuminen epäonnistui",
"badAlbum": "näet tämän sivun koska tämä kappale ei ole osa albumia. Näet tämän todennäköisesti jos kappaleesi on päämusiikkikansiosi juuressa. jellyfin ryhmittää kappaleet vain jos ne ovat kansiossa.",
"apiRouteError": "pyynnön reititys epäonnistui",
"credentialsRequired": "käyttäjätunnuksia vaaditaan",
"loginRateError": "liian monta kirjautumisyritystä, kokeile muutaman sekuntin päästä uudestaan",
"mpvRequired": "MPV vaadittu",
"networkError": "yhteysvirhe",
"openError": "tiedostoa ei voitu avata",
"localFontAccessDenied": "paikallisiin fontteihin pääsy on estetty",
"playbackError": "mediaa toistaessa tapahtui virhe",
"remotePortWarning": "käynnistä palvelin uudestaan ottaaksesi uuden portin käyttöön",
"endpointNotImplementedError": "endpoint {{endpoint}} ei ole toteutettu {{serverType}} varten"
},
"filter": {
"album": "$t(entity.album_one)",
"albumArtist": "$t(entity.albumArtist_one)",
"artist": "$t(entity.artist_one)",
"biography": "biografia",
"bitrate": "bittinopeus",
"bpm": "lyöntiä minuutissa (bpm)",
"channels": "$t(common.channel_other)",
"title": "otsikko",
"playCount": "toistomäärä",
"dateAdded": "lisätty päivänä",
"lastPlayed": "viimeksi toistettu",
"mostPlayed": "eniten toistettu",
"isRecentlyPlayed": "on äskettäin toistettu",
"rating": "arvostelu",
"recentlyAdded": "äskettäin lisätty",
"recentlyUpdated": "äskettäin päivitetty",
"releaseDate": "julkaisupäivä",
"toYear": "vuoteen",
"releaseYear": "julkaisuvuosi",
"search": "haku",
"trackNumber": "raita",
"isPublic": "on julkinen",
"genre": "$t(entity.genre_one)",
"favorited": "suosikeissa",
"fromYear": "vuodelta",
"isRated": "on arvosteltu",
"recentlyPlayed": "äskettäin toistetut",
"albumCount": "$t(entity.album_other) määrä",
"disc": "levy",
"duration": "kesto",
"id": "tunnus",
"random": "satunnainen",
"isFavorited": "on suosikeissa",
"isCompilation": "on osa kokoelmaa",
"comment": "kommentti",
"communityRating": "yhteisön arvostelu",
"criticRating": "kriitikon arvostelu",
"name": "nimi",
"note": "muistiinpano",
"owner": "$t(common.owner)",
"path": "polku",
"songCount": "kappalemäärä"
},
"form": {
"addServer": {
"input_legacyAuthentication": "käytä vanhaa kirjautumista",
"ignoreCors": "ohita CORS ($t(common.restartRequired))",
"input_name": "palvelimen nimi",
"ignoreSsl": "ohita SSL ($t(common.restartRequired))",
"input_savePassword": "tallenna salasana",
"input_url": "url-osoite",
"title": "lisää palvelin",
"error_savePassword": "salasanaa tallentaessa tapahtui virhe",
"input_password": "salasana",
"input_username": "käyttäjänimi",
"success": "palvelin lisätty onnistuneesti"
},
"createPlaylist": {
"input_public": "julkinen",
"input_name": "$t(common.name)",
"input_owner": "$t(common.owner)",
"success": "$t(entity.playlist_one) luotu onnistuneesti",
"title": "luo $t(entity.playlist_one)",
"input_description": "$t(common.description)"
},
"addToPlaylist": {
"input_skipDuplicates": "ohita kaksoiskappaleet",
"success": "lisätty $t(entity.trackWithCount, {\"count\": {{message}} }) soittolistalle $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "lisää soittolistalle $t(entity.playlist_one)",
"input_playlists": "$t(entity.playlist_other)"
},
"updateServer": {
"success": "palvelin on päivitetty onnistuneesti",
"title": "päivitä palvelin"
},
"deletePlaylist": {
"success": "$t(entity.playlist_one) poistettu onnistuneesti",
"title": "poista $t(entity.playlist_one)",
"input_confirm": "kirjoita soittolistan $t(entity.playlist_one) nimi vahvistaaksesi"
},
"editPlaylist": {
"success": "$t(entity.playlist_one) päivitetty onnistuneesti",
"title": "muokkaa $t(entity.playlist_one)",
"publicJellyfinNote": "Jellyfin ei jostain syystä kerro onko soittolista julkinen vai ei. Jos haluat sen pysyvän julkisena, pidä seuraava valinta valittuna"
},
"lyricSearch": {
"input_artist": "$t(entity.artist_one)",
"input_name": "$t(common.name)",
"title": "sanojen haku"
},
"shareItem": {
"createFailed": "jaon luonti epäonnistui (onko jako päällä?)",
"allowDownloading": "salli lataus",
"description": "kuvaus",
"setExpiration": "aseta vanheneminen",
"success": "jakolinkki kopioitu leikepöydälle (tai klikkaa tästä avataksesi)",
"expireInvalid": "vanhetumisen pitää olla tulevaisuudessa"
},
"queryEditor": {
"input_optionMatchAny": "sovita joku",
"input_optionMatchAll": "sovita kaikki"
}
},
"setting": {
"clearCacheSuccess": "välimuisti on tyhjennetty onnistuneesti",
"artistConfiguration_description": "valise näytettävät asiat ja niiden järjestys albumin artistin sivulla",
"audioDevice": "äänilaite",
"clearQueryCache_description": "feishinin 'pehmeä tyhjennys'. tämä tyhjentää soittolistat, raitojen metadatat ja tallennetut sanoitukset. asetukset, palvelimien käyttäjätunnukset ja välimuistissa olevat kuvat säilyvät",
"crossfadeDuration": "ristihäivytyksen kesto",
"audioPlayer_description": "valitse toistossa käytettävä soitin",
"buttonSize": "soittimen palkin nappien koko",
"buttonSize_description": "soittimen palkin nappien koko",
"clearCache": "tyhjennä selaimen välimuisti",
"clearQueryCache": "tyhjennä feishinin välimuisti",
"crossfadeDuration_description": "aseta ristihäivytystehosteen kesto",
"applicationHotkeys_description": "aseta sovelluksen pikanäppäimet. vaihda valintaruutua asettaaksesi valinta globaaliksi pikanäppäimeksi (vain työpöydällä)",
"crossfadeStyle": "ristihäivytyksen tyyli",
"crossfadeStyle_description": "valitse soittimessa käytettävän ristihäivytyksen tyyli",
"contextMenu_description": "mahdollistaa sinun piilottaa asiat, jotka näytetään valikossa klikatessasi objektia hiiren väärällä painikkella. poistetut valinnat piilotetaan",
"customCssEnable_description": "mahdollista oman css:n kirjoittaminen.",
"accentColor": "korostusväri",
"customCssEnable": "käytä omaa css:ää",
"albumBackgroundBlur_description": "säätää albumin taustakuvan sumennuksen määrää",
"audioExclusiveMode_description": "käytä yksinomaista ulostulotilaa. Tässä tilassa järjestelmä on yleensä lukittuna ja vain mpv voi tuottaa ääntä",
"albumBackgroundBlur": "albumin taustakuvan sumennuksen koko",
"clearCache_description": "feishinin 'kova tyhjennys'. feishinin välimuistin lisäksi tyhjennä selaimen välimuisti (tallennetut kuvat ja muut kohteet). palvelimien käyttäjättunnukset ja asetukset säilyvät",
"audioExclusiveMode": "äänen yksinomainen tila",
"audioPlayer": "soitin",
"contextMenu": "kontekstivalikon (hiiren väärä näppäin) asetukset",
"accentColor_description": "aseta sovelluksen korostusväri",
"albumBackground_description": "lisää taustakuva albumin sivuille, jotka sisältävät albumin kuvitusta",
"artistConfiguration": "albumin artistin sivun hallinta",
"audioDevice_description": "valitse toistossa käytettävä äänilaite (vain verkkosoittimessa)",
"applicationHotkeys": "sovelluksen pikanäppäimet",
"albumBackground": "albumin taustakuva"
},
"page": {
"itemDetail": {
"copiedPath": "polku on kopioitu onnistuneesti",
"copyPath": "kopioi reitti leikepöytälle",
"openFile": "näytä kappale tiedostonhallinnassa"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"albumDetail": {
"moreFromArtist": "siirrä kohteesta $t(entity.artist_one)",
"moreFromGeneric": "listää kohteesta {{item}}",
"released": "julkaistu"
},
"albumList": {
"artistAlbums": "artistin {{artist}} albumit",
"genreAlbums": "\"{{genre}}\"$t(entity.album_other)",
"title": "$t(entity.album_other)"
},
"appMenu": {
"goBack": "mene takaisin",
"openBrowserDevtools": "avaa selaimen kehitystyökalut",
"quit": "$t(common.quit)",
"selectServer": "valitse palvelin",
"settings": "$t(common.setting_other)",
"expandSidebar": "laajenna sivupalkki",
"goForward": "mene eteenpäin",
"manageServers": "hallitse palvelimia",
"collapseSidebar": "kutista sivupalkki",
"version": "versio {{version}}"
},
"contextMenu": {
"playSimilarSongs": "$t(player.playSimilarSongs)",
"addNext": "$t(player.addNext)",
"addToFavorites": "$t(action.addToFavorites)",
"addToPlaylist": "$t(action.addToPlaylist)",
"createPlaylist": "$t(action.createPlaylist)",
"deletePlaylist": "$t(action.deletePlaylist)",
"deselectAll": "$t(action.deselectAll)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"setRating": "$t(action.setRating)",
"playShuffled": "$t(player.shuffle)",
"numberSelected": "{{count}} valittuna",
"play": "$t(player.play)",
"download": "lataa",
"moveToBottom": "$t(action.moveToBottom)",
"moveToTop": "$t(action.moveToTop)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"shareItem": "jaa kohde",
"showDetails": "lisätietoa",
"addFavorite": "$t(action.addToFavorites)",
"addLast": "$t(player.addLast)",
"moveToNext": "$t(action.moveToNext)",
"removeFromQueue": "$t(action.removeFromQueue)"
},
"sidebar": {
"albumArtists": "$t(entity.albumArtist_other)",
"albums": "$t(entity.album_other)",
"settings": "$t(common.setting_other)",
"shared": "$t(entity.playlist_other) jaettu",
"tracks": "$t(entity.track_other)",
"artists": "$t(entity.artist_other)",
"folders": "$t(entity.folder_other)",
"genres": "$t(entity.genre_other)",
"home": "$t(common.home)",
"nowPlaying": "nyt soi",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)"
},
"setting": {
"generalTab": "yleinen",
"windowTab": "ikkuna",
"hotkeysTab": "pikanäppäimet",
"playbackTab": "toisto",
"advanced": "edistyneet"
},
"fullscreenPlayer": {
"upNext": "seuraavaksi",
"visualizer": "visualisaattori",
"noLyrics": "sanoja ei löytynyt",
"config": {
"showLyricMatch": "näytä sanojen yhteneväisyys",
"showLyricProvider": "näytä sanojen tarjoaja",
"lyricGap": "sanojen rako",
"synchronized": "synkronoitu",
"lyricSize": "sanojen koko",
"opacity": "läpinäkyvyys",
"unsynchronized": "synkronoimaton",
"useImageAspectRatio": "käytä kuvan kuvasuhdetta",
"dynamicBackground": "liikkuva tausta",
"dynamicImageBlur": "kuvan sumennuksen koko",
"dynamicIsImage": "käytä taustakuvaa",
"lyricOffset": "sanojen kompensointi (ms)",
"followCurrentLyric": "seuraa nykyisiä sanoja",
"lyricAlignment": "sanojen kohdistus"
},
"lyrics": "sanat",
"related": "liittyvät"
},
"genreList": {
"showAlbums": "näytä $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "näytä $t(entity.genre_one) $t(entity.track_other)",
"title": "$t(entity.genre_other)"
},
"globalSearch": {
"commands": {
"searchFor": "hae {{query}}",
"serverCommands": "palvelimen komennot",
"goToPage": "mene sivulle"
},
"title": "komennot"
},
"home": {
"explore": "tutki kirjastotasi",
"recentlyPlayed": "hiljattain soitetut",
"title": "$t(common.home)",
"mostPlayed": "eniten soitetut",
"newlyAdded": "hiljattain lisätyt julkaisut"
},
"albumArtistDetail": {
"about": "{{artist}}{sta/stä",
"viewDiscography": "katsele diskografiaa",
"relatedArtists": "liittyvät $t(entity.artist_other)",
"appearsOn": "esiintyy",
"topSongs": "parhaat kappaleet",
"topSongsFrom": "parhaat kappaleet albumilta {{title}}",
"recentReleases": "hiljattaiset julkaisut",
"viewAll": "katsele kaikkia",
"viewAllTracks": "katsele kaikkia $t(entity.track_other)"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"manageServers": {
"title": "hallitse palvelimia",
"serverDetails": "palvelimen lisätiedot",
"url": "URL",
"username": "käyttäjänimi",
"editServerDetailsTooltip": "muokkaa palvelimen lisätietoja",
"removeServer": "etäpalvelin"
},
"playlist": {
"reorder": "uudelleenjärjestely mahdollista vain, kun järjestellään id:n mukaan"
},
"trackList": {
"artistTracks": "artistin {{artist}} kappaleet",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
"title": "$t(entity.track_other)"
}
},
"player": {
"addLast": "lisää viimeinen",
"addNext": "lisää seuraava",
"favorite": "suosikki",
"queue_moveToTop": "siirrä valittu alas",
"queue_remove": "poista valittu",
"repeat": "kertaus",
"previous": "edellinen",
"queue_clear": "tyhjennä jono",
"skip": "ohita",
"skip_forward": "ohita eteenpäin",
"stop": "pysäytä",
"skip_back": "ohita taaksepäin",
"unfavorite": "poista suosikeista",
"playbackFetchNoResults": "kappaleita ei löytynyt",
"queue_moveToBottom": "siittä valittu ylös",
"pause": "tauota",
"playbackSpeed": "toistonopeus",
"repeat_all": "kertaa kaikki",
"playbackFetchCancel": "tämä vie aikaa... sulje ilmoitus peruaksesi",
"mute": "mykistä",
"shuffle": "soita sekoitettuna",
"next": "seuraava",
"play": "toista",
"playbackFetchInProgress": "ladataan kappaleita…",
"viewQueue": "katsele jonoa",
"muted": "mykistetty",
"playRandom": "toista satunnainen",
"playSimilarSongs": "toista samanlaisia kappaleita",
"repeat_off": "kertaus pois päältä",
"shuffle_off": "sekoitus pois päältä",
"toggleFullscreenPlayer": "vaihda kokoruudun soittimeen"
}
}
+98 -26
View File
@@ -11,7 +11,7 @@
"skip_back": "reculer",
"favorite": "favori",
"next": "suivant",
"shuffle": "aléatoire",
"shuffle": "lecture aléatoire",
"playbackFetchNoResults": "aucune chansons trouvées",
"playbackFetchInProgress": "chargement des chansons…",
"addNext": "ajouter ensuite",
@@ -29,13 +29,14 @@
"skip_forward": "avancer",
"pause": "pause",
"unfavorite": "retirer des favoris",
"playSimilarSongs": "jouer des chansons similaires"
"playSimilarSongs": "jouer des chansons similaires",
"viewQueue": "voir la file d'attente"
},
"action": {
"editPlaylist": "éditer $t(entity.playlist_one)",
"goToPage": "aller à la page",
"moveToTop": "déplacer en haut",
"clearQueue": "effacer la liste de lecture",
"clearQueue": "vider la file d'attente",
"addToFavorites": "ajouter aux $t(entity.favorite_other)",
"addToPlaylist": "ajouter à $t(entity.playlist_one)",
"createPlaylist": "créer $t(entity.playlist_one)",
@@ -52,7 +53,8 @@
"openIn": {
"lastfm": "Ouvrir dans Last.fm",
"musicbrainz": "Ouvrir dans MusicBrainz"
}
},
"moveToNext": "passer au suivant"
},
"common": {
"backward": "en arrière",
@@ -65,7 +67,7 @@
"edit": "éditer",
"favorite": "favoris",
"left": "gauche",
"save": "sauvegarder",
"save": "enregistrer",
"right": "droite",
"currentSong": "$t(entity.track_one) actuelle",
"collapse": "réduire",
@@ -92,7 +94,7 @@
"no": "non",
"owner": "propriétaire",
"enable": "activer",
"clear": "effacer",
"clear": "vider",
"forward": "avancer",
"delete": "supprimer",
"cancel": "annuler",
@@ -106,7 +108,7 @@
"filters": "filtres",
"create": "créer",
"bitrate": "bitrate",
"saveAndReplace": "sauvegarder et remplacer",
"saveAndReplace": "enregistrer et remplacer",
"action_one": "action",
"action_many": "actions",
"action_other": "actions",
@@ -124,12 +126,12 @@
"none": "aucun",
"menu": "menu",
"restartRequired": "redémarrage requis",
"previousSong": "précédant $t(entity.track_one)",
"previousSong": "$t(entity.track_one) précédente",
"noResultsFromQuery": "la requête n'a retourné aucun résultat",
"quit": "quitter",
"expand": "étendre",
"search": "recherche",
"saveAs": "sauvegarder en tant que",
"saveAs": "enregistrer en tant que",
"disc": "disque",
"yes": "oui",
"random": "aléatoire",
@@ -145,35 +147,36 @@
"reload": "recharger",
"trackGain": "gain de la piste",
"trackPeak": "crête de la piste",
"codec": "codec"
"codec": "codec",
"translation": "traduction"
},
"error": {
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
"systemFontError": "une erreur sest produite lors de la tentative dobtenir les polices système",
"playbackError": "une erreur s'est produite lors de la tentative de lecture du média",
"endpointNotImplementedError": "endpoint {{endpoint}} n'est pas implémenté pour {{serverType}}",
"endpointNotImplementedError": "l'endpoint {{endpoint}} n'est pas implémenté pour {{serverType}}",
"remotePortError": "une erreur s'est produite lors de la tentative de définir le port du serveur distant",
"serverRequired": "serveur requis",
"authenticationFailed": "l'authentification à échoué",
"authenticationFailed": "l'authentification a échoué",
"apiRouteError": "incapable dacheminer la demande",
"genericError": "une erreur s'est produite",
"credentialsRequired": "identifiants requis",
"sessionExpiredError": "votre session a expiré",
"remoteEnableError": "une erreur s'est produite lors de la tentative de $t(common.enable) le serveur distant",
"localFontAccessDenied": "accès refusé aux polices locales",
"serverNotSelectedError": "aucun serveur sélectionner",
"serverNotSelectedError": "aucun serveur sélectionné",
"remoteDisableError": "une erreur s'est produite lors de la tentative de $t(common.disable) le serveur distant",
"mpvRequired": "MPV requis",
"audioDeviceFetchError": "une erreur sest produite lors de la tentative dobtenir les périphériques audio",
"invalidServer": "serveur invalide",
"loginRateError": "trop de tentative de connexion, merci d'essayer dans quelque secondes",
"loginRateError": "trop de tentative de connexion, merci de réessayer dans quelques secondes",
"openError": "impossible d'ouvrir le fichier",
"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)\"."
},
"filter": {
"mostPlayed": "plus joués",
"playCount": "nombre d'écoutes",
"playCount": "nombre d'écoute",
"isCompilation": "est une compilation",
"recentlyPlayed": "récemment joué",
"isRated": "est noté",
@@ -190,7 +193,7 @@
"path": "chemin",
"favorited": "favoris",
"isRecentlyPlayed": "est récemment joué",
"isFavorited": "est favoris",
"isFavorited": "est favori",
"bpm": "bpm",
"releaseYear": "année de sortie",
"disc": "disque",
@@ -198,7 +201,7 @@
"songCount": "nombre de chansons",
"duration": "durée",
"random": "aléatoire",
"lastPlayed": "dernière joué",
"lastPlayed": "dernier joué",
"toYear": "à l'année",
"fromYear": "depuis l'année",
"criticRating": "note des critiques",
@@ -244,11 +247,14 @@
"lyricSize": "Taille des paroles",
"lyricGap": "espacement des lettres",
"dynamicIsImage": "activer l'image d'arrière-plan",
"dynamicImageBlur": "intensité de flou sur image d'arrière-plan"
"dynamicImageBlur": "intensité de flou sur image d'arrière-plan",
"lyricOffset": "paroles décalées (ms)"
},
"upNext": "à suivre",
"lyrics": "paroles",
"related": "similaire"
"related": "similaire",
"visualizer": "visualisateur",
"noLyrics": "aucune parole trouvée"
},
"appMenu": {
"selectServer": "sélectionner le serveur",
@@ -271,13 +277,15 @@
},
"albumDetail": {
"moreFromArtist": "plus de $t(entity.artist_one)",
"moreFromGeneric": "plus de {{item}}"
"moreFromGeneric": "plus de {{item}}",
"released": "publié"
},
"setting": {
"generalTab": "général",
"hotkeysTab": "raccourcis",
"windowTab": "fenêtre",
"playbackTab": "lecteur"
"playbackTab": "lecteur",
"advanced": "avancé"
},
"globalSearch": {
"commands": {
@@ -306,7 +314,10 @@
"removeFromQueue": "$t(action.removeFromQueue)",
"shareItem": "partager un élément",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"showDetails": "obtenir des informations"
"showDetails": "obtenir des informations",
"download": "télécharger",
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
@@ -344,6 +355,17 @@
"copyPath": "copier le chemin dans le presse-papiers",
"openFile": "afficher la piste dans le gestionnaire de fichiers",
"copiedPath": "chemin copié avec succès"
},
"playlist": {
"reorder": "le tri n'est possible que lorsque l'on trie par identifiant"
},
"manageServers": {
"serverDetails": "détails du serveur",
"removeServer": "supprimer le serveur",
"url": "URL du serveur",
"title": "gérer les serveurs",
"username": "nom d'utilisateur",
"editServerDetailsTooltip": "modifier les détails du serveur"
}
},
"setting": {
@@ -530,7 +552,48 @@
"playerAlbumArtResolution_description": "la 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"
"genreBehavior_description": "détermine si cliquer sur un genre ouvre par défaut la liste des pistes ou des albums",
"transcode": "activer le transcodage",
"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",
"volumeWidth": "largeur de la barre de volume",
"volumeWidth_description": "la largeur de la barre de volume",
"customCssEnable": "activer le css personnalisé",
"customCssEnable_description": "permet d'écrire du css personnalisé.",
"customCssNotice": "Attention: bien qu'il y ait un certain assainissement (blocage de url() et de content:), l'utilisation de CSS personnalisé peut toujours présenter des risques en modifiant l'interface.",
"customCss": "css personnalisé",
"webAudio": "utiliser l'audio web",
"transcodeBitrate": "débit binaire du transcodage",
"transcodeFormat": "format de transcodage",
"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 contexte (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",
"albumBackground_description": "ajoute une image d'arrière-plan pour les pages de l'album contenant les illustrations de l'album",
"albumBackgroundBlur_description": "ajuste le niveau de flou appliqué à l'image d'arrière-plan de l'album",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerbarOpenDrawer": "basculement plein écran de la barre de lecteur",
"playerbarOpenDrawer_description": "permet de cliquer sur la barre du lecteur pour ouvrir le lecteur plein écran",
"translationApiProvider": "fournisseur d'api de traduction",
"discordListening": "afficher le statut d'écoute",
"discordListening_description": "afficher le statut comme étant en écoute au lieu de lecture",
"translationApiKey_description": "clé api à utiliser pour traduire les paroles (ne prend en charge que les points de terminaison de service globaux)",
"translationTargetLanguage": "traduction langue cible",
"trayEnabled": "montrer le plateau",
"translationApiProvider_description": "le fournisseur d'api à utiliser pour la traduction des paroles",
"customCss_description": "contenu css personnalisé. Remarque : le contenu et les URL distantes sont des propriétés non autorisées. Un aperçu de votre contenu est affiché ci-dessous. Des champs supplémentaires que vous n'avez pas définis sont présents en raison de la vérification.",
"translationApiKey": "clé api de traduction",
"translationTargetLanguage_description": "langue cible pour la traduction des paroles",
"transcodeNote": "prend effet après 1 (web) - 2 (mpv) chansons",
"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": "taille du flou de l'image d'arrière-plan de l'album",
"lastfmApiKey": "clé API {{lastfm}}",
"lastfmApiKey_description": "la clé API pour {{lastfm}} est requise pour la pochette d'album"
},
"form": {
"deletePlaylist": {
@@ -574,7 +637,9 @@
"input_optionMatchAny": "correspondre à n'importe quel"
},
"editPlaylist": {
"title": "modifier $t(entity.playlist_one)"
"title": "modifier $t(entity.playlist_one)",
"publicJellyfinNote": "Jellyfin n'indique pas si une playlist est publique ou non. Si vous souhaitez que cette playlist reste publique, veuillez sélectionner l'entrée suivante",
"success": "$t(entity.playlist_one) mis à jour avec succès"
},
"lyricSearch": {
"title": "rechercher parole",
@@ -636,7 +701,13 @@
"genreWithCount_other": "{{count}} genres",
"trackWithCount_one": "{{count}} piste",
"trackWithCount_many": "{{count}} pistes",
"trackWithCount_other": "{{count}} pistes"
"trackWithCount_other": "{{count}} pistes",
"play_one": "{{count}} écouter",
"play_many": "{{count}} écoute",
"play_other": "{{count}} écoute",
"song_one": "chanson",
"song_many": "chansons",
"song_other": "chansons"
},
"table": {
"config": {
@@ -647,7 +718,8 @@
"gap": "$t(common.gap)",
"size": "$t(common.size)",
"itemGap": "écart entre les éléments (en pixel)",
"itemSize": "taille des élements (en pixel)"
"itemSize": "taille des élements (en pixel)",
"followCurrentSong": "suivre la chanson actuelle"
},
"view": {
"table": "liste",
+254
View File
@@ -0,0 +1,254 @@
{
"action": {
"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",
"viewPlaylists": "$t(entity.playlist_other) megtekintése",
"openIn": {
"lastfm": "Megnyitás Last.fm-ben",
"musicbrainz": "Megnyitás MusicBrainz-ben"
},
"clearQueue": "műsorlista kiürítése",
"createPlaylist": "$t(entity.playlist_one) létrehozása",
"deselectAll": "kijelölések törlése",
"editPlaylist": "$t(entity.playlist_one) szerkesztése",
"goToPage": "oldal meglátogatása",
"moveToBottom": "ugrás az utolsóhoz",
"moveToTop": "ugrás az elsőhöz",
"removeFromPlaylist": "eltávolítás innen: $t(entity.playlist_one)",
"removeFromQueue": "eltávolítás a műsorlistáról",
"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)"
},
"common": {
"collapse": "összecsukás",
"currentSong": "jelenlegi: $t(entity.track_one)",
"no": "nem",
"close": "bezárás",
"confirm": "rendben",
"create": "létrehozás",
"codec": "kodek",
"delete": "törlés",
"description": "leírás",
"comingSoon": "hamarosan…",
"decrease": "csökkentés",
"enable": "engedélyes",
"disable": "letiltás",
"disc": "lemez",
"modified": "módosult",
"forceRestartRequired": "a módosítások alkalmazásához újra kell indulnunk... zárd be az értesítést az újraindításhoz",
"home": "főoldal",
"name": "név",
"action_one": "művelet",
"action_other": "műveletek",
"add": "hozzáadás",
"albumGain": "album erősség",
"albumPeak": "album csúcs",
"areYouSure": "biztos vagy benne?",
"ascending": "növekvő",
"backward": "visszafelé",
"biography": "biográfia",
"bitrate": "bitráta",
"cancel": "mégse",
"center": "közép",
"channel_one": "csatorna",
"channel_other": "csatornák",
"clear": "törlés",
"configure": "konfigurálás",
"descending": "csökkenő",
"dismiss": "figyelmen kívül hagyás",
"duration": "hossz",
"edit": "szerkesztés",
"expand": "megnyitás",
"favorite": "kedvenc",
"filter_one": "szűrő",
"filter_other": "szűrők",
"filters": "szűrők",
"forward": "előre",
"gap": "rés",
"increase": "megnövelés",
"left": "bal",
"limit": "korlát",
"manage": "kezelés",
"maximize": "maximalizálás",
"menu": "menü",
"minimize": "minimalizálás",
"mbid": "MusicBrainz azonosító",
"noResultsFromQuery": "nincsenek találatok a keresett kifejezésre",
"note": "jegyzet",
"ok": "rendben",
"owner": "tulajdonos",
"path": "elérési út",
"playerMustBePaused": "a lejátszónak szüneteltetve kell lennie",
"preview": "előnézet",
"previousSong": "előző $t(entity.track_one)",
"quit": "kilépés",
"random": "véletlenszerű",
"refresh": "frissítés",
"reset": "reszetelés",
"resetToDefault": "visszaállítás alapértelmezettekre",
"right": "jobb",
"save": "mentés",
"search": "keresés",
"title": "cím",
"trackNumber": "dalszám",
"unknown": "ismeretlen",
"version": "verzió",
"yes": "igen",
"none": "egyik sem",
"restartRequired": "újraindítás szükséges",
"setting": "beállítás",
"translation": "fordítás",
"rating": "értékelések",
"reload": "újratöltés",
"share": "megosztás",
"sortOrder": "sorrend",
"saveAndReplace": "mentés és felülírás",
"saveAs": "mentés másként",
"size": "méret",
"year": "év",
"trackGain": "dal erősség",
"trackPeak": "dal csúcs"
},
"entity": {
"albumArtist_one": "album szerzője",
"albumArtist_other": "album szerzői",
"albumArtistCount_one": "{{count}} album szerző",
"albumArtistCount_other": "{{count}} album szerzők",
"albumWithCount_one": "{{count}} album",
"albumWithCount_other": "{{count}} album",
"artist_one": "előadó",
"artist_other": "előadók",
"favorite_one": "kedvelés",
"favorite_other": "kedvelések",
"folder_one": "mappa",
"folder_other": "mappák",
"genreWithCount_one": "{{count}} műfaj",
"genreWithCount_other": "{{count}} műfaj",
"track_one": "dal",
"track_other": "dalok",
"song_one": "zene",
"song_other": "zenék",
"album_one": "album",
"album_other": "albumok",
"smartPlaylist": "intelligens $t(entity.playlist_one)",
"artistWithCount_one": "{{count}} előadó",
"artistWithCount_other": "{{count}} előadó",
"playlist_one": "lejátszási lista",
"playlist_other": "lejátszási listák",
"playlistWithCount_one": "{{count}} lejátszási lista",
"playlistWithCount_other": "{{count}} lejátszási lista",
"folderWithCount_one": "{{count}} mappa",
"folderWithCount_other": "{{count}} mappa",
"genre_one": "műfaj",
"genre_other": "műfajok",
"play_one": "{{count}} lejátszás",
"play_other": "{{count}} lejátszás",
"trackWithCount_one": "{{count}} dal",
"trackWithCount_other": "{{count}} dal"
},
"error": {
"apiRouteError": "a kérést nem sikerült célbajuttatni",
"audioDeviceFetchError": "hiba történt a hangeszközök lekérésekor",
"authenticationFailed": "sikertelen hitelesítés",
"credentialsRequired": "hitelesítési adatok szükségesek",
"localFontAccessDenied": "hozzáférés megtagadásra került a helyi betűtípusokhoz",
"networkError": "hálózati hibába ütköztünk",
"openError": "a fájl megnyitása sikertelen volt",
"playbackError": "hiba történt a média lejátszásakor",
"remoteEnableError": "hiba történt a távoli szerver műveletkor: $t(common.enable)",
"remotePortError": "hiba történt a távoli szerver PORT-jának beállításakor",
"remotePortWarning": "indítsd újra a szervert az új PORT használatához",
"genericError": "hiba történt",
"endpointNotImplementedError": "a(z) {{endpoint}} végpont nincs implementálva a következőhöz: {{serverType}}",
"badAlbum": "azért látod ezt az oldalt mert ez a zeneszám nem része egy albumnak. ez általában akkor történik amikor egy szám a zenekönyvtárad gyökerébe kerül. a Jellyfin csak mappákba rendezett számokat csoportosít",
"loginRateError": "túl sok bejelentkezési kísérlet, kérlek próbáld újra pár másodperc múlva",
"mpvRequired": "MPV szükséges",
"invalidServer": "érvénytelen szerver",
"remoteDisableError": "hiba történt a távoli szerver műveletkor: $t(common.disable)",
"sessionExpiredError": "a munkameneted lejárt",
"systemFontError": "hiba történt a rendszer betűtípusainak lekérésekor",
"serverRequired": "szerver szükséges",
"serverNotSelectedError": "nincs szerver kiválasztva"
},
"filter": {
"albumCount": "$t(entity.album_other) darab",
"bitrate": "bitráta",
"comment": "megjegyzés",
"dateAdded": "hozzáadva",
"duration": "hossz",
"fromYear": "évtől",
"isCompilation": "gyűjtemény",
"isRated": "értékelt",
"lastPlayed": "utoljára lejátszva",
"mostPlayed": "legtöbbször lejátszott",
"note": "megjegyzés",
"random": "véletlenszerű",
"rating": "értékelések",
"recentlyAdded": "nemrég hozzáadott",
"releaseDate": "megjelenési dátum",
"releaseYear": "megjelenés éve",
"songCount": "dal szám",
"title": "cím",
"disc": "lemez",
"criticRating": "kritikusok értékelése",
"communityRating": "közösségi értékelés",
"albumArtist": "$t(entity.albumArtist_one)",
"biography": "biográfia",
"album": "$t(entity.album_one)",
"favorited": "kedvelt",
"isRecentlyPlayed": "mostanában lejátszott",
"name": "név",
"owner": "$t(common.owner)",
"id": "azonosító",
"recentlyPlayed": "nemrég lejátszott",
"isFavorited": "kedvelt",
"search": "keresés",
"isPublic": "nyilvános",
"playCount": "lejátszások száma",
"recentlyUpdated": "nemrég módosult",
"path": "elérési út",
"toYear": "évhez",
"trackNumber": "dalszám"
},
"form": {
"addServer": {
"error_savePassword": "hiba történt a jelszó mentésekor",
"ignoreCors": "CORS figyelmen kívül hagyása $t(common.restartRequired)",
"ignoreSsl": "SSL figyelmen kívül hagyása $t(common.restartRequired)",
"input_password": "jelszó",
"input_url": "url",
"input_username": "felhasználónév",
"success": "szerver sikeresen hozzáadva",
"title": "szerver hozzáadása",
"input_name": "szervernév",
"input_savePassword": "jelszó mentése",
"input_legacyAuthentication": "klasszikus hitelesítés bekapcsolása"
},
"addToPlaylist": {
"input_skipDuplicates": "duplikátumok átugrása",
"input_playlists": "$t(entity.playlist_other)",
"success": "hozzáadtuk ezt: $t(entity.trackWithCount, {\"count\": {{message}} }) a következőhöz: $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "hozzáadás a következőhöz: $t(entity.playlist_one)"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"input_name": "$t(common.name)",
"input_owner": "$t(common.owner)",
"input_public": "nyilvános",
"title": "$t(entity.playlist_one) létrehozása",
"success": "$t(entity.playlist_one) sikeresen létrehozva"
},
"deletePlaylist": {
"input_confirm": "a megerősítéshez írd be a(z) $t(entity.playlist_one) nevét",
"success": "$t(entity.playlist_one) sikeresen törölve",
"title": "$t(entity.playlist_one) törlése"
},
"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 nyilvános-e. Amennyiben azt szeretnéd, hogy nyilvános maradjon, válaszd ki az alábbi beviteli mezőt"
}
}
}
+747
View File
@@ -0,0 +1,747 @@
{
"action": {
"createPlaylist": "buat $t(entity.playlist_one)",
"toggleSmartPlaylistEditor": "ubah editor $t(entity.smartPlaylist)",
"goToPage": "pergi ke halaman",
"moveToTop": "pindah ke atas",
"addToPlaylist": "tambahkan ke $t(entity.playlist_one)",
"removeFromFavorites": "hapus dari $t(entity.favorite_other)",
"removeFromPlaylist": "hapus dari $t(entity.playlist_one)",
"deselectAll": "batalkan pilih semua",
"editPlaylist": "ubah $t(entity.playlist_one)",
"moveToNext": "pindah ke berikutnya",
"refresh": "$t(common.refresh)",
"removeFromQueue": "hapus dari antrean",
"setRating": "setel penilaian",
"viewPlaylists": "lihat $t(entity.playlist_other)",
"openIn": {
"lastfm": "Buka di Last.fm",
"musicbrainz": "Buka di MusicBrainz"
},
"addToFavorites": "tambahkan ke $t(entity.favorite_other)",
"clearQueue": "kosongkan antrian",
"deletePlaylist": "hapus $t(entity.playlist_one)",
"moveToBottom": "pindah ke bawah"
},
"common": {
"clear": "bersihkan",
"action_other": "aksi",
"codec": "Koded",
"channel_other": "Saluran",
"duration": "durasi",
"create": "buat",
"center": "tengah",
"areYouSure": "apakah Anda yakin?",
"add": "tambah",
"albumGain": "perolehan album",
"albumPeak": "Puncak album",
"cancel": "batal",
"close": "Tutup",
"configure": "konfigurasi",
"currentSong": "lagu saat ini $t(entity.track_one)",
"delete": "hapus",
"description": "deskripsi",
"edit": "ubah",
"biography": "biografi",
"confirm": "konfirmasi",
"descending": "menurun",
"disable": "nonaktifkan",
"disc": "disk",
"enable": "aktifkan",
"expand": "perbesar",
"favorite": "favorit",
"filter_other": "filter",
"filters": "filter",
"forceRestartRequired": "perlu restart untuk menerapkan perubahan... tutup pemberitahuan untuk memulai ulang",
"forward": "maju",
"gap": "jarak",
"home": "beranda",
"increase": "tingkatkan",
"left": "kiri",
"limit": "batasi",
"manage": "kelola",
"maximize": "maksimalkan",
"menu": "menu",
"minimize": "minimalisasi",
"modified": "dimodifikasi",
"mbid": "ID MusicBrainz",
"name": "nama",
"no": "tidak",
"none": "tidak ada",
"noResultsFromQuery": "permintaan tidak menghasilkan hasil",
"note": "catatan",
"ok": "oke",
"owner": "pemilik",
"playerMustBePaused": "pemain harus dijeda",
"preview": "Pratinjau",
"previousSong": "lagu sebelumnya $t(entity.track_one)",
"quit": "keluar",
"random": "acak",
"rating": "penilaian",
"refresh": "segarkan",
"reload": "Muat Ulang",
"reset": "reset",
"resetToDefault": "reset ke default",
"restartRequired": "restart diperlukan",
"right": "kanan",
"save": "simpan",
"saveAndReplace": "simpan dan ganti",
"saveAs": "simpan sebagai",
"search": "cari",
"setting": "pengaturan",
"share": "Bagikan",
"size": "ukuran",
"sortOrder": "urutkan",
"title": "judul",
"trackNumber": "pista",
"trackGain": "Gain pista",
"trackPeak": "puncak lagu",
"unknown": "tidak dikenal",
"version": "versi",
"year": "tahun",
"yes": "ya",
"path": "path(jalur)",
"ascending": "menaik",
"bpm": "bpm",
"bitrate": "kecepatan bit",
"collapse": "lipat",
"comingSoon": "segera hadir…",
"decrease": "kurangi",
"dismiss": "abaikan",
"translation": "terjemahan",
"backward": "mundur"
},
"entity": {
"album_other": "album",
"albumArtist_other": "artis album",
"albumArtistCount_other": "{{count}} artis album",
"albumWithCount_other": "{{count}} album",
"artist_other": "artis",
"artistWithCount_other": "{{count}} artis",
"favorite_other": "favorit",
"folder_other": "folder",
"folderWithCount_other": "{{count}} folder",
"genre_other": "genre",
"genreWithCount_other": "{{count}} genre",
"playlist_other": "daftar putar",
"play_other": "Putar {{count}}",
"playlistWithCount_other": "{{count}} daftar putar",
"smartPlaylist": "$t(entity.playlist_one) pintar",
"track_other": "pista",
"song_other": "lagu",
"trackWithCount_other": "{{count}} pista"
},
"error": {
"apiRouteError": "tidak dapat mengarahkan permintaan",
"audioDeviceFetchError": "terjadi kesalahan saat mencoba mengambil perangkat audio",
"authenticationFailed": "autentikasi gagal",
"badAlbum": "Anda melihat halaman ini karena lagu ini tidak termasuk dalam album. Masalah ini bisa terjadi jika Anda memiliki lagu di tingkat atas folder musik Anda. Jellyfin hanya mengelompokkan lagu jika mereka berada di dalam folder.",
"credentialsRequired": "kredensial diperlukan",
"endpointNotImplementedError": "endpoint {{endpoint}} tidak diimplementasikan untuk {{serverType}}",
"genericError": "terjadi kesalahan",
"invalidServer": "server tidak valid",
"localFontAccessDenied": "akses ke font lokal ditolak",
"loginRateError": "terlalu banyak percobaan login, coba beberapa detik lagi",
"mpvRequired": "MPV diperlukan",
"networkError": "terjadi kesalahan jaringan",
"openError": "Tidak dapat membuka file",
"playbackError": "terjadi kesalahan saat mencoba memutar media",
"remoteDisableError": "terjadi kesalahan saat mencoba $t(common.disable) server jarak jauh",
"remoteEnableError": "terjadi kesalahan saat mencoba $t(common.enable) server jarak jauh",
"remotePortError": "terjadi kesalahan saat mencoba mengatur port server jarak jauh",
"remotePortWarning": "restart server untuk menerapkan port baru",
"serverNotSelectedError": "tidak ada server yang dipilih",
"serverRequired": "server diperlukan",
"sessionExpiredError": "sesi Anda telah kedaluwarsa",
"systemFontError": "terjadi kesalahan saat mencoba mendapatkan font sistem"
},
"filter": {
"album": "$t(entity.album_one)",
"albumArtist": "$t(entity.albumArtist_one)",
"albumCount": "Hitung $t(entity.album_other)",
"artist": "$t(entity.artist_one)",
"biography": "biografi",
"bitrate": "bitrate",
"bpm": "lpm",
"channels": "$t(common.channel_other)",
"comment": "komentar",
"communityRating": "penilaian komunitas",
"criticRating": "penilaian kritik",
"dateAdded": "tanggal ditambahkan",
"disc": "disk",
"duration": "durasi",
"favorited": "favorit",
"genre": "$t(entity.genre_one)",
"id": "id",
"isCompilation": "apakah ini kompilasi",
"isFavorited": "apakah ini favorit",
"isPublic": "apakah ini publik",
"isRated": "apakah ini terklasifikasi",
"isRecentlyPlayed": "baru saja diputar",
"lastPlayed": "terakhir diputar",
"mostPlayed": "paling sering diputar",
"name": "nama",
"note": "catatan",
"owner": "$t(common.owner)",
"playCount": "jumlah putar",
"random": "acak",
"rating": "penilaian",
"recentlyAdded": "baru saja ditambahkan",
"recentlyPlayed": "baru saja diputar",
"recentlyUpdated": "baru saja diperbarui",
"releaseDate": "tanggal rilis",
"releaseYear": "tahun rilis",
"search": "cari",
"songCount": "jumlah lagu",
"toYear": "hingga tahun",
"trackNumber": "nomor pista",
"fromYear": "dari tahun",
"title": "judul",
"path": "path(jalur)"
},
"form": {
"addServer": {
"error_savePassword": "terjadi kesalahan saat mencoba menyimpan kata sandi",
"ignoreCors": "abaikan cors ($t(common.restartRequired))",
"ignoreSsl": "abaikan ssl ($t(common.restartRequired))",
"input_legacyAuthentication": "izinkan autentikasi warisan",
"input_name": "nama server",
"input_password": "kata sandi",
"input_savePassword": "simpan kata sandi",
"input_url": "url",
"input_username": "nama pengguna",
"success": "server berhasil ditambahkan",
"title": "tambah server"
},
"addToPlaylist": {
"input_playlists": "$t(entity.playlist_other)",
"input_skipDuplicates": "lewati duplikat",
"success": "ditambahkan $t(entity.trackWithCount, {\"count\": {{message}} }) ke $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "tambahkan ke $t(entity.playlist_one)"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"input_name": "$t(common.name)",
"input_owner": "$t(common.owner)",
"input_public": "publik",
"success": "$t(entity.playlist_one) berhasil dibuat",
"title": "buat $t(entity.playlist_one)"
},
"deletePlaylist": {
"input_confirm": "ketik nama $t(entity.playlist_one) untuk mengonfirmasi",
"success": "$t(entity.playlist_one) berhasil dihapus",
"title": "hapus $t(entity.playlist_one)"
},
"editPlaylist": {
"publicJellyfinNote": "Jellyfin entah bagaimana tidak menampilkan apakah playlist ini publik atau tidak. Jika Anda ingin playlist ini tetap publik, harap pilih entri berikut",
"success": "$t(entity.playlist_one) berhasil diperbarui",
"title": "ubah $t(entity.playlist_one)"
},
"lyricSearch": {
"input_artist": "$t(entity.artist_one)",
"input_name": "$t(common.name)",
"title": "cari lirik"
},
"queryEditor": {
"input_optionMatchAll": "cocokkan semua",
"input_optionMatchAny": "cocokkan salah satu"
},
"shareItem": {
"allowDownloading": "Izinkan unduhan",
"description": "Deskripsi",
"setExpiration": "Atur masa berlaku",
"success": "Tautan berbagi berhasil disalin ke papan klip (atau klik di sini untuk membuka)",
"expireInvalid": "Masa berlaku harus di masa depan",
"createFailed": "Tidak dapat membuat sumber daya berbagi (Apakah berbagi diaktifkan?)"
},
"updateServer": {
"success": "Server berhasil diperbarui",
"title": "perbarui server"
}
},
"page": {
"albumArtistDetail": {
"about": "Tentang {{artist}}",
"recentReleases": "Rilis terbaru",
"viewDiscography": "Lihat diskografi",
"relatedArtists": "$t(entity.artist_other) serupa",
"topSongs": "Lagu terbaik",
"topSongsFrom": "Lagu terbaik dari {{title}}",
"viewAll": "Lihat semua",
"viewAllTracks": "Lihat semua $t(entity.track_other)",
"appearsOn": "Tampil di"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"albumDetail": {
"moreFromArtist": "lebih banyak dari $t(entity.artist_one) ini",
"moreFromGeneric": "lebih banyak dari {{item}}",
"released": "dirilis"
},
"albumList": {
"artistAlbums": "album dari {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
"title": "$t(entity.album_other)"
},
"appMenu": {
"collapseSidebar": "perkecil sidebar",
"expandSidebar": "perluas sidebar",
"goBack": "kembali",
"goForward": "maju",
"manageServers": "kelola server",
"openBrowserDevtools": "buka alat pengembang browser",
"quit": "$t(common.quit)",
"selectServer": "pilih server",
"settings": "$t(common.setting_other)",
"version": "versi {{version}}"
},
"manageServers": {
"title": "kelola server",
"serverDetails": "detail server",
"url": "URL",
"username": "nama pengguna",
"editServerDetailsTooltip": "edit detail server",
"removeServer": "hapus server"
},
"contextMenu": {
"addFavorite": "$t(action.addToFavorites)",
"addLast": "$t(player.addLast)",
"addNext": "$t(player.addNext)",
"addToFavorites": "$t(action.addToFavorites)",
"addToPlaylist": "$t(action.addToPlaylist)",
"createPlaylist": "$t(action.createPlaylist)",
"deletePlaylist": "$t(action.deletePlaylist)",
"deselectAll": "$t(action.deselectAll)",
"download": "unduh",
"moveToNext": "$t(action.moveToNext)",
"moveToBottom": "$t(action.moveToBottom)",
"numberSelected": "{{count}} terpilih",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromQueue": "$t(action.removeFromQueue)",
"setRating": "$t(action.setRating)",
"playShuffled": "$t(player.shuffle)",
"shareItem": "Bagikan item",
"showDetails": "Lihat detail",
"moveToTop": "$t(action.moveToTop)",
"play": "$t(player.play)"
},
"fullscreenPlayer": {
"config": {
"dynamicBackground": "latar belakang dinamis",
"dynamicImageBlur": "ukuran blur gambar",
"dynamicIsImage": "aktifkan gambar latar belakang",
"followCurrentLyric": "ikuti lirik saat ini",
"lyricAlignment": "penyelarasan lirik",
"lyricSize": "ukuran lirik",
"opacity": "opasitas",
"showLyricMatch": "tampilkan kecocokan lirik",
"showLyricProvider": "tampilkan penyedia lirik",
"synchronized": "sinkronisasi",
"unsynchronized": "tidak sinkronisasi",
"useImageAspectRatio": "gunakan rasio aspek gambar",
"lyricOffset": "offset lirik (ms)",
"lyricGap": "jarak lirik"
},
"lyrics": "lirik",
"related": "terkait",
"upNext": "berikutnya",
"noLyrics": "tanpa lirik",
"visualizer": "visualisasi"
},
"genreList": {
"showAlbums": "Tampilkan $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "Tampilkan $t(entity.genre_one) $t(entity.track_other)",
"title": "$t(entity.genre_other)"
},
"globalSearch": {
"commands": {
"goToPage": "pergi ke halaman",
"searchFor": "cari {{query}}",
"serverCommands": "perintah server"
},
"title": "perintah"
},
"home": {
"explore": "jelajahi dari pustaka Anda",
"mostPlayed": "paling banyak diputar",
"newlyAdded": "rilis baru ditambahkan",
"recentlyPlayed": "baru saja diputar",
"title": "$t(common.home)"
},
"itemDetail": {
"copyPath": "Salin jalur ke papan klip",
"copiedPath": "Jalur berhasil disalin",
"openFile": "Tampilkan lagu di pengelola file"
},
"playlist": {
"reorder": "penataan ulang hanya aktif saat mengurutkan berdasarkan ID"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"setting": {
"advanced": "Lanjutan",
"generalTab": "umum",
"hotkeysTab": "tombol pintasan",
"playbackTab": "pemutaran",
"windowTab": "jendela"
},
"sidebar": {
"albumArtists": "$t(entity.albumArtist_other)",
"albums": "$t(entity.album_other)",
"artists": "$t(entity.artist_other)",
"folders": "$t(entity.folder_other)",
"genres": "$t(entity.genre_other)",
"home": "$t(common.home)",
"nowPlaying": "sedang diputar",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"settings": "$t(common.setting_other)",
"shared": "berbagi $t(entity.playlist_other)",
"tracks": "$t(entity.track_other)"
},
"trackList": {
"artistTracks": "lagu oleh {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
"title": "$t(entity.track_other)"
}
},
"player": {
"addLast": "tambahkan terakhir",
"favorite": "favorit",
"mute": "bisukan",
"muted": "terbisukan",
"next": "berikutnya",
"play": "putar",
"playbackFetchCancel": "ini memerlukan waktu... tutup pemberitahuan untuk membatalkan",
"playbackFetchInProgress": "memuat lagu…",
"playbackFetchNoResults": "tidak ada lagu ditemukan",
"playbackSpeed": "kecepatan pemutaran",
"playRandom": "putar acak",
"playSimilarSongs": "putar lagu serupa",
"previous": "sebelumnya",
"queue_clear": "bersihkan antrean",
"queue_moveToBottom": "pindahkan yang terpilih ke bawah",
"queue_moveToTop": "pindahkan yang terpilih ke atas",
"queue_remove": "hapus yang terpilih",
"repeat": "ulang",
"repeat_all": "ulang semua",
"repeat_off": "ulang dimatikan",
"shuffle": "putar acak",
"shuffle_off": "acak dimatikan",
"skip": "lewati",
"skip_back": "mundur",
"skip_forward": "lewati maju",
"stop": "berhenti",
"toggleFullscreenPlayer": "aktifkan pemutar layar penuh",
"unfavorite": "bukan favorit",
"pause": "jeda",
"viewQueue": "lihat antrean",
"addNext": "tambahkan berikutnya"
},
"setting": {
"accentColor": "warna sorotan",
"accentColor_description": "menetapkan warna sorotan aplikasi",
"albumBackground": "gambar latar belakang album",
"albumBackground_description": "Tambahkan gambar latar belakang ke halaman album yang berisi sampul album",
"albumBackgroundBlur": "Ukuran blur gambar latar belakang album",
"albumBackgroundBlur_description": "Atur tingkat blur gambar latar belakang album",
"applicationHotkeys": "tombol pintasan aplikasi",
"applicationHotkeys_description": "menetapkan tombol pintasan aplikasi. centang untuk menjadikannya tombol pintasan global (desktop saja)",
"artistConfiguration": "Pengaturan halaman artis album",
"artistConfiguration_description": "Atur elemen apa yang ditampilkan dan urutannya di halaman artis album",
"audioDevice": "perangkat audio",
"audioDevice_description": "pilih perangkat audio yang digunakan untuk pemutaran (hanya pemutar web)",
"audioExclusiveMode": "mode audio eksklusif",
"audioExclusiveMode_description": "aktifkan mode audio eksklusif. Dalam mode ini, sistem biasanya diblokir, dan hanya mpv yang akan diizinkan untuk output audio",
"audioPlayer": "pemutar audio",
"audioPlayer_description": "pilih pemutar audio yang digunakan untuk pemutaran",
"buttonSize": "ukuran tombol bilah pemutaran",
"buttonSize_description": "ukuran tombol pada bilah pemutaran",
"webAudio_description": "Menggunakan audio web. Ini mengaktifkan fitur lanjutan seperti Replaygain. Nonaktifkan opsi ini jika Anda mengalami masalah",
"windowBarStyle": "gaya bilah jendela",
"windowBarStyle_description": "pilih gaya bilah jendela",
"zoom": "persentase zoom",
"zoom_description": "tentukan persentase zoom aplikasi",
"clearCache_description": "'Pembersihan keras' Feishin. Untuk membersihkan cache Feishin, kosongkan cache browser (gambar yang disimpan dan elemen lainnya). Kredensial dan pengaturan server tetap terjaga",
"clearQueryCache": "Bersihkan cache Feishin",
"clearQueryCache_description": "'Pembersihan lunak' Feishin. Ini akan menyegarkan daftar putar, metadata lagu, dan mengatur ulang lirik yang disimpan. Pengaturan, kredensial server, dan gambar cache tetap terjaga",
"clearCacheSuccess": "Cache berhasil dibersihkan",
"contextMenu": "Pengaturan menu konteks (klik kanan)",
"contextMenu_description": "Memungkinkan Anda menyembunyikan elemen yang ditampilkan dalam menu saat Anda klik kanan pada elemen. Elemen yang tidak dipilih akan disembunyikan",
"crossfadeDuration": "durasi crossfade",
"crossfadeDuration_description": "atur durasi efek crossfade",
"crossfadeStyle": "gaya crossfade",
"crossfadeStyle_description": "pilih gaya crossfade yang digunakan oleh pemutar audio",
"customCssEnable": "Aktifkan CSS kustom",
"customCssEnable_description": "Memungkinkan penulisan CSS kustom.",
"customCssNotice": "Pemberitahuan: meskipun ada sanitasi (menolak url() dan content:), menggunakan CSS kustom masih dapat berisiko mengubah antarmuka.",
"customCss": "CSS kustom",
"customCss_description": "CSS kustom konten. Catatan: content dan url eksternal adalah properti yang ditolak. Pratinjau konten Anda ditampilkan di bawah. Entri tambahan yang tidak Anda tentukan hadir karena sanitasi.",
"customFontPath": "jalur font kustom",
"customFontPath_description": "tentukan jalur font kustom yang akan digunakan aplikasi",
"disableAutomaticUpdates": "nonaktifkan pembaruan otomatis",
"discordApplicationId": "ID aplikasi {{discord}}",
"discordApplicationId_description": "ID aplikasi untuk status aktivitas {{discord}} (defaultnya adalah {{defaultId}})",
"discordIdleStatus": "tampilkan status tidak aktif dalam status aktivitas",
"discordIdleStatus_description": "ketika diaktifkan, memperbarui status saat pemutar tidak aktif",
"discordListening": "Tampilkan status sebagai mendengarkan",
"discordListening_description": "tampilkan status sebagai mendengarkan alih-alih bermain",
"discordRichPresence": "status aktivitas {{discord}}",
"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",
"font_description": "tentukan font yang digunakan aplikasi",
"fontType": "jenis font",
"fontType_description": "font bawaan memilih salah satu font yang disediakan oleh Feishin. font sistem memungkinkan Anda memilih font apa pun yang disediakan oleh sistem operasi Anda. kustom memungkinkan Anda memberikan font Anda sendiri",
"fontType_optionBuiltIn": "font bawaan",
"fontType_optionCustom": "font kustom",
"fontType_optionSystem": "font sistem",
"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",
"homeConfiguration_description": "Mengatur elemen mana yang ditampilkan dan urutannya di halaman beranda",
"homeFeature": "Karusel fitur beranda",
"homeFeature_description": "Mengontrol apakah karusel besar fitur ditampilkan di halaman beranda",
"hotkey_browserBack": "mundur",
"hotkey_browserForward": "maju",
"hotkey_favoriteCurrentSong": "$t(common.currentSong) favorit",
"hotkey_favoritePreviousSong": "$t(common.previousSong) favorit",
"hotkey_globalSearch": "pencarian global",
"hotkey_localSearch": "pencarian di halaman",
"hotkey_playbackNext": "lagu berikutnya",
"hotkey_playbackPause": "jeda",
"hotkey_playbackPlay": "putar",
"hotkey_playbackPlayPause": "putar / jeda",
"hotkey_playbackPrevious": "lagu sebelumnya",
"hotkey_playbackStop": "berhenti",
"hotkey_rate0": "Bersihkan penilaian",
"hotkey_rate1": "beri penilaian 1 bintang",
"hotkey_rate2": "beri penilaian 2 bintang",
"hotkey_rate3": "beri penilaian 3 bintang",
"hotkey_rate4": "beri penilaian 4 bintang",
"hotkey_rate5": "beri penilaian 5 bintang",
"hotkey_skipBackward": "mundur",
"hotkey_skipForward": "lompat ke depan",
"hotkey_toggleCurrentSongFavorite": "ubah $t(common.currentSong) menjadi favorit",
"hotkey_toggleFullScreenPlayer": "ubah pemutar menjadi layar penuh",
"hotkey_togglePreviousSongFavorite": "ubah $t(common.previousSong) menjadi favorit",
"hotkey_toggleQueue": "ubah antrean",
"hotkey_toggleRepeat": "toggle ulangi",
"hotkey_toggleShuffle": "toggle acak",
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) tidak favorit",
"hotkey_unfavoritePreviousSong": "$t(common.previousSong) tidak favorit",
"hotkey_volumeDown": "turunkan volume",
"hotkey_volumeMute": "senyapkan volume",
"hotkey_volumeUp": "naikkan volume",
"hotkey_zoomIn": "perbesar",
"hotkey_zoomOut": "perkecil",
"imageAspectRatio": "Gunakan rasio aspek sampul asli",
"imageAspectRatio_description": "Jika diaktifkan, sampul akan ditampilkan dengan rasio aspek aslinya. Untuk seni yang tidak 1:1, ruang yang tersisa akan kosong",
"language_description": "menetapkan bahasa untuk aplikasi ($t(common.restartRequired))",
"lastfmApiKey": "Kunci API untuk {{lastfm}}",
"lastfmApiKey_description": "kunci API untuk {{lastfm}}. Diperlukan untuk sampul",
"lyricFetch": "cari lirik di Internet",
"lyricFetch_description": "mencari lirik dari berbagai sumber di Internet",
"lyricFetchProvider": "penyedia untuk mencari lirik",
"lyricFetchProvider_description": "pilih penyedia untuk mencari lirik. urutan penyedia adalah urutan pencarian",
"lyricOffset": "geser lirik (ms)",
"lyricOffset_description": "geser lirik sebanyak jumlah milidetik yang ditentukan",
"minimizeToTray": "minimalkan ke baki",
"minimizeToTray_description": "minimalkan aplikasi ke baki sistem",
"minimumScrobblePercentage": "persentase durasi scrobble minimum",
"minimumScrobblePercentage_description": "persentase minimum lagu yang harus diputar sebelum melakukan scrobble",
"minimumScrobbleSeconds": "scrobble minimum (detik)",
"minimumScrobbleSeconds_description": "durasi minimum dalam detik dari lagu yang harus diputar sebelum melakukan scrobble",
"mpvExecutablePath_description": "tentukan jalur executable mpv. jika dibiarkan kosong, jalur default akan digunakan",
"mpvExtraParameters_help": "Satu per baris",
"passwordStore": "kata sandi/penyimpanan rahasia",
"passwordStore_description": "metode penyimpanan kata sandi/kunci rahasia yang akan digunakan. ubah opsi ini jika Anda mengalami masalah dalam menyimpan kata sandi.",
"playbackStyle": "gaya pemutaran",
"playbackStyle_description": "pilih gaya pemutaran yang akan digunakan oleh pemutar audio",
"playbackStyle_optionCrossFade": "crossfade",
"playbackStyle_optionNormal": "normal",
"playButtonBehavior": "perilaku tombol putar",
"playButtonBehavior_description": "tentukan perilaku default tombol putar saat lagu ditambahkan ke antrean",
"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",
"remotePassword_description": "tentukan kata sandi untuk kontrol jarak jauh server. Kredensial ini dikirimkan dengan tidak aman secara default, jadi Anda harus menggunakan kata sandi unik untuk menghindari masalah",
"remotePort": "port kontrol jarak jauh server",
"remotePort_description": "tentukan port untuk kontrol jarak jauh server",
"remoteUsername": "nama pengguna kontrol jarak jauh server",
"remoteUsername_description": "tentukan nama pengguna untuk kontrol jarak jauh server. jika nama pengguna dan kata sandi kosong, otentikasi akan dinonaktifkan",
"replayGainClipping": "potong {{ReplayGain}}",
"replayGainClipping_description": "mencegah pemotongan yang disebabkan oleh {{ReplayGain}} dengan menurunkan gain secara otomatis",
"replayGainFallback": "alternatif {{ReplayGain}}",
"replayGainFallback_description": "gain dalam dB yang akan diterapkan jika file tidak memiliki tag {{ReplayGain}}",
"replayGainMode": "mode {{ReplayGain}}",
"replayGainMode_description": "menyesuaikan volume gain sesuai dengan nilai {{ReplayGain}} yang disimpan dalam metadata file",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"replayGainPreamp": "preamplifier (dB) {{ReplayGain}}",
"replayGainPreamp_description": "menyesuaikan gain preamplifier yang diterapkan ke nilai {{ReplayGain}}",
"sampleRate_description": "pilih rasio sampel output yang akan digunakan jika frekuensi sampel yang dipilih berbeda dari media yang sedang diputar. nilai di bawah 8000 akan menggunakan frekuensi default",
"savePlayQueue_description": "menyimpan antrean pemutaran saat aplikasi ditutup dan mengembalikannya saat dibuka",
"scrobble": "scrobble",
"scrobble_description": "melakukan scrobble pemutaran di server media Anda",
"showSkipButton": "tampilkan tombol lompat",
"showSkipButton_description": "menampilkan atau menyembunyikan tombol lompat di bilah pemutar",
"showSkipButtons": "tampilkan tombol lompat",
"showSkipButtons_description": "menampilkan atau menyembunyikan tombol lompat di bilah pemutar",
"sidebarCollapsedNavigation": "navigasi sidebar (terlipat)",
"sidebarCollapsedNavigation_description": "tampilkan atau sembunyikan navigasi di sidebar yang terlipat",
"sidebarConfiguration": "pengaturan sidebar",
"sidebarConfiguration_description": "pilih elemen dan urutan tampilannya di sidebar",
"sidebarPlaylistList": "daftar putar sidebar",
"sidebarPlaylistList_description": "tampilkan atau sembunyikan daftar putar di sidebar",
"sidePlayQueueStyle": "gaya antrean pemutaran samping",
"sidePlayQueueStyle_description": "menetapkan gaya antrean pemutaran samping",
"sidePlayQueueStyle_optionAttached": "terpasang",
"sidePlayQueueStyle_optionDetached": "terpisah",
"skipDuration": "durasi lompat",
"skipDuration_description": "tentukan durasi untuk lompat saat menggunakan tombol lompat di bilah pemutar",
"skipPlaylistPage": "lompat halaman daftar putar",
"skipPlaylistPage_description": "saat menavigasi ke daftar putar, pergi ke halaman daftar lagu dari daftar putar alih-alih halaman default",
"startMinimized": "mulai dengan minimalkan",
"startMinimized_description": "mulai aplikasi di baki sistem",
"theme": "tema",
"theme_description": "tentukan tema yang digunakan oleh aplikasi",
"themeDark": "tema (gelap)",
"themeDark_description": "tentukan tema gelap yang digunakan oleh aplikasi",
"themeLight": "tema (terang)",
"themeLight_description": "tentukan tema terang yang digunakan oleh aplikasi",
"transcodeNote": "Akan ditampilkan setelah 1 (web) - 2 (mpv) lagu",
"transcode": "aktifkan transkode",
"transcode_description": "mengaktifkan transkode ke berbagai format",
"transcodeBitrate": "bitrate untuk transkode",
"transcodeBitrate_description": "pilih bitrate untuk ditranskode. 0 berarti biarkan server yang memilih",
"transcodeFormat": "format untuk ditranskode",
"transcodeFormat_description": "pilih format untuk ditranskode. biarkan kosong agar server yang memutuskan",
"translationApiProvider": "Penyedia API penerjemahan",
"translationApiProvider_description": "Penyedia API untuk penerjemahan",
"translationApiKey": "kunci API penerjemahan",
"translationApiKey_description": "Kunci API untuk penerjemahan (hanya untuk endpoint layanan global)",
"translationTargetLanguage": "bahasa tujuan penerjemahan",
"translationTargetLanguage_description": "bahasa tujuan untuk penerjemahan",
"trayEnabled": "Tampilkan di area pemberitahuan",
"trayEnabled_description": "tampilkan/sembunyikan ikon/menu di area pemberitahuan. Jika dinonaktifkan, juga menonaktifkan meminimalkan/keluar ke baki",
"useSystemTheme": "gunakan tema sistem",
"useSystemTheme_description": "ikuti preferensi terang atau gelap yang ditetapkan oleh sistem",
"volumeWheelStep": "langkah roda volume",
"volumeWheelStep_description": "jumlah volume yang berubah saat menggulirkan roda mouse pada penggeser volume",
"volumeWidth": "Lebar penggeser volume",
"volumeWidth_description": "Lebar penggeser volume",
"webAudio": "gunakan audio web",
"clearCache": "Bersihkan cache browser",
"disableLibraryUpdateOnStartup": "nonaktifkan pemeriksaan versi baru saat startup",
"language": "bahasa",
"mpvExecutablePath": "jalur executable mpv",
"mpvExtraParameters": "parameter tambahan mpv",
"playButtonBehavior_optionPlay": "$t(player.play)",
"sampleRate": "rasio sampel",
"savePlayQueue": "simpan antrean pemutaran"
},
"table": {
"column": {
"album": "album",
"albumArtist": "artis album",
"albumCount": "$t(entity.album_other)",
"artist": "$t(entity.artist_one)",
"biography": "biografi",
"bitrate": "bitrate",
"bpm": "lpm",
"channels": "$t(common.channel_other)",
"codec": "$t(common.codec)",
"comment": "komentar",
"dateAdded": "tanggal ditambahkan",
"discNumber": "nomor disk",
"favorite": "favorit",
"genre": "$t(entity.genre_one)",
"lastPlayed": "terakhir diputar",
"path": "jalur",
"playCount": "putaran",
"rating": "penilaian",
"releaseDate": "tanggal rilis",
"releaseYear": "tahun",
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "judul",
"trackNumber": "pista"
},
"config": {
"general": {
"autoFitColumns": "sesuaikan kolom otomatis",
"followCurrentSong": "ikuti lagu saat ini",
"displayType": "tipe tampilan",
"gap": "$t(common.gap)",
"itemGap": "jarak antar elemen (px)",
"itemSize": "ukuran elemen (px)",
"size": "$t(common.size)",
"tableColumns": "kolom tabel"
},
"label": {
"actions": "$t(common.action_other)",
"album": "$t(entity.album_one)",
"albumArtist": "$t(entity.albumArtist_one)",
"artist": "$t(entity.artist_one)",
"biography": "$t(common.biography)",
"bitrate": "$t(common.bitrate)",
"bpm": "$t(common.bpm)",
"channels": "$t(common.channel_other)",
"codec": "$t(common.codec)",
"dateAdded": "tanggal ditambahkan",
"discNumber": "nomor disk",
"duration": "$t(common.duration)",
"favorite": "$t(common.favorite)",
"genre": "$t(entity.genre_one)",
"lastPlayed": "terakhir diputar",
"note": "$t(common.note)",
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"playCount": "jumlah putaran",
"rating": "$t(common.rating)",
"releaseDate": "tanggal rilis",
"rowIndex": "indeks baris",
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "$t(common.title)",
"titleCombined": "$t(common.title) (digabungkan)",
"trackNumber": "nomor pista",
"year": "$t(common.year)"
},
"view": {
"card": "kartu",
"poster": "poster",
"table": "tabel"
}
}
}
}
+273 -1
View File
@@ -9,6 +9,278 @@
"editPlaylist": "$t(entity.playlist_one) 편집",
"goToPage": "페이지 이동",
"moveToBottom": "맨 아래로 이동",
"moveToTop": "맨 위로 이동"
"moveToTop": "맨 위로 이동",
"moveToNext": "다음으로 이동",
"removeFromQueue": "대기열에서 제거",
"refresh": "$t(common.refresh)",
"removeFromFavorites": "$t(entity.favorite_other)에서 제거",
"removeFromPlaylist": "$t(entity.playlist_one)에서 제거",
"openIn": {
"musicbrainz": "MusicBrainz에서 보기",
"lastfm": "Last.fm에서 보기"
},
"viewPlaylists": "$t(entity.playlist_other) 보기",
"setRating": "평점 지정"
},
"common": {
"translation": "번역",
"resetToDefault": "기본 설정으로 되돌리기",
"right": "오른쪽",
"save": "저장",
"increase": "증가",
"version": "버전",
"year": "년",
"reset": "초기화",
"random": "랜덤",
"close": "닫기",
"codec": "코덱",
"create": "만들기",
"disc": "디스크",
"gap": "갭",
"left": "왼쪽",
"add": "추가",
"backward": "뒤로",
"saveAs": "(으)로 저장하기",
"search": "검색",
"setting": "설정",
"share": "공유",
"size": "크기",
"sortOrder": "순서",
"title": "곡명",
"trackNumber": "트랙번호",
"trackGain": "트랙 게인",
"trackPeak": "트랙 피크",
"unknown": "알 수 없음",
"cancel": "취소",
"clear": "지우기",
"collapse": "접기",
"comingSoon": "조만간…",
"configure": "설정",
"confirm": "확인",
"currentSong": "현재 $t(entity.track_one)",
"decrease": "감소",
"delete": "삭제",
"descending": "내림차순",
"description": "설명",
"disable": "비활성",
"edit": "편집",
"enable": "활성",
"expand": "확장",
"favorite": "즐겨찾기",
"forceRestartRequired": "변경 사항을 적용하려면 재실행 하세요... 알림을 닫으면 재실행합니다",
"forward": "앞으로",
"limit": "제한",
"manage": "관리하다",
"maximize": "최대화",
"menu": "메뉴",
"minimize": "최소화",
"modified": "수정된",
"name": "이름",
"path": "경로",
"playerMustBePaused": "플레이어가 일시정지 되어야 합니다",
"preview": "미리보기",
"previousSong": "이전곡 $t(entity.track_one)",
"quit": "종료",
"refresh": "새로고침",
"reload": "리로드",
"restartRequired": "반드시 재실행되어야 합니다",
"saveAndReplace": "저장하고 변경하기",
"yes": "네",
"ascending": "오름차순",
"areYouSure": "확실한가요?",
"bitrate": "비트 전송률",
"bpm": "bpm",
"biography": "바이오그래피",
"center": "중앙",
"channel_other": "채널",
"filter_other": "필터",
"mbid": "MusicBrainz ID",
"dismiss": "닫기",
"duration": "길이",
"home": "홈",
"no": "아니오",
"none": "없음",
"rating": "평점"
},
"entity": {
"albumWithCount_other": "{{count}} 앨범",
"artist_other": "아티스트",
"artistWithCount_other": "{{count}} 아티스트",
"favorite_other": "즐겨찾기",
"folder_other": "폴더",
"genre_other": "장르",
"genreWithCount_other": "{{count}} 장르",
"playlist_other": "플레이리스트",
"album_other": "앨범",
"albumArtist_other": "앨범 아티스트",
"albumArtistCount_other": "{{count}} 앨범 아티스트",
"folderWithCount_other": "{{count}} 폴더",
"trackWithCount_other": "{{count}} 트랙",
"song_other": "곡",
"play_other": "{{count}} 재생",
"playlistWithCount_other": "{{count}} 재생목록",
"smartPlaylist": "스마트 $t(entity.playlist_one)",
"track_other": "트랙"
},
"error": {
"systemFontError": "시스템 폰트를 가져오는데 실패하였습니다",
"loginRateError": "너무 많은 로그인 시도하였습니다 잠시 후 다시 시도해 주세요",
"mpvRequired": "MPV 필요",
"openError": "파일을 열 수 없습니다",
"remoteDisableError": "원격 서버를 $t(common.disable) 하는데 실패하였습니다",
"playbackError": "미디어를 재생하는 도중에 에러가 발생하였습니다",
"remoteEnableError": "원격 서버를 $t(common.enable) 하는데 실패하였습니다",
"serverNotSelectedError": "선택된 서버가 없습니다",
"serverRequired": "서버가 필요합니다",
"sessionExpiredError": "세션이 만료되었습니다",
"networkError": "네트워크 에러가 발생하였습니다",
"remotePortError": "원격 서버의 포트 설정하는데 실패하였습니다",
"remotePortWarning": "새로 설정한 포트를 적용하기 위해 서버를 재실행 해 주세요",
"audioDeviceFetchError": "오디오 장치를 불러올 수 없습니다",
"authenticationFailed": "인증 실패",
"badAlbum": "이 곡은 앨범의 일부가 아니기 때문에 표시되는 것입니다. 음악 폴더의 최상위에 곡이 있는 경우 이런 문제가 발생할 가능성이 높습니다. Jellyfin은 폴더 내 그룹만 추적합니다.",
"credentialsRequired": "인증서가 필요함",
"endpointNotImplementedError": "엔드포인트 {{endpoint}} 는 {{serverType}} 에 대해 구현되지 않았습니다",
"genericError": "에러가 발생했습니다",
"invalidServer": "잘못된 서버",
"localFontAccessDenied": "로컬 글꼴에 접근 거부되었습니다"
},
"filter": {
"title": "곡명",
"isRecentlyPlayed": "최근에 재생한",
"name": "이름",
"path": "경로",
"playCount": "재생 횟수",
"random": "무작위",
"recentlyAdded": "최근에 추가된",
"releaseDate": "발매일",
"recentlyPlayed": "최근에 재생된",
"recentlyUpdated": "최근에 업데이트된",
"search": "검색",
"dateAdded": "추가된 날짜",
"lastPlayed": "마지막으로 재생한",
"mostPlayed": "가장 많이 재생한",
"album": "$t(entity.album_one)",
"albumArtist": "$t(entity.albumArtist_one)",
"artist": "$t(entity.artist_one)",
"communityRating": "커뮤니티 평점",
"criticRating": "비평가 평점",
"disc": "디스크",
"bitrate": "비트 전송률",
"biography": "바이오그래피",
"channels": "$t(common.channel_other)",
"duration": "길이",
"bpm": "bpm"
},
"form": {
"addServer": {
"title": "서버 추가하기",
"success": "서버 추가하였습니다",
"input_name": "서버 이름",
"input_password": "비밀번호",
"input_savePassword": "비밀번호 저장하기",
"input_url": "url",
"error_savePassword": "비밀번호를 저장하는 도중 오류가 발생했습니다",
"ignoreCors": "CORS 무시 ($t(common.restartRequired))",
"ignoreSsl": "SSL 무시 ($t(common.restartRequired))",
"input_legacyAuthentication": "레거시 인증 사용",
"input_username": "유저 이름"
},
"addToPlaylist": {
"input_skipDuplicates": "중복 건너뛰기",
"title": "$t(entity.playlist_one) 에 추가",
"input_playlists": "$t(entity.playlist_other)"
},
"lyricSearch": {
"title": "가사 검색",
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)"
},
"queryEditor": {
"input_optionMatchAll": "모두 일치",
"input_optionMatchAny": "무엇이든 일치"
},
"editPlaylist": {
"title": "$t(entity.playlist_one) 편집",
"publicJellyfinNote": "Jellyfin은 재생목록 공개 여부를 노출하지 않습니다. 만약 공개되길 원한다면 다음을 선택하세요",
"success": "$t(entity.playlist_one) 업데이트 되었습니다"
},
"shareItem": {
"allowDownloading": "다운로드 허용",
"description": "설명",
"success": "클립보드에 공유 링크를 복사했습니다 (또는 열어보려면 클릭하세요)",
"expireInvalid": "만료 날짜는 미래 날짜여야만 합니다",
"createFailed": "공유 링크를 생성하는데 실패하였습니다 (혹시 공유하기 설정되어 있나요?)",
"setExpiration": "만료 기간 설정하기"
},
"updateServer": {
"title": "서버 업데이트",
"success": "서버 업데이트 되었습니다"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"input_name": "$t(common.name)",
"success": "$t(entity.playlist_one)를 생성했습니다",
"input_owner": "$t(common.owner)",
"input_public": "공개",
"title": "$t(entity.playlist_one) 생성"
},
"deletePlaylist": {
"input_confirm": "확인을 위해 $t(entity.playlist_one)의 이름을 적어주세요",
"success": "$t(entity.playlist_one)가 삭제되었습니다",
"title": "$t(entity.playlist_one) 삭제"
}
},
"page": {
"appMenu": {
"goBack": "뒤로",
"selectServer": "서버를 선택하세요",
"goForward": "앞으로",
"manageServers": "서버 설정하기",
"openBrowserDevtools": "브라우저 개발자 도구 열기",
"version": "버전 {{version}}"
},
"manageServers": {
"title": "서버 설정하기",
"serverDetails": "서버 세부설정",
"editServerDetailsTooltip": "서버 세부설정 편집하기",
"url": "URL",
"username": "username",
"removeServer": "서버 제거하기"
},
"fullscreenPlayer": {
"config": {
"opacity": "투명도",
"lyricAlignment": "가사 정렬",
"useImageAspectRatio": "이미지 종횡비 사용",
"synchronized": "동기화",
"unsynchronized": "비동기화"
},
"lyrics": "가사"
},
"contextMenu": {
"download": "다운로드",
"numberSelected": "{{count}}개 선택됨"
},
"albumArtistDetail": {
"about": "{{artist}}에 대해",
"viewDiscography": "디스코그래피 보기",
"appearsOn": "참여 앨범",
"recentReleases": "최근 앨범",
"relatedArtists": "연관 $t(entity.artist_other)"
}
},
"table": {
"config": {
"label": {
"playCount": "재생 횟수",
"dateAdded": "추가된 날짜"
},
"view": {
"card": "카드",
"poster": "포스터",
"table": "표"
}
}
}
}
+444 -1
View File
@@ -1 +1,444 @@
{}
{
"action": {
"openIn": {
"lastfm": "Åpne i Last.fm",
"musicbrainz": "Åpne i MusicBrainz"
},
"moveToBottom": "flytt til bunnen",
"deletePlaylist": "slett $t(entity.playlist_one)",
"deselectAll": "avmarker alle",
"editPlaylist": "rediger $t(entity.playlist_one)",
"addToFavorites": "legg til $t(entity.favorite_other)",
"addToPlaylist": "legg til $t(entity.playlist_one)",
"clearQueue": "tøm kø",
"createPlaylist": "opprett $t(entity.playlist_one)",
"goToPage": "gå til side",
"moveToTop": "flytt til toppen",
"refresh": "$t(common.refresh)",
"removeFromFavorites": "fjern fra $t(entity.favorite_other)",
"moveToNext": "flytt til neste",
"setRating": "angi vurdering",
"removeFromQueue": "fjern fra kø",
"removeFromPlaylist": "fjern fra $t(entity.playlist_one)",
"viewPlaylists": "vise $t(entity.playlist_other)",
"toggleSmartPlaylistEditor": "bytt $t(entity.smartPlaylist) editor"
},
"common": {
"bpm": "bpm",
"cancel": "avbryt",
"center": "midtstill",
"clear": "tøm",
"collapse": "slå sammen",
"configure": "konfigurer",
"confirm": "bekreft",
"currentSong": "gjeldende $t(entity.track_one)",
"version": "versjon",
"areYouSure": "er du sikker?",
"ascending": "stigende",
"backward": "bakover",
"biography": "biografi",
"bitrate": "bithastighet",
"close": "lukk",
"codec": "kodek",
"comingSoon": "kommer snart…",
"create": "opprett",
"decrease": "minsk",
"disable": "deaktiver",
"disc": "skive",
"duration": "lengde",
"enable": "aktiver",
"expand": "utvid",
"favorite": "favoritt",
"filters": "filter",
"forceRestartRequired": "ta omstart for å la endringene trå i kraft... lukk meldingen for å ta omstart",
"forward": "fremover",
"gap": "avstand",
"home": "hjem",
"increase": "øke",
"left": "venstre",
"limit": "grense",
"menu": "meny",
"minimize": "minimer",
"modified": "modifisert",
"mbid": "MusicBrainz ID",
"name": "navn",
"no": "nei",
"none": "ingen",
"noResultsFromQuery": "spørringen ga ikke noe resultat",
"note": "merke",
"owner": "eier",
"playerMustBePaused": "spilleren må settes på pause",
"path": "sti",
"previousSong": "forrige $t(entity.track_one)",
"refresh": "frisk opp",
"rating": "vurdering",
"random": "vilkårlig",
"reset": "tilbakestill",
"restartRequired": "omstart nødvendig",
"save": "lagre",
"saveAs": "lagre som",
"saveAndReplace": "lagre og overskriv",
"search": "søk",
"trackGain": "forsterkningsgrad spor",
"trackPeak": "maksnivå spor",
"translation": "oversettelse",
"unknown": "ukjent",
"preview": "forhåndsvisning",
"share": "del",
"quit": "avslutt",
"size": "størrelse",
"setting": "innstilling",
"trackNumber": "spor",
"title": "tittel",
"channel_one": "kanal",
"channel_other": "kanaler",
"filter_one": "filter",
"filter_other": "filter",
"add": "legg til",
"edit": "rediger",
"resetToDefault": "nullstill",
"ok": "ok",
"reload": "last inn på nytt",
"action_one": "handling",
"action_other": "handlinger",
"year": "år",
"yes": "ja",
"descending": "synkende",
"dismiss": "avkreft",
"delete": "slett",
"description": "beskrivelse",
"manage": "håndtere",
"maximize": "maksimer",
"right": "høyre",
"sortOrder": "rekkefølge"
},
"entity": {
"smartPlaylist": "smart $t(entity.playlist_one)",
"album_one": "album",
"album_other": "album",
"albumArtist_one": "albumartist",
"albumArtist_other": "albumartister",
"albumArtistCount_one": "{{count}} albumartist",
"albumArtistCount_other": "{{count}} albumartister",
"albumWithCount_one": "{{count}} album",
"albumWithCount_other": "{{count}} album",
"favorite_one": "favoritt",
"favorite_other": "favoritter",
"folder_one": "mappe",
"folder_other": "mapper",
"play_one": "{{count}} avspilling",
"play_other": "{{count}} avspillinger",
"playlistWithCount_one": "{{count}} spilleliste",
"playlistWithCount_other": "{{count}} spillelister",
"artistWithCount_one": "{{count}} artist",
"artistWithCount_other": "{{count}} artister",
"genre_one": "sjanger",
"genre_other": "sjangere",
"track_one": "spor",
"track_other": "spor",
"genreWithCount_one": "{{count}} sjanger",
"genreWithCount_other": "{{count}} sjangere",
"playlist_one": "spilleliste",
"playlist_other": "spillelister",
"folderWithCount_one": "{{count}} mappe",
"folderWithCount_other": "{{count}} mapper",
"trackWithCount_one": "{{count}} spor",
"trackWithCount_other": "{{count}} spor",
"artist_one": "artist",
"artist_other": "artister",
"song_one": "sang",
"song_other": "sanger"
},
"error": {
"apiRouteError": "kan ikke behandle forespørselen",
"mpvRequired": "MPV er påkrevd",
"authenticationFailed": "autentisering feilet",
"badAlbum": "du ser denne siden fordi sangen ikke er med i et album. Mest sannsynlig opplever du dette problemet fordi du har en sang helt øverst i musikkmappen. jellyfin gruperer kun spor som ligger i en mappe.",
"endpointNotImplementedError": "endepunkt {{endpoint}} er ikke implementert for {{serverType}}",
"credentialsRequired": "innloggingsdetaljer er påkrevd",
"genericError": "en feil har oppstått",
"invalidServer": "ugyldig server",
"playbackError": "et problem oppstod ved avspilling av media",
"localFontAccessDenied": "ingen tilgang til lokale skrifttyper",
"loginRateError": "for mange innloggingsforsøk, vennligst prøv igjen om noen få sekunder",
"audioDeviceFetchError": "en feil oppstod ved innhenting av lydenheter",
"networkError": "at nettverksproblem har oppstått",
"openError": "kunne ikke åpne fil",
"serverNotSelectedError": "ingen server er valgt",
"remotePortError": "et problem oppstod med å sette serverport",
"systemFontError": "et problem oppstod med innlasting av systemskrifttyper",
"serverRequired": "server er påkrevd",
"sessionExpiredError": "sesjonen din har utløpt",
"remotePortWarning": "ta omstart av serveren for å aktivere ny port",
"remoteDisableError": "en problem oppstod ved å $t(common.disable) serveren",
"remoteEnableError": "et problem oppstod ved å $t(common.enable) serveren"
},
"filter": {
"bpm": "bpm",
"criticRating": "kritikervurdering",
"id": "id",
"name": "navn",
"bitrate": "bithastighet",
"albumArtist": "$t(entity.albumArtist_one)",
"artist": "$t(entity.artist_one)",
"biography": "biografi",
"album": "$t(entity.album_one)",
"duration": "lengde",
"favorited": "merket som favoritt",
"comment": "kommentar",
"communityRating": "fellesskapsvurdering",
"dateAdded": "lagt til dato",
"disc": "skive",
"isPublic": "er offentlig",
"isRecentlyPlayed": "er avspilt nylig",
"mostPlayed": "mest avspilt",
"owner": "$t(common.owner)",
"path": "sti",
"lastPlayed": "sist avspilt",
"rating": "vurdering",
"recentlyPlayed": "nylig avspilt",
"playCount": "antall avspillinger",
"recentlyUpdated": "nylig oppdatert",
"random": "vilkårlig",
"search": "søk",
"songCount": "antall sanger",
"title": "tittel",
"toYear": "til år",
"releaseDate": "utgivelsesdato",
"releaseYear": "utgivelsesår",
"note": "notat",
"isRated": "er vurdert",
"fromYear": "fra år",
"isCompilation": "er samling",
"isFavorited": "er merket som favoritt",
"recentlyAdded": "nylig lagt til",
"channels": "$t(common.channel_other)",
"genre": "$t(entity.genre_one)",
"trackNumber": "spor"
},
"form": {
"createPlaylist": {
"input_description": "$t(common.description)",
"input_owner": "$t(common.owner)",
"input_public": "offentlig",
"title": "opprett $t(entity.playlist_one)",
"input_name": "$t(common.name)",
"success": "$t(entity.playlist_one) opprettet"
},
"lyricSearch": {
"input_artist": "$t(entity.artist_one)",
"input_name": "$t(common.name)",
"title": "sangtekstsøk"
},
"addServer": {
"ignoreCors": "ignorer cors ($t(common.restartRequired))",
"ignoreSsl": "ignorer ssl ($t(common.restartRequired))",
"error_savePassword": "en problem oppstod ved lagring av passord",
"input_savePassword": "lagre passord",
"input_url": "lenke",
"input_username": "brukernavn",
"success": "serveren er lagt til",
"input_legacyAuthentication": "aktiver tradisjonell autentisering",
"input_name": "servernavn",
"title": "legg til server",
"input_password": "passord"
},
"addToPlaylist": {
"success": "la $t(entity.trackWithCount, {\"count\": {{message}} }) til $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "legg til i $t(entity.playlist_one)",
"input_skipDuplicates": "hopp over duplikater",
"input_playlists": "$t(entity.playlist_other)"
},
"deletePlaylist": {
"title": "slett $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) er slettet",
"input_confirm": "skrive inn navnet på $t(entity.playlist_one) for å bekrefte"
},
"editPlaylist": {
"title": "rediger $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) er oppdatert"
},
"shareItem": {
"allowDownloading": "tillat nedlasting",
"description": "beskrivelse",
"createFailed": "opprettelse av delt ressurs feilet (er deling aktivert?)",
"setExpiration": "angi utløpstid",
"success": "del lenke som er kopiert til utklippstavlen (eller klikk her for å åpne)",
"expireInvalid": "utløpstid må være et fremtidig tidspunkt"
},
"updateServer": {
"success": "vellykket oppdatering av serveren",
"title": "oppdater server"
}
},
"page": {
"appMenu": {
"collapseSidebar": "slå sammen sidefelt",
"quit": "$t(common.quit)",
"selectServer": "velg server",
"version": "versjon {{version}}",
"manageServers": "administrere servere",
"goBack": "gå tilbake",
"openBrowserDevtools": "åpne utviklingsverktøy i nettleser",
"settings": "$t(common.setting_other)",
"expandSidebar": "utvid sidefelt",
"goForward": "gå fremover"
},
"contextMenu": {
"addToPlaylist": "$t(action.addToPlaylist)",
"showDetails": "hent info",
"moveToTop": "$t(action.moveToTop)",
"moveToBottom": "$t(action.moveToBottom)",
"numberSelected": "{{count}} valgt",
"addLast": "$t(player.addLast)",
"addNext": "$t(player.addNext)",
"createPlaylist": "$t(action.createPlaylist)",
"play": "$t(player.play)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"download": "last ned",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromQueue": "$t(action.removeFromQueue)",
"setRating": "$t(action.setRating)",
"addToFavorites": "$t(action.addToFavorites)",
"moveToNext": "$t(action.moveToNext)",
"playShuffled": "$t(player.shuffle)",
"shareItem": "del element",
"addFavorite": "$t(action.addToFavorites)",
"deletePlaylist": "$t(action.deletePlaylist)",
"deselectAll": "$t(action.deselectAll)"
},
"albumArtistDetail": {
"topSongs": "beste sanger",
"viewDiscography": "se diskografi",
"recentReleases": "nylige utgivelser",
"topSongsFrom": "beste sanger fra {{title}}",
"viewAllTracks": "se alle $t(entity.track_other)",
"viewAll": "se alle",
"about": "Om {{artist}}",
"appearsOn": "opptrer på",
"relatedArtists": "relatert $t(entity.artist_other)"
},
"albumList": {
"artistAlbums": "album av {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
"title": "$t(entity.album_other)"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"albumDetail": {
"moreFromArtist": "mer fra denne $t(entity.artist_one)",
"moreFromGeneric": "mer fra {{item}}",
"released": "utgitt"
},
"fullscreenPlayer": {
"config": {
"dynamicIsImage": "aktiver bakgrunnsbilde",
"lyricGap": "sangtekstavstand",
"dynamicImageBlur": "bilduskarphetstørrelse",
"lyricAlignment": "sangtekstjustering",
"lyricOffset": "sangtekstjustering (ms)",
"lyricSize": "sangtekststørrelse",
"opacity": "absorpsjon",
"showLyricMatch": "vis sangteksttreff",
"showLyricProvider": "vis sangteksttilbyder",
"synchronized": "synkronisert",
"unsynchronized": "usynkronisert",
"dynamicBackground": "dynamisk bakgrunn",
"useImageAspectRatio": "bruk sideforhold til bildet",
"followCurrentLyric": "følg sangtekst"
},
"noLyrics": "fant ikke sangtekst",
"lyrics": "sangtekst",
"upNext": "kommende",
"visualizer": "fremviser",
"related": "relatert"
},
"genreList": {
"title": "$t(entity.genre_other)",
"showAlbums": "vis $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "vis $t(entity.genre_one) $t(entity.track_other)"
},
"globalSearch": {
"title": "kommandoer",
"commands": {
"goToPage": "gå til side",
"searchFor": "søk etter {{query}}",
"serverCommands": "serverkommandoer"
}
},
"home": {
"recentlyPlayed": "nylig avspilt",
"explore": "utforsk biblioteket ditt",
"mostPlayed": "mest spilt",
"newlyAdded": "utgivelser nylig lagt til",
"title": "$t(common.home)"
},
"manageServers": {
"title": "administrere servere",
"url": "lenke",
"username": "brukernavn",
"editServerDetailsTooltip": "rediger serverdetaljer",
"removeServer": "fjern server",
"serverDetails": "serverdetaljer"
},
"itemDetail": {
"openFile": "vis spor i filbhehandleren",
"copiedPath": "vellykket kopiering av stien",
"copyPath": "kopier stien til utklippstavlen"
},
"trackList": {
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
"title": "$t(entity.track_other)",
"artistTracks": "spor fra {{artist}}"
},
"sidebar": {
"albumArtists": "$t(entity.albumArtist_other)",
"tracks": "$t(entity.track_other)",
"nowPlaying": "spilles nå",
"folders": "$t(entity.folder_other)",
"genres": "$t(entity.genre_other)",
"home": "$t(common.home)",
"albums": "$t(entity.album_other)",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"settings": "$t(common.setting_other)",
"shared": "delt $t(entity.playlist_other)",
"artists": "$t(entity.artist_other)"
},
"setting": {
"generalTab": "generelt",
"advanced": "avansert",
"hotkeysTab": "hurtigtaster",
"playbackTab": "avspilling",
"windowTab": "vindu"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
}
},
"player": {
"addLast": "legg til sist",
"queue_remove": "fjern valgte",
"queue_moveToBottom": "flytt valgte til toppen",
"addNext": "legg til som neste",
"favorite": "favoritt",
"mute": "skru av lyden",
"muted": "lyden er skrudd av",
"next": "neste",
"repeat_all": "gjenta alle",
"playbackFetchCancel": "dette kommer til å ta en stund... lukk denne meldingen for å avbryte",
"playRandom": "spill vilkårlig",
"queue_clear": "tøm kø",
"repeat_off": "gjentakelse er deaktivert",
"playbackFetchInProgress": "laster sanger…",
"repeat": "gjenta",
"play": "spill",
"previous": "forrige",
"queue_moveToTop": "flytt valgte til bunnen",
"playbackFetchNoResults": "ingen sanger funnet",
"playbackSpeed": "avspillingshastighet",
"playSimilarSongs": "spill lignende sanger"
}
}
+5 -2
View File
@@ -86,7 +86,8 @@
"codec": "codec",
"preview": "pré-visualizar",
"share": "compartilhar",
"close": "fechar"
"close": "fechar",
"translation": "tradução"
},
"action": {
"goToPage": "vá para página",
@@ -108,7 +109,9 @@
"openIn": {
"lastfm": "Abrir em Last.fm",
"musicbrainz": "Abrir em MusicBrainz"
}
},
"toggleSmartPlaylistEditor": "alternar editor $t(entity.smartPlaylist)",
"moveToNext": "mover para o próximo"
},
"form": {
"deletePlaylist": {
+151 -88
View File
@@ -7,8 +7,8 @@
"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)",
"removeFromPlaylist": "удалить из $t(entity.playlist_few)",
"viewPlaylists": "показать $t(entity.playlist_other)",
"refresh": "$t(common.refresh)",
"deletePlaylist": "удалить $t(entity.playlist_one)",
"removeFromQueue": "удалить из очереди",
@@ -16,7 +16,7 @@
"moveToBottom": "вниз",
"setRating": "оценить",
"toggleSmartPlaylistEditor": "вкл./откл. редактор $t(entity.smartPlaylist)",
"removeFromFavorites": "удалить из $t(entity.favorite_other)",
"removeFromFavorites": "удалить из $t(entity.favorite_few)",
"openIn": {
"lastfm": "открыть на Last.fm",
"musicbrainz": "открыть на MusicBrainz"
@@ -38,15 +38,15 @@
"currentSong": "текущий $t(entity.track_one)",
"collapse": "закрыть",
"trackNumber": "трек",
"descending": "убывание",
"descending": "по убыванию",
"add": "добавить",
"gap": "промежуток",
"ascending": "возрастанию",
"ascending": "по возрастанию",
"dismiss": "отклонить",
"year": "год",
"manage": "управлять",
"manage": "управление",
"limit": "ограничение",
"minimize": "минимизировать",
"minimize": "свернуть",
"modified": "изменено",
"duration": "длительность",
"name": "имя",
@@ -66,6 +66,9 @@
"cancel": "отменить",
"forceRestartRequired": "перезапустите приложение, чтобы применить изменения... закройте уведомление для перезапуска",
"setting": "настройка",
"setting_one": "настройка",
"setting_few": "",
"setting_many": "",
"version": "версия",
"title": "название",
"filter_one": "фильтр",
@@ -78,11 +81,11 @@
"action_one": "действие",
"action_few": "действия",
"action_many": "действий",
"playerMustBePaused": "воспроизведение должно быть остановлено",
"playerMustBePaused": "необходимо остановить воспроизведение",
"confirm": "подтвердить",
"resetToDefault": "по умолчанию",
"home": "главная страница",
"comingSoon": "скоро будет…",
"resetToDefault": "сбросить настройки",
"home": "главная",
"comingSoon": "скоро...",
"reset": "сбросить",
"channel_one": "канал",
"channel_few": "канала",
@@ -92,14 +95,14 @@
"menu": "меню",
"restartRequired": "необходим перезапуск приложения",
"previousSong": "предыдущий $t(entity.track_one)",
"noResultsFromQuery": ет результатов",
"noResultsFromQuery": ичего не найдено",
"quit": "выйти",
"expand": "расширить",
"expand": "раскрыть",
"search": "поиск",
"saveAs": "сохранить как",
"disc": "диск",
"yes": "да",
"random": "случайный",
"random": "случайно",
"size": "размер",
"biography": "биография",
"note": "заметка",
@@ -109,7 +112,12 @@
"preview": "просмотр",
"codec": "кодек",
"share": "поделиться",
"close": "закрыть"
"close": "закрыть",
"albumGain": "альбом усиление",
"trackGain": "усиление трека",
"translation": "перевод",
"albumPeak": "пик альбома",
"trackPeak": "пик трека"
},
"entity": {
"album_one": "альбом",
@@ -124,18 +132,25 @@
"playlist_one": "плейлист",
"playlist_few": "плейлиста",
"playlist_many": "плейлистов",
"play": "{{count}} прослушиваний",
"play_one": "{{count}} прослушивание",
"play_few": "",
"play_many": "",
"artist_one": "автор",
"artist_few": "автора",
"artist_many": "авторов",
"artist_many": "исполнителей",
"folderWithCount_one": "{{count}} папка",
"folderWithCount_few": "{{count}} папки",
"folderWithCount_many": "{{count}} папок",
"albumArtist_one": "автор альбома",
"albumArtist_few": "автора альбома",
"albumArtist_many": "авторов альбома",
"albumArtist_one": "исполнитель альбома",
"albumArtist_few": "исполнители альбома",
"albumArtist_many": "исполнителей альбома",
"track_one": "трек",
"track_few": "трека",
"track_many": "треков",
"song_one": "песня",
"song_few": "{{count}} песни",
"song_many": "{{count}} песен",
"albumArtistCount_one": "{{count}} автор альбома",
"albumArtistCount_few": "{{count}} автора альбома",
"albumArtistCount_many": "{{count}} авторов альбома",
@@ -162,7 +177,7 @@
"table": {
"config": {
"view": {
"card": "карта",
"card": "карточки",
"table": "таблица",
"poster": "постер"
},
@@ -171,8 +186,9 @@
"gap": "$t(common.gap)",
"tableColumns": "столбцы таблицы",
"autoFitColumns": "автоматически расставить столбцы",
"followCurrentSong": "следовать за исполняемым треком",
"size": "$t(common.size)",
"itemSize": "рамер элементов (px)",
"itemSize": "размер элементов (px)",
"itemGap": "отступ между элементами (px)"
},
"label": {
@@ -185,7 +201,7 @@
"bpm": "$t(common.bpm)",
"lastPlayed": "последний",
"trackNumber": "номер трека",
"rowIndex": "индекс ряда",
"rowIndex": "номер строки",
"rating": "$t(common.rating)",
"artist": "$t(entity.artist_one)",
"album": "$t(entity.album_one)",
@@ -234,25 +250,25 @@
}
},
"error": {
"remotePortWarning": "перезапустить сервер для применения нового порта",
"remotePortWarning": "необходимо перезапустить сервер для применения нового порта",
"systemFontError": "произошла ошибка при попытке получить системные шрифты",
"playbackError": "произошла ошибка при попытке проиграть медиа",
"playbackError": "произошла ошибка при попытке проигрывания медиа",
"endpointNotImplementedError": "запрос {{endpoint}} не реализован для {{serverType}}",
"remotePortError": "произошла ошибка при попытке установить порт удаленного сервера",
"serverRequired": "необходим сервер",
"authenticationFailed": "авторизация завершилась с ошибкой",
"serverRequired": "сервер не выбран",
"authenticationFailed": "не удалось авторизироваться",
"apiRouteError": "невозможно выполнить запрос",
"genericError": "произошла ошибка",
"credentialsRequired": "необходимы учётные данные",
"credentialsRequired": "введите данные для входа",
"sessionExpiredError": "ваш сеанс истёк",
"remoteEnableError": "ошибка произошла при попытке $t(common.enable) удалённый сервер",
"remoteEnableError": "произошла ошибка при попытке $t(common.enable) удалённый сервер",
"localFontAccessDenied": "не получилось получить доступ к шрифтам",
"serverNotSelectedError": "не выбран сервер",
"remoteDisableError": "ошибка произошла при попытке $t(common.disable) удалённый сервер",
"remoteDisableError": "произошла ошибка при попытке $t(common.disable) удалённый сервер",
"mpvRequired": "необходим MPV",
"audioDeviceFetchError": "произошла ошибка с аудиоустройством",
"invalidServer": "недействительный сервер",
"loginRateError": "слишком много попыток входа, пожалуйста, попробуйте ещё раз через несколько секунд",
"loginRateError": "превышено максимальное количество попыток входа, пожалуйста, попробуйте ещё раз через несколько секунд",
"openError": "не удалось открыть файл",
"badAlbum": "вы видите эту страницу из-за того, что эта песня не входит в альбом. скорее всего, вы видите эту ошибку, так как песня находится в корневой директории папки с музыкой. jellyfin группирует треки только по папкам.",
"networkError": "возникла ошибка сети"
@@ -265,48 +281,49 @@
"communityRating": "рейтинг сообщества",
"favorited": "любимый",
"albumArtist": "$t(entity.albumArtist_one)",
"isFavorited": "любимый",
"isFavorited": "любимые",
"bpm": "уд./мин.",
"disc": "диск",
"biography": "биография",
"artist": "$t(entity.artist_one)",
"duration": "длительность",
"fromYear": "из года",
"fromYear": "год",
"criticRating": "рейтинг критиков",
"mostPlayed": "самое воспроизводимое",
"mostPlayed": "слушают чаще всего",
"comment": "комментировать",
"playCount": "кол-во воспроизведений",
"recentlyUpdated": "недавно обновлено",
"playCount": "количество воспроизведений",
"recentlyUpdated": "обновленные недавно",
"channels": "$t(common.channel_other)",
"recentlyPlayed": "недавно проиграно",
"recentlyPlayed": "проигрывались недавно",
"owner": "$t(common.owner)",
"title": "название",
"rating": "рейтинг",
"search": "поиск",
"genre": "$t(entity.genre_one)",
"recentlyAdded": "недавно добавлено",
"recentlyAdded": "недавно добавленные",
"note": "заметка",
"name": "название",
"releaseDate": "дата выхода",
"albumCount": "кол-во $t(entity.album_other)",
"albumCount": "количество $t(entity.album_many)",
"path": "путь",
"isRecentlyPlayed": "недавно проигрывалась",
"isRecentlyPlayed": "недавно проигрывался",
"releaseYear": "год выхода",
"id": "№",
"songCount": "кол-во песен",
"songCount": "количество песен",
"isPublic": "публичный",
"random": "случайный",
"random": "случайно",
"lastPlayed": "последний раз проигрывалась",
"toYear": "до года",
"album": "$t(entity.album_one)",
"trackNumber": "трек"
},
"player": {
"repeat_all": "повтор всех",
"repeat_all": "повторять все",
"stop": "остановить",
"repeat": "повтор",
"repeat": "повторять текущий",
"queue_remove": "удалить выбранное",
"playRandom": "случайные песни",
"playRandom": "играть случайные песни",
"playSimilarSongs": "играть похожие песни",
"skip": "пропустить",
"previous": "предыдущий",
"toggleFullscreenPlayer": "включить полноэкранный режим",
@@ -316,10 +333,10 @@
"shuffle": "перемешать",
"playbackFetchNoResults": "песни не найдены",
"playbackFetchInProgress": "загрузка песен..",
"addNext": "добавить следующий",
"addNext": "воспроизвести следующим",
"playbackSpeed": "скорость воспроизведения",
"playbackFetchCancel": "это занимает некоторое время... закрыть уведомление для отмены",
"play": "прослушать",
"playbackFetchCancel": "пожалуйста, подождите немного... закройте уведомление для отмены",
"play": "играть",
"repeat_off": "повтор выключен",
"pause": "пауза",
"queue_clear": "очистить очередь",
@@ -328,13 +345,14 @@
"queue_moveToTop": "переместить выделенное вниз",
"queue_moveToBottom": "переместить выделенное вверх",
"shuffle_off": "перемешивание выключено",
"addLast": "добавить последний",
"addLast": "воспроизвести после всех",
"mute": "отключить звук",
"skip_forward": "вперёд"
"skip_forward": "вперёд",
"viewQueue": "показать очередь"
},
"page": {
"sidebar": {
"nowPlaying": "Cейчас проигрывается",
"nowPlaying": "сейчас играет",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"tracks": "$t(entity.track_other)",
@@ -354,30 +372,41 @@
"followCurrentLyric": "следовать за текущими словами песни",
"opacity": "непрозрачность",
"lyricSize": "размер слов",
"showLyricProvider": "показать провайдера слов",
"unsynchronized": "несинхронизировано",
"showLyricProvider": "показать источник слов",
"unsynchronized": "не синхронизировано",
"lyricAlignment": "выравнивание слов песни",
"lyricOffset": "задержка слов (мсек)",
"useImageAspectRatio": "использовать соотношение сторон изображения",
"lyricGap": "пробел между словами",
"dynamicIsImage": "включить фоновое изображение",
"dynamicImageBlur": "сила размытия изображения"
},
"upNext": "следующее",
"lyrics": "слова песни",
"related": "схожие"
"upNext": "играет",
"lyrics": "слова",
"related": "похожие",
"visualizer": "визуализатор",
"noLyrics": "слова для песни не найдены"
},
"appMenu": {
"selectServer": "выбрать сервер",
"selectServer": "список серверов",
"version": "версия {{version}}",
"settings": "$t(common.setting_other)",
"manageServers": "настроить список серверов",
"expandSidebar": "развернуть",
"manageServers": "редактировать список серверов",
"expandSidebar": "развернуть боковую панель",
"collapseSidebar": "Скрыть боковую панель",
"openBrowserDevtools": "открыть инструменты разработчика",
"quit": "$t(common.quit)",
"goBack": "назад",
"goForward": "вперёд"
},
"manageServers": {
"title": "сервера",
"serverDetails": "информация о сервере",
"url": "адрес",
"username": "пользователь",
"editServerDetailsTooltip": "изменить настройки сервера",
"removeServer": "удалить сервер"
},
"contextMenu": {
"addToPlaylist": "$t(action.addToPlaylist)",
"addToFavorites": "$t(action.addToFavorites)",
@@ -390,6 +419,7 @@
"removeFromFavorites": "$t(action.removeFromFavorites)",
"addNext": "$t(player.addNext)",
"deselectAll": "$t(action.deselectAll)",
"download": "скачать",
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
@@ -399,11 +429,11 @@
"shareItem": "поделиться"
},
"home": {
"mostPlayed": "наибольшее кол-во воспроизведений",
"mostPlayed": "слушают чаще всего",
"newlyAdded": "недавно добавленные релизы",
"title": "$t(common.home)",
"explore": "изучите вашу медиатеку",
"recentlyPlayed": "недавно прослушано"
"explore": "откройте новое",
"recentlyPlayed": "игралось недавно"
},
"albumDetail": {
"moreFromArtist": "больше от $t(entity.artist_one)",
@@ -420,8 +450,8 @@
},
"genreList": {
"title": "$t(entity.genre_other)",
"showAlbums": "показать $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "показать $t(entity.genre_one) $t(entity.track_other)"
"showAlbums": "показать $t(entity.genre_one) $t(entity.album_many)",
"showTracks": "показать $t(entity.genre_one) $t(entity.track_many)"
},
"trackList": {
"title": "$t(entity.track_other)",
@@ -435,6 +465,9 @@
},
"title": "комманды"
},
"playlist": {
"reorder": "сортировка доступна только по ID"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
@@ -448,7 +481,7 @@
"viewAll": "посмотреть всё",
"appearsOn": "появляется в",
"viewDiscography": "посмотреть дискографию",
"relatedArtists": "похож на $t(entity.artist_other)",
"relatedArtists": "похож на $t(entity.artist_many)",
"viewAllTracks": "посмотреть все $t(entity.track_other)",
"recentReleases": "недавние релизы",
"about": "О {{artist}}",
@@ -464,7 +497,7 @@
"deletePlaylist": {
"title": "удалить $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) успешно удалён",
"input_confirm": "напишите название $t(entity.playlist_one)а для подтверждения"
"input_confirm": "напишите название $t(entity.playlist_few) для подтверждения"
},
"createPlaylist": {
"input_description": "$t(common.description)",
@@ -477,24 +510,24 @@
"addServer": {
"title": "добавить сервер",
"input_username": "пользователь",
"input_url": "url",
"input_url": "адрес",
"input_password": "пароль",
"input_legacyAuthentication": "включить старую авторизацию",
"input_name": "название сервера",
"success": "сервер добавлен успешно",
"success": "сервер успешно добавлен",
"input_savePassword": "сохранить пароль",
"ignoreSsl": "игнорирование ssl ($t(common.restartRequired))",
"ignoreCors": "игнорирование корсетов ($t(common.restartRequired))",
"error_savePassword": "произошла ошибка во время попытки сохранения пароля"
"ignoreSsl": "игнорировать ssl ($t(common.restartRequired))",
"ignoreCors": "игнорировать CORS ($t(common.restartRequired))",
"error_savePassword": "произошла ошибка при сохранении пароля"
},
"addToPlaylist": {
"success": "добавлено: $t(entity.trackWithCount, {\"count\": {{message}} }) в $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "добавить в $t(entity.playlist_one)",
"input_skipDuplicates": "пропустить дубликаты",
"input_skipDuplicates": "не добавлять дубликаты",
"input_playlists": "$t(entity.playlist_other)"
},
"updateServer": {
"title": "обновить сервер",
"title": "обновление сервера",
"success": "сервер успешно обновлён"
},
"queryEditor": {
@@ -512,7 +545,7 @@
"shareItem": {
"success": "ссылка скопирована в буфер обмена (нажмите здесь, чтобы открыть)",
"expireInvalid": "время истечения срока действия должно быть в будущем",
"createFailed": "не удалось создать ссылку для общего доступа (общий доступ включён?)",
"createFailed": "не удалось создать ссылку для общего доступа (проверьте, включен ли общий доступ?)",
"allowDownloading": "разрешить скачивание",
"setExpiration": "установить срок действия",
"description": "описание"
@@ -521,16 +554,22 @@
"setting": {
"accentColor": "цвет акцента",
"accentColor_description": "устанавливает цвет акцента для приложения",
"albumBackground": "фоновое изображение альбомов",
"albumBackground_description": "добавляет фоновое изображение для страниц альбомов, содержащих обложку",
"albumBackgroundBlur": "размытие фонового изображения альбома",
"albumBackgroundBlur_description": "определяет степень размытия фонового изображения на странице альбомов",
"applicationHotkeys": "горячие клавиши приложения",
"crossfadeStyle_description": "выберите вид эффекта crossfade для аудиоплеера",
"customCssEnable": "использовать кастомные css",
"customCssEnable_description": "разрешить использование кастомных css.",
"enableRemote_description": "включает сервер удалённого управления для управления воспроизведением с помощью других устройств",
"fontType_optionSystem": "системный",
"mpvExecutablePath_description": "укажите папку, в которой находится исполняющий файл аудиоплеера MPV. если оставить пустым, будет использоваться путь по умолчанию",
"crossfadeStyle": "Вид эффекта crossfade",
"crossfadeStyle": "вид эффекта crossfade",
"fontType_optionBuiltIn": "встроенный",
"disableLibraryUpdateOnStartup": "Отключить проверку новых версий при запуске приложения",
"minimizeToTray_description": "Сворачивать приложение в панель уведомлений",
"audioPlayer_description": "Укажите - какой аудиоплеер использовать для воспроизведения",
"disableLibraryUpdateOnStartup": "отключить проверку новых версий при запуске приложения",
"minimizeToTray_description": "сворачивать приложение в панель уведомлений",
"audioPlayer_description": "укажите, какой аудиоплеер использовать для воспроизведения",
"disableAutomaticUpdates": "отключить проверку обновлений",
"exitToTray_description": "При закрытии приложения - оно останется в панели уведомлений",
"fontType_optionCustom": "пользовательский",
@@ -544,7 +583,7 @@
"crossfadeDuration": "Длительность эффекта crossfade",
"audioPlayer": "Аудиоплеер",
"minimizeToTray": "сворачивать в панель уведомлений",
"font_description": "Выберите - какой шрифт использовать в приложении",
"font_description": "Выберите, какой шрифт использовать в приложении",
"remoteUsername": "имя пользователя для доступа к серверу удалённого управления",
"buttonSize_description": "размер кнопок в панели управления воспроизведением",
"clearCache": "очистить кэш браузера",
@@ -554,11 +593,11 @@
"buttonSize": "размер кнопок панели управления воспроизведением",
"hotkey_volumeDown": "уменьшить громкость",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"theme_description": "устанавливает тему, которая будет использоваться приложением",
"theme_description": "устанавливает тему, которая будет использоваться в приложении",
"passwordStore": "хранилище паролей/секретов",
"sidebarPlaylistList": "список плейлистов в боковой панели",
"windowBarStyle_description": "выберите стиль заголовка окна",
"followLyric": "следовать тексту трека",
"followLyric": "следовать за текстом трека",
"volumeWheelStep": "шаг регулировки громкости колёсиком мыши",
"windowBarStyle": "стиль заголовка окна",
"hotkey_zoomOut": "уменьшить масштаб",
@@ -567,7 +606,7 @@
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"clearQueryCache_description": "\"мягкая очистка\" feishin. при выполнении обновляются плейлисты, метаданные треков, но сохранённые тексты треков сбрасываются. настройки, учётные данные и кэшированные изображения сохраняются",
"clearQueryCache_description": "так называемая \"мягкая очистка\" feishin: обновляются плейлисты, метаданные треков, но сохранённые тексты треков сбрасываются. настройки, учётные данные и кэшированные изображения сохраняются",
"hotkey_favoriteCurrentSong": "добавить $t(common.currentSong) в избранное",
"genreBehavior": "поведения страницы жанров",
"globalMediaHotkeys": "глобальные мультимедийные горячие клавиши",
@@ -576,11 +615,11 @@
"hotkey_globalSearch": "глобальный поиск",
"hotkey_playbackNext": "следующий трек",
"hotkey_playbackPause": "пауза",
"hotkey_playbackPlay": "прослушать",
"hotkey_playbackPlayPause": "прослушать / пауза",
"hotkey_playbackPlay": "играть",
"hotkey_playbackPlayPause": "играть / пауза",
"hotkey_playbackPrevious": "предыдущий трек",
"hotkey_playbackStop": "остановить",
"hotkey_rate0": "очистить оценку",
"hotkey_rate0": "убрать оценку",
"hotkey_rate1": "оценить в 1 звезду",
"hotkey_rate2": "оценить в 2 звезды",
"hotkey_rate3": "оценить в 3 звезды",
@@ -609,6 +648,8 @@
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)",
"playerAlbumArtResolution_description": "разрешение большой версии обложки альбома в проигрывателе. при большем разрешении она выглядит более четкой, но может замедлить загрузку. по умолчанию равно 0 - устанавливает разрешение автоматически",
"playerbarOpenDrawer": "полноэкранный переключатель по панели проигрывателя",
"playerbarOpenDrawer_description": "позволяет перейти в полноэкранный режим воспроизведения нажатием на панель проигрывателя",
"remotePort": "порт сервера удалённого управления",
"remotePort_description": "устанавливает порт для сервера удалённого управления",
"replayGainClipping": "{{ReplayGain}} клиппинг",
@@ -622,11 +663,21 @@
"showSkipButtons": "показывать кнопки перемотки",
"showSkipButtons_description": "показывать или скрывать кнопки перемотки на панели управления воспроизведением",
"sidebarPlaylistList_description": "показать или скрыть список плейлистов на боковой панели",
"sidePlayQueueStyle": "вид отображения боковой очереди",
"sidePlayQueueStyle_description": "определяет вид отображения боковой очереди",
"sidePlayQueueStyle_optionAttached": "закрепленная",
"sidePlayQueueStyle_optionDetached": "плавающая",
"skipDuration": "время перемотки",
"skipDuration_description": "задает время перемотки при использовании кнопок перемотки на панели проигрывателя",
"useSystemTheme": "использовать тему системы",
"themeLight": "тема (светлая)",
"themeLight_description": "устанавливает светлую тему приложения",
"transcodeNote": "эффект применяется после 1 (для веб) - 2 (для mpv) песни",
"transcode": "включить транскодинг",
"transcode_description": "активирует транскодинг в другие форматы",
"transcodeBitrate": "битрейт транскодинга",
"transcodeBitrate_description": "выберите битрейт транскодинга. 0 - автоматическое определение сервером",
"transcodeFormat": "формат транкодинга",
"useSystemTheme_description": "использует тему, заданную в системе (светлую/тёмную)",
"zoom": "процент масштабирования",
"zoom_description": "устанавливает процент масштабирования приложения",
@@ -634,13 +685,15 @@
"genreBehavior_description": "определяет, что отобразится при открытии на жанр — список треков или альбомов",
"globalMediaHotkeys_description": "включить или отключить использование системных мультимедийных горячих клавиш для управления воспроизведением",
"homeConfiguration_description": "позволяет настроить видимость и порядок элементов на домашней странице",
"homeFeature": "улучшенная карусель на главной",
"homeFeature_description": "определяет, показывать ли улучшенную карусель на главной странице",
"hotkey_toggleQueue": "показать/скрыть очередь воспроизведения",
"imageAspectRatio": "использовать оригинальное соотношение сторон обложки",
"imageAspectRatio_description": "если эта опция включена, обложки будут отображаться в соответствии с их собственным соотношением сторон. для обложек не 1:1 оставшееся пространство будет пустым",
"minimumScrobblePercentage": "минимальное время для скробблинга (в процентах)",
"playbackStyle": "стиль воспроизведения",
"playerAlbumArtResolution": "разрешение обложки альбома",
"remotePassword_description": "задает пароль для сервера удалённого управления. По умолчанию эти учетные данные передаются небезопасным способом, поэтому следует использовать уникальный пароль, который вам не важен",
"remotePassword_description": "задает пароль для сервера удалённого управления. По умолчанию эти учетные данные передаются небезопасным способом, поэтому следует использовать уникальный пароль, который вам неважен",
"replayGainClipping_description": "Предотвращение клиппинга, вызванного {{ReplayGain}}, путём автоматического снижения усиления",
"replayGainFallback_description": "усиление в db для применения, если у файла нет тегов {{ReplayGain}}",
"replayGainMode_description": "регулировать усиление громкости в соответствии со значениями {{ReplayGain}}, хранящимися в метаданных файла",
@@ -658,8 +711,10 @@
"startMinimized": "запуск в свёрнутом режиме",
"themeDark_description": "устанавливает тёмную тему приложения",
"hotkey_volumeMute": "отключить звук",
"clearCache_description": "\"жесткая очистка\" feishin. кроме очистки кэша feishin, также очищает кэш браузера (сохранённые картинки и другие ресурсы). учётные данные и настройки сохраняются",
"clearCache_description": "\"жесткая очистка\" feishin: кроме очистки кэша feishin, также очищает кэш браузера (сохранённые картинки и другие ресурсы). учётные данные и настройки сохраняются",
"clearCacheSuccess": "кэш успешно удалён",
"contextMenu": "конфигурация контекстного меню (нажатие правой кнопкой мыши)",
"contextMenu_description": "позволяет скрыть элементы, отображаемые в меню, появляющемся при нажатии правой кнопки мыши на элемент. все, что не отмечено, будет скрыто",
"customFontPath": "путь к пользовательскому шрифту",
"customFontPath_description": "укажите путь к пользовательскому шрифту, который будет использоваться в приложении",
"externalLinks_description": "включает отображение внешних ссылок (Last.fm, MusicBrainz) на страницах альбомов и артистов",
@@ -681,6 +736,10 @@
"scrobble_description": "скробблинг треков на вашем медиасервере",
"startMinimized_description": "запуск приложения в области уведомлений",
"volumeWheelStep_description": "количество громкости, изменяемое при прокрутке колёсика мыши над ползунком громкости",
"volumeWidth": "ширина слайдера звука",
"volumeWidth_description": "ширина слайдера звука (в px)",
"webAudio": "использовать веб аудио",
"webAudio_description": "использование веб аудио. включение активирует продвинутые возможности (например, replaygain). отключите, если вам это не нужно",
"discordRichPresence": "состояние профиля {{discord}}",
"discordApplicationId": "{{discord}} application id",
"discordApplicationId_description": "application id приложения {{discord}} которое будет отображаться в статусе профиля (по умолчанию {{defaultId}})",
@@ -688,9 +747,13 @@
"discordIdleStatus_description": "если включено, то обновляет статус, когда пользователь бездействует",
"discordUpdateInterval": "интервал обновления статуса профиля {{discord}}",
"discordUpdateInterval_description": "время в секундах между каждым обновлением (минимум 15 секунд)",
"doubleClickBehavior": "добавить в очередь все найденные треки при двойном клике",
"doubleClickBehavior_description": "есть включено: все найденные в поиске треки будут добавлены в очередь при двойном клике (иначе - только выбранный)",
"lyricOffset_description": "Смещение появления текста треков на указанное количество миллисекунд",
"skipPlaylistPage": "пропустить страницу плейлиста",
"applicationHotkeys_description": "настройка горячих клавиш приложения. включите чекбокс, чтобы сделать горячую клавишу глобальной (только для ПК)",
"skipPlaylistPage": "пропускать страницу плейлиста",
"applicationHotkeys_description": "настройка горячих клавиш приложения. поставьте галочку, чтобы сделать горячую клавишу глобальной (только для ПК)",
"artistConfiguration": "конфигурация страницы альбомов исполнителей",
"artistConfiguration_description": "позволяет настроить видимость и порядок элементов на странице альбомов исполнителей",
"fontType_description": "встроенный позволяет выбрать один из шрифтов, предоставляемых Feishin. системный позволяет выбрать любой шрифт, предоставляемый вашей операционной системой. пользовательский позволяет выбрать свой собственный шрифт",
"discordRichPresence_description": "включить статус воспроизведения в статус профиля в {{discord}}. Ключи изображений: {{icon}}, {{playing}} и {{paused}} ",
"lyricOffset": "синхронизация текста треков (мс)"
+5 -1
View File
@@ -209,7 +209,11 @@
"moveToBottom": "idi na dno",
"setRating": "oceni",
"toggleSmartPlaylistEditor": "pokreni $t(entity.smartPlaylist) editor",
"removeFromFavorites": "ukloni iz $t(entity.favorite_other)"
"removeFromFavorites": "ukloni iz $t(entity.favorite_other)",
"openIn": {
"lastfm": "Otvori u Last.fm",
"musicbrainz": "Otvori u MusicBrainz"
}
},
"common": {
"backward": "nazad",
+767
View File
@@ -0,0 +1,767 @@
{
"action": {
"addToFavorites": "$ t இல் சேர்க்கவும் (entity.foavorite_other)",
"clearQueue": "தெளிவான வரிசை",
"goToPage": "பக்கத்திற்குச் செல்லுங்கள்",
"moveToBottom": "கீழே செல்லுங்கள்",
"moveToTop": "மேலே செல்லுங்கள்",
"refresh": "$ t (காமன்.ரெஃப்ரெச்)",
"removeFromFavorites": "$ t இலிருந்து அகற்று (entity.foavorite_other)",
"removeFromPlaylist": "$ t இலிருந்து அகற்று (entity.playlist_one)",
"removeFromQueue": "வரிசையிலிருந்து அகற்று",
"setRating": "மதிப்பீட்டை அமைக்கவும்",
"toggleSmartPlaylistEditor": "மாற்று $ t (entity.smartplaylist) ஆசிரியர்",
"viewPlaylists": "$ t (entity.playlist_other) காண்க",
"addToPlaylist": "$ t இல் சேர்க்கவும் (entity.playlist_one)",
"createPlaylist": "$ t ஐ உருவாக்கவும் (entity.playlist_one)",
"deletePlaylist": "$ t (entity.playlist_one) ஐ நீக்கு",
"deselectAll": "அனைத்தையும் தேர்வு செய்யுங்கள்",
"editPlaylist": "திருத்து $ t (entity.playlist_one)",
"moveToNext": "அடுத்து செல்லுங்கள்",
"openIn": {
"lastfm": "Last.fm இல் திறந்திருக்கும்",
"musicbrainz": "மியூசிக் பிரைன்ச் திறந்திருக்கும்"
}
},
"common": {
"description": "விவரம்",
"minimize": "குறைக்கவும்",
"modified": "மாற்றியமைக்கப்பட்ட",
"noResultsFromQuery": "வினவல் எந்த முடிவுகளும் திரும்பவில்லை",
"note": "குறிப்பு",
"ok": "சரி",
"configure": "உள்ளமைக்கவும்",
"confirm": "உறுதிப்படுத்தவும்",
"create": "உருவாக்கு",
"currentSong": "தற்போதைய $ t (entity.track_one)",
"decrease": "குறைவு",
"action_one": "செயல்",
"action_other": "செயல்கள்",
"add": "கூட்டு",
"albumGain": "ஆல்பம் ஆதாயம்",
"albumPeak": "ஆல்பம் உச்சம்",
"areYouSure": "நீங்கள் உறுதியாக இருக்கிறீர்களா?",
"ascending": "ஏறுதல்",
"backward": "பின்னோக்கு",
"biography": "சுயசரிதை",
"bitrate": "பிட்ரேட்",
"bpm": "பிபிஎம்",
"cancel": "ரத்துசெய்",
"center": "நடுவண்",
"channel_one": "வாய்க்கால்",
"channel_other": "சேனல்கள்",
"clear": "தெளிவான",
"close": "மூடு",
"codec": "புரிப்பு",
"collapse": "சரிவு",
"comingSoon": "விரைவில் வருகிறது…",
"delete": "நீக்கு",
"descending": "இறங்கு",
"disable": "முடக்கு",
"disc": "வட்டு",
"dismiss": "தள்ளுபடி",
"duration": "காலம்",
"edit": "தொகு",
"enable": "இயக்கு",
"saveAs": "என சேமி",
"expand": "விரிவாக்கு",
"favorite": "பிடித்த",
"filter_one": "வடிப்பி",
"filter_other": "வடிப்பான்கள்",
"filters": "வடிப்பான்கள்",
"forceRestartRequired": "மாற்றங்களைப் பயன்படுத்த மறுதொடக்கம்… மறுதொடக்கம் செய்ய அறிவிப்பை மூடு",
"forward": "முன்னோக்கி",
"gap": "இடைவெளி",
"home": "வீடு",
"increase": "அதிகரிப்பு",
"left": "இடது",
"limit": "வரம்பு",
"manage": "நிர்வகிக்கவும்",
"maximize": "அதிகரிக்கவும்",
"menu": "பட்டியல்",
"mbid": "மியூசிக் பிரேன்ச் ஐடி",
"name": "பெயர்",
"no": "இல்லை",
"none": "எதுவுமில்லை",
"owner": "உரிமையாளர்",
"path": "பாதை",
"playerMustBePaused": "வீரர் இடைநிறுத்தப்பட வேண்டும்",
"preview": "முன்னோட்டம்",
"previousSong": "முந்தைய $ t (entity.track_one)",
"quit": "வெளியேறு",
"random": "சீரற்ற",
"rating": "செயல்வரம்பு",
"refresh": "புதுப்பிப்பு",
"reload": "ஏற்றவும்",
"reset": "மீட்டமை",
"resetToDefault": "இயல்புநிலைக்கு மீட்டமைக்கவும்",
"restartRequired": "மறுதொடக்கம் தேவை",
"right": "வலது",
"save": "சேமி",
"saveAndReplace": "சேமித்து மாற்றவும்",
"search": "தேடல்",
"setting": "அமைத்தல்",
"share": "பங்கு",
"size": "அளவு",
"sortOrder": "ஒழுங்கு",
"unknown": "தெரியவில்லை",
"version": "பதிப்பு",
"year": "ஆண்டு",
"yes": "ஆம்",
"title": "தலைப்பு",
"trackNumber": "மின்தடம்",
"trackGain": "தடமறிதல்",
"trackPeak": "ட்ராக் பீக்",
"translation": "மொழிபெயர்ப்பு"
},
"entity": {
"folderWithCount_one": "{{count}} கோப்புறை",
"folderWithCount_other": "{{count}} கோப்புறைகள்",
"genre_one": "வகை",
"genre_other": "வகைகள்",
"genreWithCount_one": "{{count}} வகை",
"genreWithCount_other": "{{count}} வகைகள்",
"album_one": "ஆல்பம்",
"album_other": "ஆல்பம்",
"albumArtist_one": "ஆல்பம் கலைஞர்",
"albumArtist_other": "ஆல்பம் கலைஞர்கள்",
"albumArtistCount_one": "{{count}} ஆல்பம் கலைஞர்",
"albumArtistCount_other": "{{count}} ஆல்பம் கலைஞர்கள்",
"albumWithCount_one": "{{count}} ஆல்பம்",
"albumWithCount_other": "{{count}} ஆல்பங்கள்",
"artist_one": "கலைஞர்",
"artist_other": "கலைஞர்கள்",
"artistWithCount_one": "{{count}} கலைஞர்",
"artistWithCount_other": "{{count}} கலைஞர்கள்",
"favorite_one": "பிடித்த",
"favorite_other": "பிடித்தவை",
"folder_one": "கோப்புறை",
"folder_other": "கோப்புறைகள்",
"playlist_one": "பிளேலிச்ட்",
"playlist_other": "பிளேலிச்ட்கள்",
"play_one": "{{count}} play",
"play_other": "{{count}} நாடகங்கள்",
"playlistWithCount_one": "{{count}} பிளேலிச்ட்",
"playlistWithCount_other": "{{count}} பிளேலிச்ட்கள்",
"smartPlaylist": "அறிவுள்ள $ t (entity.playlist_one)",
"track_one": "மின்தடம்",
"track_other": "தடங்கள்",
"song_one": "பாடல்",
"song_other": "பாடல்கள்",
"trackWithCount_one": "{{count}} டிராக்",
"trackWithCount_other": "{{count}} தடங்கள்"
},
"error": {
"mpvRequired": "MPV தேவை",
"remotePortError": "தொலை சேவையக துறைமுகத்தை அமைக்க முயற்சிக்கும்போது பிழை ஏற்பட்டது",
"remotePortWarning": "புதிய துறைமுகத்தைப் பயன்படுத்த சேவையகத்தை மறுதொடக்கம் செய்யுங்கள்",
"serverNotSelectedError": "சேவையகம் எதுவும் தேர்ந்தெடுக்கப்படவில்லை",
"serverRequired": "சேவையகம் தேவை",
"remoteEnableError": "தொலைநிலை சேவையகத்தை $ t (பொதுவானது) முயற்சிக்கும்போது பிழை ஏற்பட்டது",
"apiRouteError": "பாதை கோரிக்கை செய்ய முடியவில்லை",
"audioDeviceFetchError": "ஆடியோ சாதனங்களைப் பெற முயற்சிக்கும்போது பிழை ஏற்பட்டது",
"authenticationFailed": "ஏற்பு தோல்வியடைந்தது",
"badAlbum": "இந்த பாடல் ஆல்பத்தின் பகுதியாக இல்லாததால் இந்தப் பக்கத்தைப் பார்க்கிறீர்கள். உங்கள் இசை கோப்புறையின் மேல் மட்டத்தில் ஒரு பாடல் இருந்தால் இந்த சிக்கலைப் பார்க்கிறீர்கள். செல்லிஃபின் ஒரு கோப்புறையில் இருந்தால் தடங்களை மட்டுமே குழுக்கள்.",
"credentialsRequired": "நற்சான்றிதழ்கள் தேவை",
"endpointNotImplementedError": "Endpoint {{endpoint}} {{serverType} க்கு க்கு செயல்படுத்தப்படவில்லை",
"genericError": "பிழை ஏற்பட்டது",
"invalidServer": "தவறான சேவையகம்",
"localFontAccessDenied": "உள்ளக எழுத்துருக்களுக்கு மறுக்கப்பட்டது",
"loginRateError": "பல உள்நுழைவு முயற்சிகள், தயவுசெய்து சில நொடிகளில் மீண்டும் முயற்சிக்கவும்",
"networkError": "பிணைய பிழை ஏற்பட்டது",
"openError": "கோப்பைத் திறக்க முடியவில்லை",
"playbackError": "ஊடகங்களை விளையாட முயற்சிக்கும்போது பிழை ஏற்பட்டது",
"remoteDisableError": "தொலைநிலை சேவையகத்தை $ t (பொதுவானது. குறைக்கக்கூடிய) முயற்சிக்கும்போது பிழை ஏற்பட்டது",
"sessionExpiredError": "உங்கள் அமர்வு காலாவதியானது",
"systemFontError": "கணினி எழுத்துருக்களைப் பெற முயற்சிக்கும்போது பிழை ஏற்பட்டது"
},
"filter": {
"albumArtist": "$ t (entity.albumartist_one)",
"albumCount": "$ t (entity.album_other) எண்ணிக்கை",
"artist": "$ t (entity.artist_one)",
"biography": "சுயசரிதை",
"bitrate": "பிட்ரேட்",
"bpm": "பிபிஎம்",
"channels": "$ t (common.channel_other)",
"comment": "கருத்து",
"communityRating": "சமூக மதிப்பீடு",
"path": "பாதை",
"playCount": "விளையாட்டு எண்ணிக்கை",
"random": "சீரற்ற",
"rating": "செயல்வரம்பு",
"album": "$ t (entity.album_one)",
"criticRating": "விமர்சகர் மதிப்பீடு",
"dateAdded": "தேதி சேர்க்கப்பட்டது",
"disc": "வட்டு",
"duration": "காலம்",
"favorited": "பிடித்தது",
"fromYear": "ஆண்டு முதல்",
"genre": "$ t (entity.genre_one)",
"id": "ஐடி",
"isCompilation": "தொகுப்பு",
"isFavorited": "பிடித்தது",
"isPublic": "பொது",
"isRated": "மதிப்பிடப்படுகிறது",
"isRecentlyPlayed": "அண்மைக் காலத்தில் விளையாடியது",
"lastPlayed": "கடைசியாக விளையாடியது",
"mostPlayed": "அதிகம் விளையாடியது",
"name": "பெயர்",
"note": "குறிப்பு",
"owner": "$ t (பொதுவானவர்)",
"recentlyAdded": "அண்மைக் காலத்தில் சேர்க்கப்பட்டது",
"recentlyPlayed": "அண்மைக் காலத்தில் விளையாடியது",
"recentlyUpdated": "அண்மைக் காலத்தில் புதுப்பிக்கப்பட்டது",
"releaseDate": "வெளியீட்டு தேதி",
"releaseYear": "வெளியீட்டு ஆண்டு",
"search": "தேடல்",
"songCount": "பாடல் எண்ணிக்கை",
"title": "தலைப்பு",
"toYear": "ஆண்டு",
"trackNumber": "மின்தடம்"
},
"form": {
"addServer": {
"input_password": "கடவுச்சொல்",
"error_savePassword": "கடவுச்சொல்லை சேமிக்க முயற்சிக்கும்போது பிழை ஏற்பட்டது",
"ignoreCors": "CORS ஐ புறக்கணிக்கவும் ($ t (Common.RestartRequired))",
"ignoreSsl": "SSL ஐ புறக்கணிக்கவும் ($ t (பொதுவானது.",
"input_legacyAuthentication": "மரபு அங்கீகாரத்தை இயக்கவும்",
"input_name": "சேவையக பெயர்",
"input_savePassword": "கடவுச்சொல்லைச் சேமிக்கவும்",
"input_url": "முகவரி",
"input_username": "பயனர்பெயர்",
"success": "சேவையகம் வெற்றிகரமாக சேர்க்கப்பட்டது",
"title": "சேவையகத்தைச் சேர்க்கவும்"
},
"deletePlaylist": {
"input_confirm": "உறுதிப்படுத்த $ t (entity.playlist_one) பெயரைத் தட்டச்சு செய்க",
"success": "$ t (entity.playlist_one) வெற்றிகரமாக நீக்கப்பட்டது",
"title": "$ t (entity.playlist_one) ஐ நீக்கு"
},
"editPlaylist": {
"title": "திருத்து $ t (entity.playlist_one)",
"publicJellyfinNote": "சில காரணங்களால் செல்லிஃபின் ஒரு பிளேலிச்ட் பொதுவில் இல்லையா என்பதை அம்பலப்படுத்தவில்லை. இது பொதுவில் இருக்க விரும்பினால், தயவுசெய்து பின்வரும் உள்ளீட்டைத் தேர்ந்தெடுக்கவும்",
"success": "$ t (entity.playlist_one) வெற்றிகரமாக புதுப்பிக்கப்பட்டது"
},
"lyricSearch": {
"input_artist": "$ t (entity.artist_one)",
"input_name": "$ t (common.name)",
"title": "பாடல் தேடல்"
},
"queryEditor": {
"input_optionMatchAll": "அனைத்தையும் பொருத்துங்கள்",
"input_optionMatchAny": "எந்த பொருத்தவும்"
},
"shareItem": {
"description": "விவரம்",
"setExpiration": "காலாவதியை அமைக்கவும்",
"expireInvalid": "காலாவதி எதிர்காலத்தில் இருக்க வேண்டும்",
"allowDownloading": "பதிவிறக்க அனுமதிக்கவும்",
"success": "இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்ட இணைப்பைப் பகிரவும் (அல்லது திறக்க இங்கே சொடுக்கு செய்க)",
"createFailed": "பங்கை உருவாக்கத் தவறிவிட்டது (பகிர்வு இயக்கப்பட்டதா?)"
},
"createPlaylist": {
"success": "$ t (entity.playlist_one) வெற்றிகரமாக உருவாக்கப்பட்டது",
"title": "$ t (entity.playlist_one) ஐ உருவாக்கவும்",
"input_description": "$ t (common.description)",
"input_name": "$ t (common.name)",
"input_owner": "$ t (பொதுவானவர்)",
"input_public": "பொது"
},
"addToPlaylist": {
"input_playlists": "$ t (entity.playlist_other)",
"input_skipDuplicates": "நகல்களைத் தவிர்க்கவும்",
"success": "$ t (entity.trackwithCount, {\"count\": {{message}}}) $ t (entity.playlistwithCount, {\"count\": {{numOfPlaylists}}})",
"title": "$ t இல் சேர்க்கவும் (entity.playlist_one)"
},
"updateServer": {
"success": "சேவையகம் வெற்றிகரமாக புதுப்பிக்கப்பட்டது",
"title": "புதுப்பிப்பு சேவையகம்"
}
},
"page": {
"albumArtistDetail": {
"about": "{{artist}} பற்றி",
"appearsOn": "தோன்றும்",
"recentReleases": "அண்மைக் கால வெளியீடுகள்",
"viewDiscography": "டிச்கோகிராஃபி காண்க",
"topSongs": "சிறந்த பாடல்கள்",
"viewAllTracks": "அனைத்தையும் காண்க (entity.track_other)",
"relatedArtists": "தொடர்புடைய $ t (entity.artist_other)",
"topSongsFrom": "{{title}} இலிருந்து சிறந்த பாடல்கள்",
"viewAll": "அனைத்தையும் காண்க"
},
"appMenu": {
"goBack": "திரும்பிச் செல்லுங்கள்",
"collapseSidebar": "பக்கப்பட்டி சரிவு",
"expandSidebar": "பக்கப்பட்டியை விரிவாக்குங்கள்",
"goForward": "முன்னோக்கிச் செல்லுங்கள்",
"manageServers": "சேவையகங்களை நிர்வகிக்கவும்",
"openBrowserDevtools": "திறந்த உலாவி தேவ்டூல்கள்",
"quit": "$ t (common.quit)",
"selectServer": "சேவையகத்தைத் தேர்ந்தெடுக்கவும்",
"settings": "$ t (common.setting_other)",
"version": "பதிப்பு {{version}}"
},
"manageServers": {
"url": "முகவரி",
"title": "சேவையகங்களை நிர்வகிக்கவும்",
"serverDetails": "சேவையக விவரங்கள்",
"username": "பயனர்பெயர்",
"editServerDetailsTooltip": "சேவையக விவரங்களைத் திருத்தவும்",
"removeServer": "சேவையகத்தை அகற்று"
},
"contextMenu": {
"addNext": "$ t (player.addnext)",
"deletePlaylist": "$ t (action.deleteplaylist)",
"deselectAll": "$ t (action.deselectall)",
"download": "பதிவிறக்கம்",
"moveToBottom": "$ t (action.movetobottom)",
"moveToTop": "$ t (action.movetotop)",
"numberSelected": "{{count}} தேர்ந்தெடுக்கப்பட்டது",
"playSimilarSongs": "$ t (player.playsimilarsongs)",
"removeFromFavorites": "$ t (action.removefromfoavites)",
"removeFromPlaylist": "$ t (action.RemoveFrollayList)",
"removeFromQueue": "$ t (action.RemoveFromQueue)",
"setRating": "$ t (action.setrating)",
"playShuffled": "$ t (player.shuffle)",
"addFavorite": "$ t (action.addtofoaverites)",
"addLast": "$ t (player.addlast)",
"moveToNext": "$ t (action.movetonext)",
"play": "$ t (player.play)",
"shareItem": "உருப்படியைப் பகிரவும்",
"showDetails": "தகவலைப் பெறுங்கள்",
"addToFavorites": "$ t (action.addtofoaverites)",
"addToPlaylist": "$ t (action.addtoplailist)",
"createPlaylist": "$ t (action.createplaylist)"
},
"fullscreenPlayer": {
"config": {
"followCurrentLyric": "தற்போதைய பாடலைப் பின்பற்றுங்கள்",
"lyricAlignment": "பாடல் சீரமைப்பு",
"lyricOffset": "பாடல் ஆஃப்செட் (எம்.எச்)",
"synchronized": "ஒத்திசைக்கப்பட்டது",
"dynamicBackground": "மாறும் பின்னணி",
"dynamicImageBlur": "பட மங்கலான அளவு",
"dynamicIsImage": "பின்னணி படத்தை இயக்கவும்",
"lyricGap": "பாடல் இடைவெளி",
"lyricSize": "பாடல் அளவு",
"opacity": "ஒளிபுகாநிலை",
"showLyricMatch": "பாடல் போட்டியைக் காட்டு",
"showLyricProvider": "பாடல் வழங்குநரைக் காட்டு",
"unsynchronized": "ஒத்திசைக்கப்படாதது",
"useImageAspectRatio": "பட விகித விகிதத்தைப் பயன்படுத்தவும்"
},
"upNext": "அடுத்து",
"visualizer": "காட்சிப்படுத்தல்",
"noLyrics": "பாடல் வரிகள் இல்லை",
"lyrics": "பாடல்",
"related": "தொடர்புடைய"
},
"genreList": {
"showAlbums": "$ t (entity.genre_one) $ t (entity.album_other)",
"showTracks": "$ t (entity.genre_one) $ t (entity.track_other)",
"title": "$ t (entity.genre_other)"
},
"globalSearch": {
"commands": {
"goToPage": "பக்கத்திற்குச் செல்லுங்கள்",
"searchFor": "{{query}} ஐத் தேடுங்கள்",
"serverCommands": "சேவையக கட்டளைகள்"
},
"title": "கட்டளைகள்"
},
"home": {
"explore": "உங்கள் நூலகத்திலிருந்து ஆராயுங்கள்",
"mostPlayed": "அதிகம் விளையாடியது",
"newlyAdded": "புதிதாக சேர்க்கப்பட்ட வெளியீடுகள்",
"recentlyPlayed": "அண்மைக் காலத்தில் விளையாடியது",
"title": "$ t (காமன்.ஓம்)"
},
"itemDetail": {
"copyPath": "இடைநிலைப்பலகைக்கு பாதையை நகலெடுக்கவும்",
"copiedPath": "பாதை வெற்றிகரமாக நகலெடுக்கப்பட்டது",
"openFile": "கோப்பு மேலாளரில் தடத்தைக் காட்டு"
},
"playlist": {
"reorder": "ஐடியால் வரிசைப்படுத்தும்போது மட்டுமே மறுசீரமைப்பு இயக்கப்பட்டது"
},
"playlistList": {
"title": "$ t (entity.playlist_other)"
},
"setting": {
"advanced": "மேம்பட்ட",
"generalTab": "பொது",
"hotkeysTab": "ஆட்கீச்",
"playbackTab": "பின்னணி",
"windowTab": "சாளரம்"
},
"sidebar": {
"folders": "$ t (entity.folder_other)",
"genres": "$ t (entity.genre_other)",
"home": "$ t (காமன்.ஓம்)",
"nowPlaying": "இப்போது விளையாடுகிறது",
"playlists": "$ t (entity.playlist_other)",
"search": "$ t (பொதுவானது. தேடல்)",
"settings": "$ t (common.setting_other)",
"albumArtists": "$ t (entity.albumartist_other)",
"albums": "$ t (entity.album_other)",
"artists": "$ t (entity.artist_other)",
"shared": "பகிரப்பட்ட $ t (entity.playlist_other)",
"tracks": "$ t (entity.track_other)"
},
"trackList": {
"title": "$ t (entity.track_other)",
"genreTracks": "\"{{genre}}\" $ t (entity.track_other)",
"artistTracks": "{{artist}}"
},
"albumArtistList": {
"title": "$ t (entity.albumartist_other)"
},
"albumDetail": {
"moreFromArtist": "இந்த $ t (entity.artist_one) இலிருந்து மேலும்",
"moreFromGeneric": "{{item} இருந்து இலிருந்து மேலும்",
"released": "வெளியிடப்பட்டது"
},
"albumList": {
"artistAlbums": "ஆல்பங்கள் {{artist}}",
"genreAlbums": "\"{{genre}}\" $ t (entity.album_other)",
"title": "$ t (entity.album_other)"
}
},
"player": {
"addLast": "கடைசியாக சேர்க்கவும்",
"addNext": "அடுத்து சேர்க்கவும்",
"favorite": "பிடித்த",
"mute": "முடக்கு",
"muted": "முடக்கிய",
"next": "அடுத்தது",
"play": "விளையாடுங்கள்",
"playSimilarSongs": "ஒத்த பாடல்களை வாசிக்கவும்",
"previous": "முந்தைய",
"queue_clear": "தெளிவான வரிசை",
"queue_remove": "தேர்ந்தெடுக்கப்பட்டதை அகற்று",
"repeat": "மீண்டும்",
"repeat_all": "அனைத்தையும் மீண்டும் செய்யவும்",
"repeat_off": "முடக்கப்பட்டதை மீண்டும் செய்யவும்",
"shuffle": "விளையாட்டு மாற்றப்பட்டது",
"shuffle_off": "கலக்கு முடக்கப்பட்டது",
"skip": "தவிர்",
"playbackFetchCancel": "இது சிறிது நேரம் ஆகும்… ரத்து செய்ய அறிவிப்பை மூடு",
"playbackFetchInProgress": "பாடல்களை ஏற்றுகிறது…",
"playbackFetchNoResults": "பாடல்கள் எதுவும் கிடைக்கவில்லை",
"playbackSpeed": "பிளேபேக் விரைவு",
"playRandom": "சீரற்ற முறையில் விளையாடுங்கள்",
"queue_moveToBottom": "மேலே தேர்ந்தெடுக்கப்பட்ட நகர்த்து",
"queue_moveToTop": "தேர்ந்தெடுக்கப்பட்டதை கீழே நகர்த்தவும்",
"skip_back": "பின்னோக்கி தவிர்க்கவும்",
"skip_forward": "முன்னோக்கி தவிர்க்கவும்",
"stop": "நிறுத்து",
"toggleFullscreenPlayer": "முழுத்திரை பிளேயரை மாற்றவும்",
"unfavorite": "மாறாத",
"pause": "இடைநிறுத்தம்",
"viewQueue": "வரிசையைக் காண்க"
},
"setting": {
"accentColor": "உச்சரிப்பு நிறம்",
"accentColor_description": "பயன்பாட்டிற்கான உச்சரிப்பு வண்ணத்தை அமைக்கிறது",
"albumBackground": "ஆல்பம் பின்னணி படம்",
"applicationHotkeys": "பயன்பாட்டு ஆட்கீச்",
"applicationHotkeys_description": "பயன்பாட்டு ஆட்கீசை உள்ளமைக்கவும். உலகளாவிய ஆட்ச்கியாக அமைக்க தேர்வுப்பெட்டியை மாற்றவும் (டெச்க்டாப் மட்டும்)",
"artistConfiguration": "ஆல்பம் கலைஞர் பக்க உள்ளமைவு",
"audioDevice_description": "பிளேபேக்கிற்கு பயன்படுத்த ஆடியோ சாதனத்தைத் தேர்ந்தெடுக்கவும் (வெப் பிளேயர் மட்டும்)",
"audioExclusiveMode": "ஆடியோ பிரத்தியேக பயன்முறை",
"audioPlayer": "ஆடியோ பிளேயர்",
"audioPlayer_description": "பிளேபேக்கிற்கு பயன்படுத்த ஆடியோ பிளேயரைத் தேர்ந்தெடுக்கவும்",
"customCssEnable_description": "தனிப்பயன் சிஎச்எச் ஐ எழுத அனுமதிக்கவும்.",
"customCss": "தனிப்பயன் சிஎச்எச்",
"customFontPath": "தனிப்பயன் எழுத்துரு பாதை",
"customFontPath_description": "பயன்பாட்டிற்கு பயன்படுத்த தனிப்பயன் எழுத்துருவுக்கு பாதையை அமைக்கிறது",
"disableLibraryUpdateOnStartup": "தொடக்கத்தில் புதிய பதிப்புகளைச் சரிபார்ப்பதை முடக்கு",
"discordApplicationId": "{{discord}} பயன்பாட்டு ஐடி",
"discordListening": "கேட்பது என நிலையைக் காட்டுங்கள்",
"exitToTray_description": "கணினி தட்டில் பயன்பாட்டிலிருந்து வெளியேறவும்",
"floatingQueueArea": "மிதக்கும் வரிசை ஓவர் பகுதியைக் காட்டு",
"floatingQueueArea_description": "நாடக வரிசையைக் காண திரையின் வலது பக்கத்தில் ஒரு ஓவர் ஐகானைக் காண்பி",
"followLyric": "தற்போதைய பாடலைப் பின்பற்றுங்கள்",
"followLyric_description": "தற்போதைய விளையாட்டு நிலைக்கு பாடலை உருட்டவும்",
"font": "எழுத்துரு",
"font_description": "பயன்பாட்டிற்கு பயன்படுத்த எழுத்துருவை அமைக்கிறது",
"fontType": "எழுத்துரு வகை",
"fontType_description": "உள்ளமைக்கப்பட்ட எழுத்துரு ஃபீசின் வழங்கிய எழுத்துருக்களில் ஒன்றைத் தேர்ந்தெடுக்கிறது. உங்கள் இயக்க முறைமை வழங்கிய எந்த எழுத்துருவையும் தேர்ந்தெடுக்க கணினி எழுத்துரு உங்களை அனுமதிக்கிறது. உங்கள் சொந்த எழுத்துருவை வழங்க தனிப்பயன் உங்களை அனுமதிக்கிறது",
"fontType_optionBuiltIn": "உள்ளமைக்கப்பட்ட எழுத்துரு",
"fontType_optionCustom": "தனிப்பயன் எழுத்துரு",
"fontType_optionSystem": "கணினி எழுத்துரு",
"gaplessAudio": "இடைவெளி இல்லாத ஆடியோ",
"gaplessAudio_description": "MPV க்கான இடைவெளி இல்லாத ஆடியோ அமைப்பை அமைக்கிறது",
"gaplessAudio_optionWeak": "பலவீனமான (பரிந்துரைக்கப்படுகிறது)",
"genreBehavior": "வகை பக்கம் இயல்புநிலை நடத்தை",
"genreBehavior_description": "ஒரு வகையைக் சொடுக்கு செய்வது டிராக் அல்லது ஆல்பம் பட்டியலில் இயல்பாகத் திறக்கிறதா என்பதை தீர்மானிக்கிறது",
"globalMediaHotkeys_description": "பிளேபேக்கைக் கட்டுப்படுத்த உங்கள் கணினி மீடியா ஆட்கீசின் பயன்பாட்டை இயக்கவும் அல்லது முடக்கவும்",
"homeConfiguration": "முகப்பு பக்க உள்ளமைவு",
"homeFeature": "வீட்டில் கொணர்வி இடம்பெற்றது",
"hotkey_favoriteCurrentSong": "பிடித்த $ t (common.curressong)",
"hotkey_globalSearch": "உலக தேடல்",
"hotkey_playbackPrevious": "முந்தைய பாடல்",
"hotkey_playbackStop": "நிறுத்து",
"hotkey_rate0": "மதிப்பீடு தெளிவாக",
"hotkey_rate1": "மதிப்பீடு 1 விண்மீன்",
"hotkey_rate2": "மதிப்பீடு 2 நட்சத்திரங்கள்",
"hotkey_rate3": "மதிப்பீடு 3 நட்சத்திரங்கள்",
"hotkey_rate4": "மதிப்பீடு 4 நட்சத்திரங்கள்",
"hotkey_rate5": "மதிப்பீடு 5 நட்சத்திரங்கள்",
"hotkey_toggleFullScreenPlayer": "முழு திரை பிளேயரை மாற்றவும்",
"hotkey_togglePreviousSongFavorite": "மாற்றவும் (பொதுவானது. ப்ரீவியச்ங்) பிடித்தது",
"hotkey_toggleQueue": "வரிசையை மாற்றவும்",
"hotkey_toggleRepeat": "மீண்டும் மீண்டும்",
"hotkey_toggleShuffle": "கலக்கு மாற்று",
"hotkey_unfavoriteCurrentSong": "சாதகமற்ற $ t (common.curressong)",
"hotkey_unfavoritePreviousSong": "சாதகமற்ற $ t (காமன்.பிரெவியச்ங்)",
"hotkey_volumeDown": "தொகுதி கீழே",
"hotkey_volumeMute": "தொகுதி முடக்கு",
"hotkey_volumeUp": "தொகுதி",
"language": "மொழி",
"language_description": "பயன்பாட்டிற்கான மொழியை அமைக்கிறது ($ t (பொதுவானது.",
"lastfmApiKey": "{{lastfm}} பநிஇ key",
"lastfmApiKey_description": "{{lastfm} க்கு க்கான பநிஇ விசை. கவர் கலைக்கு தேவை",
"lyricFetch": "இணையத்திலிருந்து வரிகளை பெறுங்கள்",
"lyricFetchProvider_description": "பாடல் பெற வழங்குநர்களைத் தேர்ந்தெடுக்கவும். வழங்குநர்களின் வரிசை அவர்கள் வினவப்படும் ஒழுங்கு",
"lyricOffset": "பாடல் ஆஃப்செட் (எம்.எச்)",
"minimizeToTray": "தட்டில் குறைக்கவும்",
"minimumScrobblePercentage": "குறைந்தபட்ச துணிச்சல் காலம் (சதவீதம்)",
"minimumScrobblePercentage_description": "பாடலின் குறைந்தபட்ச விழுக்காடு அதைத் துடைப்பதற்கு முன்பு இசைக்க வேண்டும்",
"minimumScrobbleSeconds": "குறைந்தபட்ச தோண்டல் (விநாடிகள்)",
"minimumScrobbleSeconds_description": "பாடலின் விநாடிகளில் குறைந்தபட்ச காலம் அது வேட்டையாடப்படுவதற்கு முன்பு இசைக்கப்பட வேண்டும்",
"mpvExecutablePath": "MPV இயங்கக்கூடிய பாதை",
"mpvExecutablePath_description": "MPV இயங்கக்கூடிய பாதையை அமைக்கிறது. காலியாக இருந்தால், இயல்புநிலை பாதை பயன்படுத்தப்படும்",
"mpvExtraParameters": "MPV அளவுருக்கள்",
"mpvExtraParameters_help": "ஒரு வரிக்கு ஒன்று",
"passwordStore": "கடவுச்சொற்கள்/ரகசிய கடை",
"passwordStore_description": "என்ன கடவுச்சொல்/ரகசிய கடை பயன்படுத்த வேண்டும். கடவுச்சொற்களை சேமிப்பதில் சிக்கல்கள் இருந்தால் இதை மாற்றவும்.",
"playbackStyle": "பிளேபேக் பாணி",
"playbackStyle_description": "ஆடியோ பிளேயருக்கு பயன்படுத்த பிளேபேக் பாணியைத் தேர்ந்தெடுக்கவும்",
"playbackStyle_optionCrossFade": "கிராச்ஃபேட்",
"playbackStyle_optionNormal": "சாதாரண",
"playButtonBehavior": "பொத்தான் நடத்தை விளையாடுங்கள்",
"playButtonBehavior_description": "வரிசையில் பாடல்களைச் சேர்க்கும்போது ப்ளே பொத்தானின் இயல்புநிலை நடத்தை அமைக்கிறது",
"playButtonBehavior_optionAddLast": "$ t (player.addlast)",
"playButtonBehavior_optionAddNext": "$ t (player.addnext)",
"playerAlbumArtResolution": "பிளேயர் ஆல்பம் கலைத் தீர்மானம்",
"playerAlbumArtResolution_description": "பெரிய வீரரின் ஆல்பம் கலை முன்னோட்டத்திற்கான தீர்மானம். பெரியது இது மிகவும் மிருதுவானதாக தோற்றமளிக்கிறது, ஆனால் மெதுவாக ஏற்றுவதை மெதுவாகக் கொண்டிருக்கலாம். இயல்புநிலை 0 க்கு, அதாவது ஆட்டோ",
"playerbarOpenDrawer": "பிளேயர்பார் முழுத்திரை மாற்று",
"playerbarOpenDrawer_description": "முழு திரை பிளேயரைத் திறக்க பிளேயர்பாரைக் சொடுக்கு செய்ய அனுமதிக்கிறது",
"remotePassword": "ரிமோட் கண்ட்ரோல் சர்வர் கடவுச்சொல்",
"remotePassword_description": "ரிமோட் கண்ட்ரோல் சேவையகத்திற்கான கடவுச்சொல்லை அமைக்கிறது. இந்த நற்சான்றிதழ்கள் இயல்பாகவே பாதுகாப்பற்ற முறையில் மாற்றப்படுகின்றன, எனவே நீங்கள் கவலைப்படாத தனிப்பட்ட கடவுச்சொல்லைப் பயன்படுத்த வேண்டும்",
"remotePort": "ரிமோட் கண்ட்ரோல் சர்வர் துறைமுகம்",
"remotePort_description": "ரிமோட் கண்ட்ரோல் சேவையகத்திற்கான துறைமுகத்தை அமைக்கிறது",
"remoteUsername": "ரிமோட் கண்ட்ரோல் சர்வர் பயனர்பெயர்",
"remoteUsername_description": "ரிமோட் கண்ட்ரோல் சேவையகத்திற்கான பயனர்பெயரை அமைக்கிறது. பயனர்பெயர் மற்றும் கடவுச்சொல் இரண்டும் காலியாக இருந்தால், ஏற்பு முடக்கப்படும்",
"replayGainClipping": "{{ReplayGain}} கிளிப்பிங்",
"replayGainClipping_description": "ஆதாயத்தை தானாகவே குறைப்பதன் மூலம் {{ReplayGain} by காரணமாக ஏற்படும் கிளிப்பிங்கைத் தடுக்கவும்",
"replayGainFallback": "{{{ReplayGain}}} falback",
"replayGainFallback_description": "கோப்பில் {{ReplayGain}} குறிச்சொற்கள் இல்லையென்றால் விண்ணப்பிக்க DB இல் ஆதாயம்",
"replayGainMode": "{{ReplayGain}} பயன்முறை",
"replayGainMode_description": "{{ReplayGain}}} மதிப்புகளின் படி தொகுதி ஆதாயத்தை சரிசெய்யவும் மேனிலை தரவு கோப்பு",
"replayGainMode_optionAlbum": "$ t (entity.album_one)",
"replayGainMode_optionNone": "$ t (common.none)",
"replayGainMode_optionTrack": "$ t (entity.track_one)",
"replayGainPreamp": "{{ReplayGain}} preamp (db)",
"replayGainPreamp_description": "{{ReplayGain}}} மதிப்புகளுக்கு பயன்படுத்தப்படும் Preamp ஆதாயத்தை சரிசெய்யவும்",
"sampleRate": "மாதிரி வீதம்",
"sampleRate_description": "தேர்ந்தெடுக்கப்பட்ட மாதிரி அதிர்வெண் தற்போதைய மீடியாவிலிருந்து வேறுபட்டால் பயன்படுத்த வேண்டிய வெளியீட்டு மாதிரி வீதத்தைத் தேர்ந்தெடுக்கவும். 8000 க்கும் குறைவான மதிப்பு இயல்புநிலை அதிர்வெண்ணைப் பயன்படுத்தும்",
"themeLight_description": "பயன்பாட்டிற்கு பயன்படுத்த ஒளி கருப்பொருள் அமைக்கிறது",
"transcodeNote": "1 (வலை) - 2 (MPV) பாடல்களுக்குப் பிறகு நடைமுறைக்கு வருகிறது",
"transcode": "டிரான்ச்கோடிங்கை இயக்கவும்",
"transcode_description": "வெவ்வேறு வடிவங்களுக்கு மாற்றுவதை செயல்படுத்துகிறது",
"transcodeBitrate": "டிரான்ச்கோடிற்கு பிட்ரேட்",
"transcodeBitrate_description": "டிரான்ச்கோடிற்கு பிட்ரேட்டைத் தேர்ந்தெடுக்கிறது. 0 என்றால் சேவையகம் எடுக்கட்டும்",
"transcodeFormat": "டிரான்ச்கோடுக்கு வடிவம்",
"transcodeFormat_description": "டிரான்ச்கோடிற்கு வடிவமைப்பைத் தேர்ந்தெடுக்கிறது. சேவையகம் தீர்மானிக்க காலியாக விடவும்",
"translationApiProvider": "மொழிபெயர்ப்பு பநிஇ வழங்குநர்",
"translationApiProvider_description": "மொழிபெயர்ப்புக்கான பநிஇ வழங்குநர்",
"translationApiKey": "மொழிபெயர்ப்பு பநிஇ விசை",
"translationApiKey_description": "மொழிபெயர்ப்பிற்கான பநிஇ விசை (உலகளாவிய பணி இறுதிப்புள்ளியை மட்டும் ஆதரிக்கவும்)",
"translationTargetLanguage": "மொழிபெயர்ப்பு இலக்கு மொழி",
"translationTargetLanguage_description": "மொழிபெயர்ப்பிற்கான இலக்கு மொழி",
"trayEnabled": "தட்டில் காட்டு",
"trayEnabled_description": "தட்டு ஐகான்/மெனுவைக் காட்டவும்/மறைக்கவும். முடக்கப்பட்டால், தட்டில் குறைக்க/வெளியேறவும் முடக்குகிறது",
"volumeWidth_description": "தொகுதி ச்லைடரின் அகலம்",
"webAudio": "வலை ஆடியோவைப் பயன்படுத்தவும்",
"webAudio_description": "வலை ஆடியோவைப் பயன்படுத்தவும். இது ரீப்ளே கெய்ன் போன்ற மேம்பட்ட அம்சங்களை செயல்படுத்துகிறது. நீங்கள் வேறுவிதமாக அனுபவித்தால் முடக்கு",
"artistConfiguration_description": "எந்த உருப்படிகள் காண்பிக்கப்படுகின்றன, எந்த வரிசையில், ஆல்பம் கலைஞர் பக்கத்தில் உள்ளமைக்கவும்",
"audioDevice": "ஆடியோ சாதனம்",
"audioExclusiveMode_description": "பிரத்யேக வெளியீட்டு பயன்முறையை இயக்கவும். இந்த பயன்முறையில், கணினி வழக்கமாக பூட்டப்படுகிறது, மேலும் MPV மட்டுமே ஆடியோவை வெளியிட முடியும்",
"buttonSize": "பிளேயர் பார் பொத்தான் அளவு",
"buttonSize_description": "பிளேயர் பார் பொத்தான்களின் அளவு",
"clearCache": "தெளிவான உலாவி தற்காலிக சேமிப்பு",
"clearCache_description": "ஃபீசினின் ஒரு 'கடினமான தெளிவான'. ஃபெசினின் தற்காலிக சேமிப்பை அழிப்பதைத் தவிர, உலாவி தற்காலிக சேமிப்பை (சேமித்த படங்கள் மற்றும் பிற சொத்துக்கள்) வெறுமை செய்யுங்கள். சேவையக நற்சான்றிதழ்கள் மற்றும் அமைப்புகள் பாதுகாக்கப்படுகின்றன",
"albumBackground_description": "ஆல்பம் கலை கொண்ட ஆல்பம் பக்கங்களுக்கு பின்னணி படத்தை சேர்க்கிறது",
"albumBackgroundBlur": "ஆல்பம் பின்னணி பட மங்கலான அளவு",
"albumBackgroundBlur_description": "ஆல்பத்தின் பின்னணி படத்திற்கு பயன்படுத்தப்படும் மங்கலின் அளவை சரிசெய்கிறது",
"clearQueryCache": "தெளிவான ஃபைசின் கேச்",
"clearQueryCache_description": "ஃபீசினின் 'மென்மையான தெளிவான'. இது பிளேலிச்ட்களைப் புதுப்பிக்கும், மெட்டாடேட்டாவைக் கண்காணிக்கும் மற்றும் சேமித்த பாடல் வரிகளை மீட்டமைக்கும். அமைப்புகள், சேவையக நற்சான்றிதழ்கள் மற்றும் தற்காலிக சேமிப்பு படங்கள் பாதுகாக்கப்படுகின்றன",
"clearCacheSuccess": "கேச் வெற்றிகரமாக அழிக்கப்பட்டது",
"contextMenu": "சூழல் பட்டியல் (வலது கிளிக்) உள்ளமைவு",
"crossfadeDuration": "கிராச்ஃபேட் காலம்",
"crossfadeDuration_description": "கிராச்ஃபேட் விளைவின் காலத்தை அமைக்கிறது",
"crossfadeStyle": "கிராச்ஃபேட் பாணி",
"crossfadeStyle_description": "ஆடியோ பிளேயருக்கு பயன்படுத்த கிராச்ஃபேட் பாணியைத் தேர்ந்தெடுக்கவும்",
"customCssEnable": "தனிப்பயன் சிஎச்எச் ஐ இயக்கவும்",
"customCssNotice": "எச்சரிக்கை: சில சுத்திகரிப்பு (URL () மற்றும் உள்ளடக்கத்தை அனுமதிக்காதது :) இருக்கும்போது, தனிப்பயன் சிஎச்எச் ஐப் பயன்படுத்துவது இடைமுகத்தை மாற்றுவதன் மூலம் ஆபத்துக்களை ஏற்படுத்தக்கூடும்.",
"contextMenu_description": "நீங்கள் ஒரு உருப்படியை வலது சொடுக்கு செய்யும் போது பட்டியலில் காட்டப்பட்டுள்ள உருப்படிகளை மறைக்க உங்களை அனுமதிக்கிறது. சரிபார்க்கப்படாத உருப்படிகள் மறைக்கப்படும்",
"disableAutomaticUpdates": "தானியங்கி புதுப்பிப்புகளை முடக்கு",
"discordApplicationId_description": "{{discord}} பணக்கார இருப்புக்கான பயன்பாட்டு ஐடி (இயல்புநிலை {{defaultId}})",
"discordIdleStatus": "பணக்கார இருப்பு செயலற்ற நிலையைக் காட்டுங்கள்",
"discordIdleStatus_description": "இயக்கப்பட்டால், பிளேயர் சும்மா இருக்கும்போது நிலையைப் புதுப்பிக்கவும்",
"discordListening_description": "விளையாடுவதற்குப் பதிலாக கேட்பது என்று அந்த நிலையைக் காட்டுங்கள்",
"discordRichPresence": "{{discord}} பணக்கார இருப்பு",
"discordRichPresence_description": "{{discord}} பணக்கார இருப்பில் பின்னணி நிலையை இயக்கவும். பட விசைகள்: {{icon}}, {{playing}}, மற்றும் {{paused}} ",
"customCss_description": "தனிப்பயன் சிஎச்எச் உள்ளடக்கம். குறிப்பு: உள்ளடக்கம் மற்றும் தொலைநிலை முகவரி கள் அனுமதிக்கப்படாத பண்புகள். உங்கள் உள்ளடக்கத்தின் முன்னோட்டம் கீழே காட்டப்பட்டுள்ளது. நீங்கள் அமைக்காத கூடுதல் புலங்கள் சுத்திகரிப்பு காரணமாக உள்ளன.",
"doubleClickBehavior": "இரட்டை சொடுக்கு செய்யும் போது தேடப்பட்ட அனைத்து தடங்களையும் வரிசைப்படுத்தவும்",
"doubleClickBehavior_description": "உண்மை என்றால், தட தேடலில் பொருந்தக்கூடிய அனைத்து தடங்களும் வரிசையில் நிற்கப்படும். இல்லையெனில், சொடுக்கு செய்யப்பட்ட ஒன்று மட்டுமே வரிசையில் நிற்கப்படும்",
"enableRemote": "ரிமோட் கண்ட்ரோல் சேவையகத்தை இயக்கவும்",
"enableRemote_description": "பயன்பாட்டைக் கட்டுப்படுத்த மற்ற சாதனங்களை அனுமதிக்க ரிமோட் கண்ட்ரோல் சேவையகத்தை இயக்குகிறது",
"externalLinks": "வெளிப்புற இணைப்புகளைக் காட்டு",
"externalLinks_description": "கலைஞர்/ஆல்பம் பக்கங்களில் வெளிப்புற இணைப்புகளை (last.fm, மியூசிக் ப்ரெய்ன்ச்) காண்பிக்க உதவுகிறது",
"exitToTray": "தட்டில் வெளியேறவும்",
"globalMediaHotkeys": "உலகளாவிய மீடியா ஆட்கீச்",
"discordUpdateInterval": "{{discord}} பணக்கார இருப்பு புதுப்பிப்பு இடைவெளி",
"discordUpdateInterval_description": "ஒவ்வொரு புதுப்பிப்புக்கும் இடையிலான விநாடிகளில் நேரம் (குறைந்தபட்சம் 15 வினாடிகள்)",
"homeConfiguration_description": "என்னென்ன உருப்படிகள் காட்டப்படுகின்றன, எந்த வரிசையில், முகப்பு பக்கத்தில் உள்ளமைக்கவும்",
"homeFeature_description": "முகப்பு பக்கத்தில் பெரிய பிரத்யேக கொணர்வி காட்ட வேண்டுமா என்பதைக் கட்டுப்படுத்துகிறது",
"hotkey_browserBack": "உலாவி மீண்டும்",
"hotkey_browserForward": "முன்னோக்கி உலாவி",
"hotkey_favoritePreviousSong": "பிடித்த $ t (காமன்.பிரெவியச்ங்)",
"hotkey_localSearch": "பக்க தேடல்",
"hotkey_playbackNext": "அடுத்த பாடல்",
"hotkey_playbackPause": "இடைநிறுத்தம்",
"hotkey_playbackPlay": "விளையாடுங்கள்",
"hotkey_playbackPlayPause": "விளையாடு / இடைநிறுத்தம்",
"hotkey_skipBackward": "பின்தங்கிய நிலையில் தவிர்க்கவும்",
"hotkey_zoomIn": "பெரிதாக்கு",
"hotkey_zoomOut": "சிறிதாக்கு",
"imageAspectRatio": "சொந்த கவர் கலை விகித விகிதத்தைப் பயன்படுத்தவும்",
"imageAspectRatio_description": "இயக்கப்பட்டால், கவர் கலை அவற்றின் சொந்த விகித விகிதத்தைப் பயன்படுத்தி காண்பிக்கப்படும். 1: 1 இல்லாத கலைக்கு, மீதமுள்ள இடம் காலியாக இருக்கும்",
"lyricFetch_description": "பல்வேறு இணைய மூலங்களிலிருந்து பாடல் வரிகள்",
"lyricFetchProvider": "பாடல் பெற வழங்குநர்கள்",
"lyricOffset_description": "குறிப்பிட்ட அளவு மில்லி விநாடிகளால் பாடலை ஈடுசெய்யவும்",
"hotkey_skipForward": "முன்னோக்கி செல்லுங்கள்",
"hotkey_toggleCurrentSongFavorite": "மாற்று $ t (common.curressong) பிடித்தது",
"minimizeToTray_description": "கணினி தட்டில் பயன்பாட்டைக் குறைக்கவும்",
"playButtonBehavior_optionPlay": "$ t (player.play)",
"playButtonBehavior_optionPlayShuffled": "$ t (player.shuffle)",
"savePlayQueue": "விளையாட்டு வரிசையை சேமிக்கவும்",
"savePlayQueue_description": "பயன்பாடு மூடப்படும் போது ப்ளே வரிசையை சேமித்து, பயன்பாடு திறக்கப்படும் போது அதை மீட்டெடுக்கவும்",
"scrobble": "ச்க்ரோபில்",
"scrobble_description": "உங்கள் மீடியா சேவையகத்திற்கு ச்க்ரோபில் விளையாடுகிறது",
"showSkipButton": "ச்கிப் பொத்தான்களைக் காட்டு",
"showSkipButton_description": "பிளேயர் பட்டியில் ச்கிப் பொத்தான்களைக் காட்டவும் அல்லது மறைக்கவும்",
"sidebarConfiguration": "பக்கப்பட்டி உள்ளமைவு",
"sidebarConfiguration_description": "பக்கப்பட்டியில் தோன்றும் உருப்படிகள் மற்றும் வரிசையைத் தேர்ந்தெடுக்கவும்",
"showSkipButtons": "ச்கிப் பொத்தான்களைக் காட்டு",
"showSkipButtons_description": "பிளேயர் பட்டியில் ச்கிப் பொத்தான்களைக் காட்டவும் அல்லது மறைக்கவும்",
"sidebarCollapsedNavigation": "பக்கப்பட்டி (சரிந்த) வழிசெலுத்தல்",
"sidebarCollapsedNavigation_description": "சரிந்த பக்கப்பட்டியில் வழிசெலுத்தலைக் காட்டவும் அல்லது மறைக்கவும்",
"sidebarPlaylistList": "பக்கப்பட்டி பிளேலிச்ட் பட்டியல்",
"sidebarPlaylistList_description": "பக்கப்பட்டியில் பிளேலிச்ட் பட்டியலைக் காட்டவும் அல்லது மறைக்கவும்",
"sidePlayQueueStyle": "சைட் பிளே வரிசை பாணி",
"sidePlayQueueStyle_description": "பக்க நாடக வரிசையின் பாணியை அமைக்கிறது",
"sidePlayQueueStyle_optionAttached": "இணைக்கப்பட்டுள்ளது",
"sidePlayQueueStyle_optionDetached": "பிரிக்கப்பட்டது",
"theme_description": "பயன்பாட்டிற்கு பயன்படுத்த கருப்பொருள் அமைக்கிறது",
"themeDark": "கருப்பொருள் (இருண்ட)",
"themeDark_description": "பயன்பாட்டிற்கு பயன்படுத்த இருண்ட கருப்பொருள் அமைக்கிறது",
"skipDuration": "கால அளவைத் தவிர்க்கவும்",
"skipDuration_description": "பிளேயர் பட்டியில் தவிர் பொத்தான்களைப் பயன்படுத்தும் போது தவிர்க்க வேண்டிய காலத்தை அமைக்கிறது",
"skipPlaylistPage": "பிளேலிச்ட் பக்கத்தைத் தவிர்க்கவும்",
"skipPlaylistPage_description": "பிளேலிச்ட்டுக்கு செல்லும்போது, இயல்புநிலை பக்கத்திற்கு பதிலாக பிளேலிச்ட் பாடல் பட்டியல் பக்கத்திற்குச் செல்லவும்",
"startMinimized": "குறைக்கத் தொடங்குங்கள்",
"startMinimized_description": "கணினி தட்டில் பயன்பாட்டைத் தொடங்கவும்",
"theme": "கருப்பொருள்",
"themeLight": "கருப்பொருள் (ஒளி)",
"volumeWheelStep": "தொகுதி சக்கர படி",
"volumeWheelStep_description": "தொகுதி ச்லைடரில் சுட்டி சக்கரத்தை ச்க்ரோலிங் செய்யும் போது மாற்ற வேண்டிய அளவின் அளவு",
"volumeWidth": "தொகுதி ச்லைடர் அகலம்",
"windowBarStyle": "சாளரம் பார் பாணி",
"windowBarStyle_description": "சாளர பட்டியின் பாணியைத் தேர்ந்தெடுக்கவும்",
"useSystemTheme": "கணினி கருப்பொருளைப் பயன்படுத்தவும்",
"useSystemTheme_description": "கணினி வரையறுக்கப்பட்ட ஒளி அல்லது இருண்ட விருப்பத்தைப் பின்பற்றவும்",
"zoom": "சூம் விழுக்காடு",
"zoom_description": "பயன்பாட்டிற்கான சூம் சதவீதத்தை அமைக்கிறது"
},
"table": {
"config": {
"label": {
"album": "$ t (entity.album_one)",
"artist": "$ t (entity.artist_one)",
"biography": "$ t (காமன். -புவியியல்)",
"bitrate": "$ t (common.bitrate)",
"bpm": "$ t (common.bpm)",
"channels": "$ t (common.channel_other)",
"codec": "$ t (common.codec)",
"dateAdded": "தேதி சேர்க்கப்பட்டது",
"rating": "$ t (பொதுவானது. ரேட்டிங்)",
"releaseDate": "வெளியீட்டு தேதி",
"rowIndex": "வரிசை அட்டவணை",
"size": "$ t (common.size)",
"trackNumber": "ட்ராக் எண்",
"year": "$ t (பொதுவானது.",
"lastPlayed": "கடைசியாக விளையாடியது",
"note": "$ t (பொதுவானது. குறிப்பு)",
"owner": "$ t (பொதுவானவர்)",
"actions": "$ t (common.action_other)",
"albumArtist": "$ t (entity.albumartist_one)",
"discNumber": "வட்டு எண்",
"duration": "$ t (பொதுவானது.",
"favorite": "$ t (common.foavorite)",
"genre": "$ t (entity.genre_one)",
"path": "$ t (common.path)",
"playCount": "விளையாட்டு எண்ணிக்கை",
"songCount": "$ t (entity.track_other)",
"title": "$ t (common.title)",
"titleCombined": "$ t (common.title) (ஒருங்கிணைந்த)"
},
"view": {
"card": "அட்டை",
"table": "அட்டவணை",
"poster": "சுவரொட்டி"
},
"general": {
"autoFitColumns": "ஆட்டோ பொருத்தம் நெடுவரிசைகள்",
"followCurrentSong": "தற்போதைய பாடலைப் பின்தொடரவும்",
"displayType": "காட்சி வகை",
"gap": "$ t (comman.gap)",
"itemGap": "உருப்படி இடைவெளி (பிஎக்ச்)",
"itemSize": "உருப்படி அளவு (பிஎக்ச்)",
"size": "$ t (common.size)",
"tableColumns": "அட்டவணை நெடுவரிசைகள்"
}
},
"column": {
"album": "ஆல்பம்",
"albumArtist": "ஆல்பம் கலைஞர்",
"albumCount": "$ t (entity.album_other)",
"artist": "$ t (entity.artist_one)",
"biography": "சுயசரிதை",
"bitrate": "பிட்ரேட்",
"bpm": "பிபிஎம்",
"channels": "$ t (common.channel_other)",
"codec": "$ t (common.codec)",
"comment": "கருத்து",
"dateAdded": "தேதி சேர்க்கப்பட்டது",
"discNumber": "வட்டு",
"favorite": "பிடித்த",
"genre": "$ t (entity.genre_one)",
"lastPlayed": "கடைசியாக விளையாடியது",
"path": "பாதை",
"playCount": "நாடகங்கள்",
"rating": "செயல்வரம்பு",
"releaseDate": "வெளியீட்டு தேதி",
"releaseYear": "ஆண்டு",
"size": "$ t (common.size)",
"songCount": "$ t (entity.track_other)",
"title": "தலைப்பு",
"trackNumber": "மின்தடம்"
}
}
}
+41 -12
View File
@@ -20,7 +20,8 @@
"openIn": {
"lastfm": "在 Last.fm 中打开",
"musicbrainz": "在 MusicBrainz 中打开"
}
},
"moveToNext": "移至下一首"
},
"common": {
"increase": "增高",
@@ -107,7 +108,8 @@
"albumGain": "专辑增益",
"codec": "编解码器",
"share": "分享",
"preview": "预览"
"preview": "预览",
"translation": "翻译"
},
"entity": {
"albumArtist_other": "专辑艺术家",
@@ -126,7 +128,8 @@
"smartPlaylist": "智能$t(entity.playlist_one)",
"genreWithCount_other": "{{count}} 种流派",
"trackWithCount_other": "{{count}} 首乐曲",
"play_other": "{{count}} 次播放"
"play_other": "{{count}} 次播放",
"song_other": "歌曲"
},
"player": {
"repeat_all": "循环全部",
@@ -140,7 +143,7 @@
"skip_back": "向后跳过",
"favorite": "收藏",
"next": "下一首",
"shuffle": "随机",
"shuffle": "随机播放",
"playbackFetchNoResults": "未找到歌曲",
"playbackFetchInProgress": "正在加载歌曲…",
"addNext": "添加为播放列表下一首",
@@ -152,13 +155,14 @@
"unfavorite": "取消收藏",
"queue_moveToTop": "将所选项移至底部",
"queue_moveToBottom": "将所选项移至顶部",
"shuffle_off": "随机关闭",
"shuffle_off": "禁用随机播放",
"addLast": "添加至播放列表末尾",
"mute": "静音",
"skip_forward": "向前跳过",
"playbackSpeed": "播放速度",
"pause": "暂停",
"playSimilarSongs": "播放类似的曲目"
"playSimilarSongs": "播放类似的曲目",
"viewQueue": "查看播放队列"
},
"setting": {
"crossfadeStyle_description": "选择用于音频播放器的淡入淡出风格",
@@ -350,7 +354,7 @@
"volumeWidth": "音量滑块宽度",
"volumeWidth_description": "音量滑块的宽度",
"discordListening": "显示状态为正在监听",
"discordListening_description": "将状态显示为正在监听,而不是正在播放”。请注意,这当前会破坏计时器栏",
"discordListening_description": "将状态显示为正在监听,而不是正在播放",
"contextMenu_description": "允许您隐藏右键单击项目时显示在菜单中的项目。未选中的项目将被隐藏",
"customCssEnable_description": "允许编写自定义 css。",
"customCss": "自定义css",
@@ -374,7 +378,18 @@
"webAudio_description": "使用 web 音频。这将启用重播增益等高级功能。如果您遇到其他情况,请禁用",
"artistConfiguration_description": "配置专辑艺术家页面上显示的项目及其显示顺序",
"webAudio": "使用 web 音频",
"artistConfiguration": "专辑艺术家页面配置"
"artistConfiguration": "专辑艺术家页面配置",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"trayEnabled_description": "显示/隐藏托盘图标/菜单。如果禁用,也会禁用最小化/退出到托盘",
"trayEnabled": "显示托盘",
"translationApiProvider": "翻译api提供商",
"translationApiProvider_description": "翻译api提供商",
"translationApiKey": "翻译api密钥",
"translationApiKey_description": "翻译api密钥(仅支持全球服务节点)",
"translationTargetLanguage": "目标翻译语言",
"translationTargetLanguage_description": "目标翻译语言",
"lastfmApiKey": "{{lastfm}} API 密钥",
"lastfmApiKey_description": "{{lastfm}} 的 API 密钥。封面艺术图所需"
},
"error": {
"remotePortWarning": "重启服务器使新端口生效",
@@ -473,11 +488,14 @@
"lyricGap": "歌词间距",
"followCurrentLyric": "跟随当前歌词",
"dynamicImageBlur": "图像模糊大小",
"dynamicIsImage": "启用背景图像"
"dynamicIsImage": "启用背景图像",
"lyricOffset": "歌词延迟补偿(毫秒)"
},
"lyrics": "歌词",
"related": "相关",
"upNext": "即将播放"
"upNext": "即将播放",
"visualizer": "可视化",
"noLyrics": "未找到歌词"
},
"appMenu": {
"selectServer": "选择服务器",
@@ -538,7 +556,9 @@
"showDetails": "获取信息",
"shareItem": "分享项目",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "下载"
"download": "下载",
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"trackList": {
"title": "$t(entity.track_other)",
@@ -579,6 +599,14 @@
},
"playlist": {
"reorder": "仅在按 ID 排序时启用重排序"
},
"manageServers": {
"url": "URL",
"title": "管理服务器",
"serverDetails": "服务器详细信息",
"username": "用户名",
"editServerDetailsTooltip": "编辑服务器详细信息",
"removeServer": "移除服务器"
}
},
"form": {
@@ -650,7 +678,8 @@
"autoFitColumns": "列宽自适应",
"size": "$t(common.size)",
"itemGap": "项目间隙(px",
"itemSize": "项目大小 (px)"
"itemSize": "项目大小 (px)",
"followCurrentSong": "关注当前播放的歌曲"
},
"view": {
"table": "表格",
+3 -1
View File
@@ -647,7 +647,9 @@ if (!singleInstance) {
});
createWindow();
createTray();
if (store.get('window_enable_tray', true)) {
createTray();
}
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
+10 -10
View File
@@ -48,7 +48,7 @@ export default class MenuBuilder {
label: 'Electron',
submenu: [
{
label: 'About ElectronReact',
label: 'About Feishin',
selector: 'orderFrontStandardAboutPanel:',
},
{ type: 'separator' },
@@ -56,7 +56,7 @@ export default class MenuBuilder {
{ type: 'separator' },
{
accelerator: 'Command+H',
label: 'Hide ElectronReact',
label: 'Hide Feishin',
selector: 'hide:',
},
{
@@ -147,27 +147,27 @@ export default class MenuBuilder {
submenu: [
{
click() {
shell.openExternal('https://electronjs.org');
shell.openExternal('https://github.com/jeffvli/feishin');
},
label: 'Learn More',
},
{
click() {
shell.openExternal(
'https://github.com/electron/electron/tree/main/docs#readme',
'https://github.com/jeffvli/feishin?tab=readme-ov-file#getting-started',
);
},
label: 'Documentation',
},
{
click() {
shell.openExternal('https://www.electronjs.org/community');
shell.openExternal('https://github.com/jeffvli/feishin/discussions');
},
label: 'Community Discussions',
},
{
click() {
shell.openExternal('https://github.com/electron/electron/issues');
shell.openExternal('https://github.com/jeffvli/feishin/issues');
},
label: 'Search Issues',
},
@@ -246,27 +246,27 @@ export default class MenuBuilder {
submenu: [
{
click() {
shell.openExternal('https://electronjs.org');
shell.openExternal('https://github.com/jeffvli/feishin');
},
label: 'Learn More',
},
{
click() {
shell.openExternal(
'https://github.com/electron/electron/tree/main/docs#readme',
'https://github.com/jeffvli/feishin?tab=readme-ov-file#getting-started',
);
},
label: 'Documentation',
},
{
click() {
shell.openExternal('https://www.electronjs.org/community');
shell.openExternal('https://github.com/jeffvli/feishin/discussions');
},
label: 'Community Discussions',
},
{
click() {
shell.openExternal('https://github.com/electron/electron/issues');
shell.openExternal('https://github.com/jeffvli/feishin/issues');
},
label: 'Search Issues',
},
+131 -574
View File
@@ -1,119 +1,11 @@
import { useAuthStore } from '/@/renderer/store';
import { toast } from '/@/renderer/components/toast/index';
import type {
AlbumDetailArgs,
AlbumListArgs,
SongListArgs,
SongDetailArgs,
AlbumArtistDetailArgs,
AlbumArtistListArgs,
SetRatingArgs,
ShareItemArgs,
GenreListArgs,
CreatePlaylistArgs,
DeletePlaylistArgs,
PlaylistDetailArgs,
PlaylistListArgs,
MusicFolderListArgs,
PlaylistSongListArgs,
ArtistListArgs,
UpdatePlaylistArgs,
UserListArgs,
FavoriteArgs,
TopSongListArgs,
AddToPlaylistArgs,
AddToPlaylistResponse,
RemoveFromPlaylistArgs,
RemoveFromPlaylistResponse,
ScrobbleArgs,
ScrobbleResponse,
AlbumArtistDetailResponse,
FavoriteResponse,
CreatePlaylistResponse,
AlbumArtistListResponse,
AlbumDetailResponse,
AlbumListResponse,
ArtistListResponse,
GenreListResponse,
MusicFolderListResponse,
PlaylistDetailResponse,
PlaylistListResponse,
RatingResponse,
SongDetailResponse,
SongListResponse,
TopSongListResponse,
UpdatePlaylistResponse,
UserListResponse,
AuthenticationResponse,
SearchArgs,
SearchResponse,
LyricsArgs,
LyricsResponse,
ServerInfo,
ServerInfoArgs,
StructuredLyricsArgs,
StructuredLyric,
SimilarSongsArgs,
Song,
ServerType,
ShareItemResponse,
MoveItemArgs,
DownloadArgs,
TranscodingArgs,
} from '/@/renderer/api/types';
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
import { ndController } from '/@/renderer/api/navidrome/navidrome-controller';
import { ssController } from '/@/renderer/api/subsonic/subsonic-controller';
import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller';
import type { ServerType, ControllerEndpoint, AuthenticationResponse } from '/@/renderer/api/types';
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
import i18n from '/@/i18n/i18n';
export type ControllerEndpoint = Partial<{
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
authenticate: (
url: string,
body: { password: string; username: string },
) => Promise<AuthenticationResponse>;
clearPlaylist: () => void;
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
getArtistDetail: () => void;
getArtistInfo: (args: any) => void;
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
getDownloadUrl: (args: DownloadArgs) => string;
getFavoritesList: () => void;
getFolderItemList: () => void;
getFolderList: () => void;
getFolderSongs: () => void;
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
getLyrics: (args: LyricsArgs) => Promise<LyricsResponse>;
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
getSimilarSongs: (args: SimilarSongsArgs) => Promise<Song[]>;
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
getStructuredLyrics: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
getTranscodingUrl: (args: TranscodingArgs) => string;
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
movePlaylistItem: (args: MoveItemArgs) => Promise<void>;
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
search: (args: SearchArgs) => Promise<SearchResponse>;
setRating: (args: SetRatingArgs) => Promise<RatingResponse>;
shareItem: (args: ShareItemArgs) => Promise<ShareItemResponse>;
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
}>;
type ApiController = {
jellyfin: ControllerEndpoint;
navidrome: ControllerEndpoint;
@@ -121,133 +13,15 @@ type ApiController = {
};
const endpoints: ApiController = {
jellyfin: {
addToPlaylist: jfController.addToPlaylist,
authenticate: jfController.authenticate,
clearPlaylist: undefined,
createFavorite: jfController.createFavorite,
createPlaylist: jfController.createPlaylist,
deleteFavorite: jfController.deleteFavorite,
deletePlaylist: jfController.deletePlaylist,
getAlbumArtistDetail: jfController.getAlbumArtistDetail,
getAlbumArtistList: jfController.getAlbumArtistList,
getAlbumDetail: jfController.getAlbumDetail,
getAlbumList: jfController.getAlbumList,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getDownloadUrl: jfController.getDownloadUrl,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: jfController.getGenreList,
getLyrics: jfController.getLyrics,
getMusicFolderList: jfController.getMusicFolderList,
getPlaylistDetail: jfController.getPlaylistDetail,
getPlaylistList: jfController.getPlaylistList,
getPlaylistSongList: jfController.getPlaylistSongList,
getRandomSongList: jfController.getRandomSongList,
getServerInfo: jfController.getServerInfo,
getSimilarSongs: jfController.getSimilarSongs,
getSongDetail: jfController.getSongDetail,
getSongList: jfController.getSongList,
getStructuredLyrics: undefined,
getTopSongs: jfController.getTopSongList,
getTranscodingUrl: jfController.getTranscodingUrl,
getUserList: undefined,
movePlaylistItem: jfController.movePlaylistItem,
removeFromPlaylist: jfController.removeFromPlaylist,
scrobble: jfController.scrobble,
search: jfController.search,
setRating: undefined,
shareItem: undefined,
updatePlaylist: jfController.updatePlaylist,
},
navidrome: {
addToPlaylist: ndController.addToPlaylist,
authenticate: ndController.authenticate,
clearPlaylist: undefined,
createFavorite: ssController.createFavorite,
createPlaylist: ndController.createPlaylist,
deleteFavorite: ssController.removeFavorite,
deletePlaylist: ndController.deletePlaylist,
getAlbumArtistDetail: ndController.getAlbumArtistDetail,
getAlbumArtistList: ndController.getAlbumArtistList,
getAlbumDetail: ndController.getAlbumDetail,
getAlbumList: ndController.getAlbumList,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getDownloadUrl: ssController.getDownloadUrl,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: ndController.getGenreList,
getLyrics: undefined,
getMusicFolderList: ssController.getMusicFolderList,
getPlaylistDetail: ndController.getPlaylistDetail,
getPlaylistList: ndController.getPlaylistList,
getPlaylistSongList: ndController.getPlaylistSongList,
getRandomSongList: ssController.getRandomSongList,
getServerInfo: ndController.getServerInfo,
getSimilarSongs: ndController.getSimilarSongs,
getSongDetail: ndController.getSongDetail,
getSongList: ndController.getSongList,
getStructuredLyrics: ssController.getStructuredLyrics,
getTopSongs: ssController.getTopSongList,
getTranscodingUrl: ssController.getTranscodingUrl,
getUserList: ndController.getUserList,
movePlaylistItem: ndController.movePlaylistItem,
removeFromPlaylist: ndController.removeFromPlaylist,
scrobble: ssController.scrobble,
search: ssController.search3,
setRating: ssController.setRating,
shareItem: ndController.shareItem,
updatePlaylist: ndController.updatePlaylist,
},
subsonic: {
authenticate: ssController.authenticate,
clearPlaylist: undefined,
createFavorite: ssController.createFavorite,
createPlaylist: undefined,
deleteFavorite: ssController.removeFavorite,
deletePlaylist: undefined,
getAlbumArtistDetail: undefined,
getAlbumArtistList: undefined,
getAlbumDetail: undefined,
getAlbumList: undefined,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getDownloadUrl: ssController.getDownloadUrl,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: undefined,
getLyrics: undefined,
getMusicFolderList: ssController.getMusicFolderList,
getPlaylistDetail: undefined,
getPlaylistList: undefined,
getServerInfo: ssController.getServerInfo,
getSimilarSongs: ssController.getSimilarSongs,
getSongDetail: undefined,
getSongList: undefined,
getStructuredLyrics: ssController.getStructuredLyrics,
getTopSongs: ssController.getTopSongList,
getTranscodingUrl: ssController.getTranscodingUrl,
getUserList: undefined,
scrobble: ssController.scrobble,
search: ssController.search3,
setRating: undefined,
shareItem: undefined,
updatePlaylist: undefined,
},
jellyfin: JellyfinController,
navidrome: NavidromeController,
subsonic: SubsonicController,
};
const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => {
const apiController = <K extends keyof ControllerEndpoint>(
endpoint: K,
type?: ServerType,
): NonNullable<ControllerEndpoint[K]> => {
const serverType = type || useAuthStore.getState().currentServer?.type;
if (!serverType) {
@@ -277,344 +51,127 @@ const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) =>
);
}
return endpoints[serverType][endpoint];
return controllerFn;
};
const authenticate = async (
url: string,
body: { legacy?: boolean; password: string; username: string },
type: ServerType,
) => {
return (apiController('authenticate', type) as ControllerEndpoint['authenticate'])?.(url, body);
};
export interface GeneralController extends Omit<Required<ControllerEndpoint>, 'authenticate'> {
authenticate: (
url: string,
body: { legacy?: boolean; password: string; username: string },
type: ServerType,
) => Promise<AuthenticationResponse>;
}
const getAlbumList = async (args: AlbumListArgs) => {
return (
apiController(
'getAlbumList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumList']
)?.(args);
};
const getAlbumDetail = async (args: AlbumDetailArgs) => {
return (
apiController(
'getAlbumDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumDetail']
)?.(args);
};
const getSongList = async (args: SongListArgs) => {
return (
apiController(
'getSongList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getSongList']
)?.(args);
};
const getSongDetail = async (args: SongDetailArgs) => {
return (
apiController(
'getSongDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getSongDetail']
)?.(args);
};
const getMusicFolderList = async (args: MusicFolderListArgs) => {
return (
apiController(
'getMusicFolderList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getMusicFolderList']
)?.(args);
};
const getGenreList = async (args: GenreListArgs) => {
return (
apiController(
'getGenreList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getGenreList']
)?.(args);
};
const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs) => {
return (
apiController(
'getAlbumArtistDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumArtistDetail']
)?.(args);
};
const getAlbumArtistList = async (args: AlbumArtistListArgs) => {
return (
apiController(
'getAlbumArtistList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumArtistList']
)?.(args);
};
const getArtistList = async (args: ArtistListArgs) => {
return (
apiController(
'getArtistList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getArtistList']
)?.(args);
};
const getPlaylistList = async (args: PlaylistListArgs) => {
return (
apiController(
'getPlaylistList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getPlaylistList']
)?.(args);
};
const createPlaylist = async (args: CreatePlaylistArgs) => {
return (
apiController(
'createPlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['createPlaylist']
)?.(args);
};
const updatePlaylist = async (args: UpdatePlaylistArgs) => {
return (
apiController(
'updatePlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['updatePlaylist']
)?.(args);
};
const deletePlaylist = async (args: DeletePlaylistArgs) => {
return (
apiController(
'deletePlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['deletePlaylist']
)?.(args);
};
const addToPlaylist = async (args: AddToPlaylistArgs) => {
return (
apiController(
'addToPlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['addToPlaylist']
)?.(args);
};
const removeFromPlaylist = async (args: RemoveFromPlaylistArgs) => {
return (
apiController(
'removeFromPlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['removeFromPlaylist']
)?.(args);
};
const getPlaylistDetail = async (args: PlaylistDetailArgs) => {
return (
apiController(
'getPlaylistDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getPlaylistDetail']
)?.(args);
};
const getPlaylistSongList = async (args: PlaylistSongListArgs) => {
return (
apiController(
'getPlaylistSongList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getPlaylistSongList']
)?.(args);
};
const getUserList = async (args: UserListArgs) => {
return (
apiController(
'getUserList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getUserList']
)?.(args);
};
const createFavorite = async (args: FavoriteArgs) => {
return (
apiController(
'createFavorite',
args.apiClientProps.server?.type,
) as ControllerEndpoint['createFavorite']
)?.(args);
};
const deleteFavorite = async (args: FavoriteArgs) => {
return (
apiController(
'deleteFavorite',
args.apiClientProps.server?.type,
) as ControllerEndpoint['deleteFavorite']
)?.(args);
};
const updateRating = async (args: SetRatingArgs) => {
return (
apiController(
'setRating',
args.apiClientProps.server?.type,
) as ControllerEndpoint['setRating']
)?.(args);
};
const shareItem = async (args: ShareItemArgs) => {
return (
apiController(
'shareItem',
args.apiClientProps.server?.type,
) as ControllerEndpoint['shareItem']
)?.(args);
};
const getTopSongList = async (args: TopSongListArgs) => {
return (
apiController(
'getTopSongs',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getTopSongs']
)?.(args);
};
const scrobble = async (args: ScrobbleArgs) => {
return (
apiController(
'scrobble',
args.apiClientProps.server?.type,
) as ControllerEndpoint['scrobble']
)?.(args);
};
const search = async (args: SearchArgs) => {
return (
apiController('search', args.apiClientProps.server?.type) as ControllerEndpoint['search']
)?.(args);
};
const getRandomSongList = async (args: RandomSongListArgs) => {
return (
apiController(
'getRandomSongList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getRandomSongList']
)?.(args);
};
const getLyrics = async (args: LyricsArgs) => {
return (
apiController(
'getLyrics',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getLyrics']
)?.(args);
};
const getServerInfo = async (args: ServerInfoArgs) => {
return (
apiController(
'getServerInfo',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getServerInfo']
)?.(args);
};
const getStructuredLyrics = async (args: StructuredLyricsArgs) => {
return (
apiController(
'getStructuredLyrics',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getStructuredLyrics']
)?.(args);
};
const getSimilarSongs = async (args: SimilarSongsArgs) => {
return (
apiController(
'getSimilarSongs',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getSimilarSongs']
)?.(args);
};
const movePlaylistItem = async (args: MoveItemArgs) => {
return (
apiController(
'movePlaylistItem',
args.apiClientProps.server?.type,
) as ControllerEndpoint['movePlaylistItem']
)?.(args);
};
const getDownloadUrl = (args: DownloadArgs) => {
return (
apiController(
'getDownloadUrl',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getDownloadUrl']
)?.(args);
};
const getTranscodingUrl = (args: TranscodingArgs) => {
return (
apiController(
'getTranscodingUrl',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getTranscodingUrl']
)?.(args);
};
export const controller = {
addToPlaylist,
authenticate,
createFavorite,
createPlaylist,
deleteFavorite,
deletePlaylist,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumDetail,
getAlbumList,
getArtistList,
getDownloadUrl,
getGenreList,
getLyrics,
getMusicFolderList,
getPlaylistDetail,
getPlaylistList,
getPlaylistSongList,
getRandomSongList,
getServerInfo,
getSimilarSongs,
getSongDetail,
getSongList,
getStructuredLyrics,
getTopSongList,
getTranscodingUrl,
getUserList,
movePlaylistItem,
removeFromPlaylist,
scrobble,
search,
shareItem,
updatePlaylist,
updateRating,
export const controller: GeneralController = {
addToPlaylist(args) {
return apiController('addToPlaylist', args.apiClientProps.server?.type)?.(args);
},
authenticate(url, body, type) {
return apiController('authenticate', type)(url, body);
},
createFavorite(args) {
return apiController('createFavorite', args.apiClientProps.server?.type)?.(args);
},
createPlaylist(args) {
return apiController('createPlaylist', args.apiClientProps.server?.type)?.(args);
},
deleteFavorite(args) {
return apiController('deleteFavorite', args.apiClientProps.server?.type)?.(args);
},
deletePlaylist(args) {
return apiController('deletePlaylist', args.apiClientProps.server?.type)?.(args);
},
getAlbumArtistDetail(args) {
return apiController('getAlbumArtistDetail', args.apiClientProps.server?.type)?.(args);
},
getAlbumArtistList(args) {
return apiController('getAlbumArtistList', args.apiClientProps.server?.type)?.(args);
},
getAlbumArtistListCount(args) {
return apiController('getAlbumArtistListCount', args.apiClientProps.server?.type)?.(args);
},
getAlbumDetail(args) {
return apiController('getAlbumDetail', args.apiClientProps.server?.type)?.(args);
},
getAlbumList(args) {
return apiController('getAlbumList', args.apiClientProps.server?.type)?.(args);
},
getAlbumListCount(args) {
return apiController('getAlbumListCount', args.apiClientProps.server?.type)?.(args);
},
getDownloadUrl(args) {
return apiController('getDownloadUrl', args.apiClientProps.server?.type)?.(args);
},
getGenreList(args) {
return apiController('getGenreList', args.apiClientProps.server?.type)?.(args);
},
getLyrics(args) {
return apiController('getLyrics', args.apiClientProps.server?.type)?.(args);
},
getMusicFolderList(args) {
return apiController('getMusicFolderList', args.apiClientProps.server?.type)?.(args);
},
getPlaylistDetail(args) {
return apiController('getPlaylistDetail', args.apiClientProps.server?.type)?.(args);
},
getPlaylistList(args) {
return apiController('getPlaylistList', args.apiClientProps.server?.type)?.(args);
},
getPlaylistListCount(args) {
return apiController('getPlaylistListCount', args.apiClientProps.server?.type)?.(args);
},
getPlaylistSongList(args) {
return apiController('getPlaylistSongList', args.apiClientProps.server?.type)?.(args);
},
getRandomSongList(args) {
return apiController('getRandomSongList', args.apiClientProps.server?.type)?.(args);
},
getServerInfo(args) {
return apiController('getServerInfo', args.apiClientProps.server?.type)?.(args);
},
getSimilarSongs(args) {
return apiController('getSimilarSongs', args.apiClientProps.server?.type)?.(args);
},
getSongDetail(args) {
return apiController('getSongDetail', args.apiClientProps.server?.type)?.(args);
},
getSongList(args) {
return apiController('getSongList', args.apiClientProps.server?.type)?.(args);
},
getSongListCount(args) {
return apiController('getSongListCount', args.apiClientProps.server?.type)?.(args);
},
getStructuredLyrics(args) {
return apiController('getStructuredLyrics', args.apiClientProps.server?.type)?.(args);
},
getTopSongs(args) {
return apiController('getTopSongs', args.apiClientProps.server?.type)?.(args);
},
getTranscodingUrl(args) {
return apiController('getTranscodingUrl', args.apiClientProps.server?.type)?.(args);
},
getUserList(args) {
return apiController('getUserList', args.apiClientProps.server?.type)?.(args);
},
movePlaylistItem(args) {
return apiController('movePlaylistItem', args.apiClientProps.server?.type)?.(args);
},
removeFromPlaylist(args) {
return apiController('removeFromPlaylist', args.apiClientProps.server?.type)?.(args);
},
scrobble(args) {
return apiController('scrobble', args.apiClientProps.server?.type)?.(args);
},
search(args) {
return apiController('search', args.apiClientProps.server?.type)?.(args);
},
setRating(args) {
return apiController('setRating', args.apiClientProps.server?.type)?.(args);
},
shareItem(args) {
return apiController('shareItem', args.apiClientProps.server?.type)?.(args);
},
updatePlaylist(args) {
return apiController('updatePlaylist', args.apiClientProps.server?.type)?.(args);
},
};
+1
View File
@@ -1,6 +1,7 @@
// Should follow a strict naming convention: "<FEATURE GROUP>_<FEATURE NAME>"
// For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART"
export enum ServerFeature {
BFR = 'bfr',
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
PLAYLISTS_SMART = 'playlistsSmart',
+1 -1
View File
@@ -574,7 +574,7 @@ export enum JFSongListSort {
ARTIST = 'Artist,Album,SortName',
COMMUNITY_RATING = 'CommunityRating,SortName',
DURATION = 'Runtime,AlbumArtist,Album,SortName',
NAME = 'SortName,Name',
NAME = 'Name',
PLAY_COUNT = 'PlayCount,SortName',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
File diff suppressed because it is too large Load Diff
+18 -11
View File
@@ -53,7 +53,7 @@ const getAlbumArtistCoverArtUrl = (args: {
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&` +
`?width=${size}` +
'&quality=96'
);
};
@@ -69,7 +69,7 @@ const getAlbumCoverArtUrl = (args: { baseUrl: string; item: JFAlbum; size: numbe
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
`?width=${size}` +
'&quality=96'
);
};
@@ -86,7 +86,7 @@ const getSongCoverArtUrl = (args: {
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
`?width=${size}` +
'&quality=96'
);
}
@@ -97,7 +97,7 @@ const getSongCoverArtUrl = (args: {
`${args.baseUrl}/Items` +
`/${args.item?.AlbumId}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
`?width=${size}` +
'&quality=96'
);
}
@@ -116,7 +116,7 @@ const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size:
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
`?width=${size}` +
'&quality=96'
);
};
@@ -153,11 +153,16 @@ const normalizeSong = (
discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1,
discSubtitle: null,
duration: item.RunTimeTicks / 10000,
gain: item.LUFS
? {
track: -18 - item.LUFS,
}
: null,
gain:
item.NormalizationGain !== undefined
? {
track: item.NormalizationGain,
}
: item.LUFS
? {
track: -18 - item.LUFS,
}
: null,
genres: item.GenreItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
@@ -171,6 +176,7 @@ const normalizeSong = (
lastPlayedAt: null,
lyrics: null,
name: item.Name,
participants: null,
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
peak: null,
playCount: (item.UserData && item.UserData.PlayCount) || 0,
@@ -230,6 +236,7 @@ const normalizeAlbum = (
})),
id: item.Id,
imagePlaceholderUrl: null,
participants: null,
imageUrl: getAlbumCoverArtUrl({
baseUrl: server?.url || '',
item,
@@ -388,7 +395,7 @@ const getGenreCoverArtUrl = (args: {
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
`?width=${size}` +
'&quality=96'
);
};
+2 -1
View File
@@ -413,6 +413,7 @@ const song = z.object({
MediaSources: z.array(mediaSources),
MediaType: z.string(),
Name: z.string(),
NormalizationGain: z.number().optional(),
ParentIndexNumber: z.number(),
PlaylistItemId: z.string().optional(),
PremiereDate: z.string().optional(),
@@ -544,7 +545,7 @@ const songListSort = {
ARTIST: 'Artist,Album,SortName',
COMMUNITY_RATING: 'CommunityRating,SortName',
DURATION: 'Runtime,AlbumArtist,Album,SortName',
NAME: 'SortName,Name',
NAME: 'Name',
PLAY_COUNT: 'PlayCount,SortName',
RANDOM: 'Random,SortName',
RECENTLY_ADDED: 'DateCreated,SortName',
+77 -17
View File
@@ -199,17 +199,17 @@ export type NDGenreListParams = {
NDOrder;
export enum NDAlbumListSort {
ALBUM_ARTIST = 'albumArtist',
ALBUM_ARTIST = 'album_artist',
ARTIST = 'artist',
DURATION = 'duration',
NAME = 'name',
PLAY_COUNT = 'playCount',
PLAY_COUNT = 'play_count',
PLAY_DATE = 'play_date',
RANDOM = 'random',
RATING = 'rating',
RECENTLY_ADDED = 'recently_added',
SONG_COUNT = 'songCount',
STARRED = 'starred',
STARRED = 'starred_at',
YEAR = 'max_year',
}
@@ -229,15 +229,15 @@ export type NDAlbumListParams = {
NDOrder;
export enum NDSongListSort {
ALBUM = 'album, order_album_artist_name, disc_number, track_number, title',
ALBUM_ARTIST = 'order_album_artist_name, album, disc_number, track_number, title',
ALBUM_SONGS = 'album, discNumber, trackNumber',
ALBUM = 'album',
ALBUM_ARTIST = 'order_album_artist_name',
ALBUM_SONGS = 'album',
ARTIST = 'artist',
BPM = 'bpm',
CHANNELS = 'channels',
COMMENT = 'comment',
DURATION = 'duration',
FAVORITED = 'starred ASC, starredAt ASC',
FAVORITED = 'starred_at',
GENRE = 'genre',
ID = 'id',
PLAY_COUNT = 'playCount',
@@ -247,7 +247,7 @@ export enum NDSongListSort {
RECENTLY_ADDED = 'createdAt',
TITLE = 'title',
TRACK = 'track',
YEAR = 'year, album, discNumber, trackNumber',
YEAR = 'year',
}
export type NDSongListParams = {
@@ -261,7 +261,7 @@ export type NDSongListParams = {
export enum NDAlbumArtistListSort {
ALBUM_COUNT = 'albumCount',
FAVORITED = 'starred ASC, starredAt ASC',
FAVORITED = 'starred_at',
NAME = 'name',
PLAY_COUNT = 'playCount',
RATING = 'rating',
@@ -353,7 +353,7 @@ export type NDPlaylistListResponse = NDPlaylist[];
export enum NDPlaylistListSort {
DURATION = 'duration',
NAME = 'name',
OWNER = 'ownerName',
OWNER = 'owner_name',
PUBLIC = 'public',
SONG_COUNT = 'songCount',
UPDATED_AT = 'updatedAt',
@@ -381,39 +381,99 @@ export type NDPlaylistSongList = {
export const NDSongQueryFields = [
{ label: 'Album', type: 'string', value: 'album' },
{ label: 'Album Artist', type: 'string', value: 'albumartist' },
{ label: 'Album Artists', type: 'string', value: 'albumartists' },
{ label: 'Album Comment', type: 'string', value: 'albumcomment' },
{ label: 'Album Type', type: 'string', value: 'albumtype' },
{ label: 'Album Version', type: 'string', value: 'albumversion' },
{ label: 'Arranger', type: 'string', value: 'arranger' },
{ label: 'Artist', type: 'string', value: 'artist' },
{ label: 'Artists', type: 'string', value: 'artists' },
{ label: 'Barcode', type: 'string', value: 'barcode' },
{ label: 'Bitrate', type: 'number', value: 'bitrate' },
{ label: 'BPM', type: 'number', value: 'bpm' },
{ label: 'Catalog Number', type: 'string', value: 'catalognumber' },
{ label: 'Channels', type: 'number', value: 'channels' },
{ label: 'Comment', type: 'string', value: 'comment' },
{ label: 'Composer', type: 'string', value: 'composer' },
{ label: 'Conductor', type: 'string', value: 'conductor' },
{ label: 'Copyright', type: 'string', value: 'copyright' },
{ label: 'Date Added', type: 'date', value: 'dateadded' },
{ label: 'Date Favorited', type: 'date', value: 'dateloved' },
{ label: 'Date Last Played', type: 'date', value: 'lastplayed' },
{ label: 'Date Modified', type: 'date', value: 'datemodified' },
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
{ label: 'DJ Mixer', type: 'string', value: 'djmixer' },
{ label: 'Director', type: 'string', value: 'director' },
{ label: 'Disc Number', type: 'number', value: 'discnumber' },
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
{ label: 'Disc Total', type: 'number', value: 'disctotal' },
{ label: 'Duration', type: 'number', value: 'duration' },
{ label: 'Encoded By', type: 'string', value: 'encodedby' },
{ label: 'Encoder Settings', type: 'string', value: 'encodersettings' },
{ label: 'Engineer', type: 'string', value: 'engineer' },
{ label: 'Explicit Status', type: 'string', value: 'explicitstatus' },
{ label: 'File Path', type: 'string', value: 'filepath' },
{ label: 'File Type', type: 'string', value: 'filetype' },
{ label: 'Genre', type: 'string', value: 'genre' },
{ label: 'Grouping', type: 'string', value: 'grouping' },
{ label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' },
{ label: 'Playlist', type: 'playlist', value: 'id' },
{ label: 'Is Compilation', type: 'boolean', value: 'compilation' },
{ label: 'Is Favorite', type: 'boolean', value: 'loved' },
{ label: 'ISRC', type: 'string', value: 'isrc' },
{ label: 'Key', type: 'string', value: 'key' },
{ label: 'Language', type: 'string', value: 'language' },
{ label: 'License', type: 'string', value: 'license' },
{ label: 'Lyricist', type: 'string', value: 'lyricist' },
{ label: 'Lyrics', type: 'string', value: 'lyrics' },
{ label: 'Media', type: 'string', value: 'media' },
{ label: 'Mixer', type: 'string', value: 'mixer' },
{ label: 'Mood', type: 'string', value: 'mood' },
{ label: 'Movement', type: 'string', value: 'movement' },
{ label: 'Movement Name', type: 'string', value: 'movementname' },
{ label: 'Movement Total', type: 'number', value: 'movementtotal' },
{ label: 'MusicBrainz Artist Id', type: 'string', value: 'musicbrainz_albumartistid' },
{ label: 'MusicBrainz Album Artist Id', type: 'string', value: 'musicbrainz_albumartistid' },
{ label: 'MusicBrainz Album Id', type: 'string', value: 'musicbrainz_albumid' },
{ label: 'MusicBrainz Disc Id', type: 'string', value: 'musicbrainz_discid' },
{ label: 'MusicBrainz Recording Id', type: 'string', value: 'musicbrainz_recordingid' },
{ label: 'MusicBrainz Release Group Id', type: 'string', value: 'musicbrainz_releasegroupid' },
{ label: 'MusicBrainz Track Id', type: 'string', value: 'musicbrainz_trackid' },
{ label: 'MusicBrainz Work Id', type: 'string', value: 'musicbrainz_workid' },
{ label: 'Name', type: 'string', value: 'title' },
{ label: 'Original Date', type: 'date', value: 'originaldate' },
{ label: 'Performer', type: 'string', value: 'performer' },
{ label: 'Play Count', type: 'number', value: 'playcount' },
{ label: 'Playlist', type: 'playlist', value: 'id' },
{ label: 'Producer', type: 'string', value: 'producer' },
{ label: 'R128 Album Gain', type: 'number', value: 'r128_album_gain' },
{ label: 'R128 Track Gain', type: 'number', value: 'r128_track_gain' },
{ label: 'Rating', type: 'number', value: 'rating' },
{ label: 'Record Label', type: 'string', value: 'recordlabel' },
{ label: 'Recording Date', type: 'date', value: 'recordingdate' },
{ label: 'Release Country', type: 'string', value: 'releasecountry' },
{ label: 'Release Date', type: 'date', value: 'releasedate' },
{ label: 'Release Status', type: 'string', value: 'releasestatus' },
{ label: 'Release Type', type: 'string', value: 'releasetype' },
{ label: 'ReplayGain Album Gain', type: 'number', value: 'replaygain_album_gain' },
{ label: 'ReplayGain Album Peak', type: 'number', value: 'replaygain_album_peak' },
{ label: 'ReplayGain Track Gain', type: 'number', value: 'replaygain_track_gain' },
{ label: 'ReplayGain Track Peak', type: 'number', value: 'replaygain_track_peak' },
{ label: 'Remixer', type: 'string', value: 'remixer' },
{ label: 'Script', type: 'string', value: 'script' },
{ label: 'Size', type: 'number', value: 'size' },
{ label: 'Sort Album', type: 'string', value: 'sortalbum' },
{ label: 'Sort Album Artist', type: 'string', value: 'sortalbumartist' },
{ label: 'Sort Artist', type: 'string', value: 'sortartist' },
{ label: 'Sort Name', type: 'string', value: 'sorttitle' },
{ label: 'Track Number', type: 'number', value: 'tracknumber' },
{ label: 'Sort Album', type: 'string', value: 'albumsort' },
{ label: 'Sort Album Artist', type: 'string', value: 'albumartistsort' },
{ label: 'Sort Album Artists', type: 'string', value: 'albumartistssort' },
{ label: 'Sort Artist', type: 'string', value: 'artistsort' },
{ label: 'Sort Artists', type: 'string', value: 'artistssort' },
{ label: 'Sort Composer', type: 'string', value: 'composersort' },
{ label: 'Sort Lyricist', type: 'string', value: 'lyricistsort' },
{ label: 'Sort Name', type: 'string', value: 'titlesort' },
{ label: 'Subtitle', type: 'string', value: 'subtitle' },
{ label: 'Track Number', type: 'number', value: 'track' },
{ label: 'Track Total', type: 'number', value: 'tracktotal' },
{ label: 'Year', type: 'number', value: 'year' },
{ label: 'Website', type: 'string', value: 'website' },
{ label: 'Work', type: 'string', value: 'work' },
];
export const NDSongQueryPlaylistOperators = [
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,7 @@ import {
Genre,
ServerListItem,
ServerType,
RelatedArtist,
} from '/@/renderer/api/types';
import z from 'zod';
import { ndType } from './navidrome-types';
@@ -41,7 +42,7 @@ const getCoverArtUrl = (args: {
`?id=${args.coverArtId}` +
`&${args.credential}` +
'&v=1.13.0' +
'&c=feishin' +
'&c=Feishin' +
`&size=${size}`
);
};
@@ -54,10 +55,73 @@ const normalizePlayDate = (item: WithDate): string | null => {
return !item.playDate || item.playDate.includes('0001-') ? null : item.playDate;
};
const getArtists = (
item:
| z.infer<typeof ndType._response.song>
| z.infer<typeof ndType._response.playlistSong>
| z.infer<typeof ndType._response.album>,
) => {
let albumArtists: RelatedArtist[] | undefined;
let artists: RelatedArtist[] | undefined;
let participants: Record<string, RelatedArtist[]> | null = null;
if (item.participants) {
participants = {};
for (const [role, list] of Object.entries(item.participants)) {
if (role === 'albumartist' || role === 'artist') {
const roleList = list.map((item) => ({
id: item.id,
imageUrl: null,
name: item.name,
}));
if (role === 'albumartist') {
albumArtists = roleList;
} else {
artists = roleList;
}
} else {
const subRoles = new Map<string | undefined, RelatedArtist[]>();
for (const artist of list) {
const item: RelatedArtist = {
id: artist.id,
imageUrl: null,
name: artist.name,
};
if (subRoles.has(artist.subRole)) {
subRoles.get(artist.subRole)!.push(item);
} else {
subRoles.set(artist.subRole, [item]);
}
}
for (const [subRole, items] of subRoles.entries()) {
if (subRole) {
participants[`${role} (${subRole})`] = items;
} else {
participants[role] = items;
}
}
}
}
}
if (albumArtists === undefined) {
albumArtists = [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }];
}
if (artists === undefined) {
artists = [{ id: item.artistId, imageUrl: null, name: item.artist }];
}
return { albumArtists, artists, participants };
};
const normalizeSong = (
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
server: ServerListItem | null,
deviceId: string,
imageSize?: number,
): Song => {
let id;
@@ -81,10 +145,9 @@ const normalizeSong = (
const imagePlaceholderUrl = null;
return {
album: item.album,
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
albumId: item.albumId,
...getArtists(item),
artistName: item.artist,
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
bitRate: item.bitRate,
bpm: item.bpm ? item.bpm : null,
channels: item.channels ? item.channels : null,
@@ -99,7 +162,7 @@ const normalizeSong = (
item.rgAlbumGain || item.rgTrackGain
? { album: item.rgAlbumGain, track: item.rgTrackGain }
: null,
genres: item.genres?.map((genre) => ({
genres: (item.genres || []).map((genre) => ({
id: genre.id,
imageUrl: null,
itemType: LibraryItem.GENRE,
@@ -117,17 +180,17 @@ const normalizeSong = (
item.rgAlbumPeak || item.rgTrackPeak
? { album: item.rgAlbumPeak, track: item.rgTrackPeak }
: null,
playCount: item.playCount,
playCount: item.playCount || 0,
playlistItemId,
releaseDate: (item.releaseDate
? new Date(item.releaseDate)
: new Date(item.year, 0, 1)
: new Date(Date.UTC(item.year, 0, 1))
).toISOString(),
releaseYear: String(item.year),
serverId: server?.id || 'unknown',
serverType: ServerType.NAVIDROME,
size: item.size,
streamUrl: `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`,
streamUrl: `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=Feishin&${server?.credential}`,
trackNumber: item.trackNumber,
uniqueId: nanoid(),
updatedAt: item.updatedAt,
@@ -156,13 +219,12 @@ const normalizeAlbum = (
return {
albumArtist: item.albumArtist,
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
...getArtists(item),
backdropImageUrl: imageBackdropUrl,
comment: item.comment || null,
createdAt: item.createdAt.split('T')[0],
duration: item.duration * 1000 || null,
genres: item.genres?.map((genre) => ({
duration: item.duration !== undefined ? item.duration * 1000 : null,
genres: (item.genres || []).map((genre) => ({
id: genre.id,
imageUrl: null,
itemType: LibraryItem.GENRE,
@@ -181,7 +243,7 @@ const normalizeAlbum = (
: item.originalYear
? new Date(item.originalYear, 0, 1).toISOString()
: null,
playCount: item.playCount,
playCount: item.playCount || 0,
releaseDate: (item.releaseDate
? new Date(item.releaseDate)
: new Date(item.minYear, 0, 1)
@@ -191,7 +253,7 @@ const normalizeAlbum = (
serverType: ServerType.NAVIDROME,
size: item.size,
songCount: item.songCount,
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server, '')) : undefined,
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server)) : undefined,
uniqueId: nanoid(),
updatedAt: item.updatedAt,
userFavorite: item.starred,
@@ -217,11 +279,11 @@ const normalizeAlbumArtist = (
}
return {
albumCount: item.albumCount,
albumCount: item.stats?.albumartist.albumCount || item.albumCount,
backgroundImageUrl: null,
biography: item.biography || null,
duration: null,
genres: item.genres?.map((genre) => ({
genres: (item.genres || []).map((genre) => ({
id: genre.id,
imageUrl: null,
itemType: LibraryItem.GENRE,
@@ -233,7 +295,7 @@ const normalizeAlbumArtist = (
lastPlayedAt: normalizePlayDate(item),
mbz: item.mbzArtistId || null,
name: item.name,
playCount: item.playCount,
playCount: item.playCount || 0,
serverId: server?.id || 'unknown',
serverType: ServerType.NAVIDROME,
similarArtists:
@@ -242,7 +304,7 @@ const normalizeAlbumArtist = (
imageUrl: artist?.artistImageUrl || null,
name: artist.name,
})) || null,
songCount: item.songCount,
songCount: item.stats?.albumartist.songCount || item.songCount,
userFavorite: item.starred,
userRating: item.rating,
};
+42 -70
View File
@@ -1,4 +1,10 @@
import { z } from 'zod';
import {
NDAlbumArtistListSort,
NDAlbumListSort,
NDPlaylistListSort,
NDSongListSort,
} from '/@/renderer/api/navidrome.types';
const sortOrderValues = ['ASC', 'DESC'] as const;
@@ -64,20 +70,26 @@ const genreListParameters = paginationParameters.extend({
const genreList = z.array(genre);
const stats = z.object({
albumCount: z.number(),
size: z.number(),
songCount: z.number(),
});
const albumArtist = z.object({
albumCount: z.number(),
biography: z.string(),
externalInfoUpdatedAt: z.string(),
externalUrl: z.string(),
fullText: z.string(),
genres: z.array(genre),
genres: z.array(genre).nullable(),
id: z.string(),
largeImageUrl: z.string().optional(),
mbzArtistId: z.string().optional(),
mediumImageUrl: z.string().optional(),
name: z.string(),
orderArtistName: z.string(),
playCount: z.number(),
playCount: z.number().optional(),
playDate: z.string().optional(),
rating: z.number(),
size: z.number(),
@@ -85,26 +97,28 @@ const albumArtist = z.object({
songCount: z.number(),
starred: z.boolean(),
starredAt: z.string(),
stats: z.record(z.string(), stats).optional(),
});
const albumArtistList = z.array(albumArtist);
const ndAlbumArtistListSort = {
ALBUM_COUNT: 'albumCount',
FAVORITED: 'starred ASC, starredAt ASC',
NAME: 'name',
PLAY_COUNT: 'playCount',
RATING: 'rating',
SONG_COUNT: 'songCount',
} as const;
const albumArtistListParameters = paginationParameters.extend({
_sort: z.nativeEnum(ndAlbumArtistListSort).optional(),
_sort: z.nativeEnum(NDAlbumArtistListSort).optional(),
genre_id: z.string().optional(),
missing: z.boolean().optional(),
name: z.string().optional(),
role: z.string().optional(),
starred: z.boolean().optional(),
});
const participant = z.object({
id: z.string(),
name: z.string(),
subRole: z.string().optional(),
});
const participants = z.record(z.string(), z.array(participant));
const album = z.object({
albumArtist: z.string(),
albumArtistId: z.string(),
@@ -116,10 +130,10 @@ const album = z.object({
coverArtId: z.string().optional(), // Removed after v0.48.0
coverArtPath: z.string().optional(), // Removed after v0.48.0
createdAt: z.string(),
duration: z.number(),
duration: z.number().optional(),
fullText: z.string(),
genre: z.string(),
genres: z.array(genre),
genres: z.array(genre).nullable(),
id: z.string(),
maxYear: z.number(),
mbzAlbumArtistId: z.string().optional(),
@@ -130,7 +144,8 @@ const album = z.object({
orderAlbumName: z.string(),
originalDate: z.string().optional(),
originalYear: z.number().optional(),
playCount: z.number(),
participants: z.optional(participants),
playCount: z.number().optional(),
playDate: z.string().optional(),
rating: z.number().optional(),
releaseDate: z.string().optional(),
@@ -145,23 +160,8 @@ const album = z.object({
const albumList = z.array(album);
const ndAlbumListSort = {
ALBUM_ARTIST: 'albumArtist',
ARTIST: 'artist',
DURATION: 'duration',
NAME: 'name',
PLAY_COUNT: 'playCount',
PLAY_DATE: 'play_date',
RANDOM: 'random',
RATING: 'rating',
RECENTLY_ADDED: 'recently_added',
SONG_COUNT: 'songCount',
STARRED: 'starred',
YEAR: 'max_year',
} as const;
const albumListParameters = paginationParameters.extend({
_sort: z.nativeEnum(ndAlbumListSort).optional(),
_sort: z.nativeEnum(NDAlbumListSort).optional(),
album_id: z.string().optional(),
artist_id: z.string().optional(),
compilation: z.boolean().optional(),
@@ -198,7 +198,7 @@ const song = z.object({
externalUrl: z.string().optional(),
fullText: z.string(),
genre: z.string(),
genres: z.array(genre),
genres: z.array(genre).nullable(),
hasCoverArt: z.boolean(),
id: z.string(),
imageFiles: z.string().optional(),
@@ -213,8 +213,9 @@ const song = z.object({
orderAlbumName: z.string(),
orderArtistName: z.string(),
orderTitle: z.string(),
participants: z.optional(participants),
path: z.string(),
playCount: z.number(),
playCount: z.number().optional(),
playDate: z.string().optional(),
rating: z.number().optional(),
releaseDate: z.string().optional(),
@@ -229,6 +230,7 @@ const song = z.object({
starred: z.boolean(),
starredAt: z.string().optional(),
suffix: z.string(),
tags: z.record(z.string(), z.array(z.string())).optional(),
title: z.string(),
trackNumber: z.number(),
updatedAt: z.string(),
@@ -237,33 +239,12 @@ const song = z.object({
const songList = z.array(song);
const ndSongListSort = {
ALBUM: 'album, order_album_artist_name, disc_number, track_number, title',
ALBUM_ARTIST: 'order_album_artist_name, album, disc_number, track_number, title',
ALBUM_SONGS: 'album, discNumber, trackNumber',
ARTIST: 'artist',
BPM: 'bpm',
CHANNELS: 'channels',
COMMENT: 'comment',
DURATION: 'duration',
FAVORITED: 'starred ASC, starredAt ASC',
GENRE: 'genre',
ID: 'id',
PLAY_COUNT: 'playCount',
PLAY_DATE: 'playDate',
RATING: 'rating',
RECENTLY_ADDED: 'createdAt',
TITLE: 'title',
TRACK: 'track',
YEAR: 'year, album, discNumber, trackNumber',
};
const songListParameters = paginationParameters.extend({
_sort: z.nativeEnum(ndSongListSort).optional(),
_sort: z.nativeEnum(NDSongListSort).optional(),
album_artist_id: z.array(z.string()).optional(),
album_id: z.array(z.string()).optional(),
artist_id: z.array(z.string()).optional(),
genre_id: z.string().optional(),
genre_id: z.array(z.string()).optional(),
path: z.string().optional(),
starred: z.boolean().optional(),
title: z.string().optional(),
@@ -290,17 +271,8 @@ const playlist = z.object({
const playlistList = z.array(playlist);
const ndPlaylistListSort = {
DURATION: 'duration',
NAME: 'name',
OWNER: 'ownerName',
PUBLIC: 'public',
SONG_COUNT: 'songCount',
UPDATED_AT: 'updatedAt',
} as const;
const playlistListParameters = paginationParameters.extend({
_sort: z.nativeEnum(ndPlaylistListSort).optional(),
_sort: z.nativeEnum(NDPlaylistListSort).optional(),
owner_id: z.string().optional(),
q: z.string().optional(),
smart: z.boolean().optional(),
@@ -367,11 +339,11 @@ const moveItem = z.null();
export const ndType = {
_enum: {
albumArtistList: ndAlbumArtistListSort,
albumList: ndAlbumListSort,
albumArtistList: NDAlbumArtistListSort,
albumList: NDAlbumListSort,
genreList: genreListSort,
playlistList: ndPlaylistListSort,
songList: ndSongListSort,
playlistList: NDPlaylistListSort,
songList: NDSongListSort,
userList: ndUserListSort,
},
_parameters: {
+46
View File
@@ -50,6 +50,19 @@ export const queryKeys: Record<
Record<string, (...props: any) => QueryFunctionContext['queryKey']>
> = {
albumArtists: {
count: (serverId: string, query?: AlbumArtistListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'albumArtists', 'count', filter, pagination] as const;
}
if (query) {
return [serverId, 'albumArtists', 'count', filter] as const;
}
return [serverId, 'albumArtists', 'count'] as const;
},
detail: (serverId: string, query?: AlbumArtistDetailQuery) => {
if (query) return [serverId, 'albumArtists', 'detail', query] as const;
return [serverId, 'albumArtists', 'detail'] as const;
@@ -73,6 +86,27 @@ export const queryKeys: Record<
},
},
albums: {
count: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
const { pagination, filter } = splitPaginatedQuery(query);
if (query && pagination && artistId) {
return [serverId, 'albums', 'count', artistId, filter, pagination] as const;
}
if (query && pagination) {
return [serverId, 'albums', 'count', filter, pagination] as const;
}
if (query && artistId) {
return [serverId, 'albums', 'count', artistId, filter] as const;
}
if (query) {
return [serverId, 'albums', 'count', filter] as const;
}
return [serverId, 'albums', 'count'] as const;
},
detail: (serverId: string, query?: AlbumDetailQuery) =>
[serverId, 'albums', 'detail', query] as const,
list: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
@@ -208,6 +242,18 @@ export const queryKeys: Record<
root: (serverId: string) => [serverId] as const,
},
songs: {
count: (serverId: string, query?: SongListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'songs', 'count', filter, pagination] as const;
}
if (query) {
return [serverId, 'songs', 'count', filter] as const;
}
return [serverId, 'songs', 'count'] as const;
},
detail: (serverId: string, query?: SongDetailQuery) => {
if (query) return [serverId, 'songs', 'detail', query] as const;
return [serverId, 'songs', 'detail'] as const;
+108 -4
View File
@@ -27,6 +27,46 @@ export const contract = c.router({
200: ssType._response.createFavorite,
},
},
createPlaylist: {
method: 'GET',
path: 'createPlaylist.view',
query: ssType._parameters.createPlaylist,
responses: {
200: ssType._response.createPlaylist,
},
},
deletePlaylist: {
method: 'GET',
path: 'deletePlaylist.view',
query: ssType._parameters.deletePlaylist,
responses: {
200: ssType._response.baseResponse,
},
},
getAlbum: {
method: 'GET',
path: 'getAlbum.view',
query: ssType._parameters.getAlbum,
responses: {
200: ssType._response.getAlbum,
},
},
getAlbumList2: {
method: 'GET',
path: 'getAlbumList2.view',
query: ssType._parameters.getAlbumList2,
responses: {
200: ssType._response.getAlbumList2,
},
},
getArtist: {
method: 'GET',
path: 'getArtist.view',
query: ssType._parameters.getArtist,
responses: {
200: ssType._response.getArtist,
},
},
getArtistInfo: {
method: 'GET',
path: 'getArtistInfo.view',
@@ -35,6 +75,22 @@ export const contract = c.router({
200: ssType._response.artistInfo,
},
},
getArtists: {
method: 'GET',
path: 'getArtists.view',
query: ssType._parameters.getArtists,
responses: {
200: ssType._response.getArtists,
},
},
getGenres: {
method: 'GET',
path: 'getGenres.view',
query: ssType._parameters.getGenres,
responses: {
200: ssType._response.getGenres,
},
},
getMusicFolderList: {
method: 'GET',
path: 'getMusicFolders.view',
@@ -42,6 +98,22 @@ export const contract = c.router({
200: ssType._response.musicFolderList,
},
},
getPlaylist: {
method: 'GET',
path: 'getPlaylist.view',
query: ssType._parameters.getPlaylist,
responses: {
200: ssType._response.getPlaylist,
},
},
getPlaylists: {
method: 'GET',
path: 'getPlaylists.view',
query: ssType._parameters.getPlaylists,
responses: {
200: ssType._response.getPlaylists,
},
},
getRandomSongList: {
method: 'GET',
path: 'getRandomSongs.view',
@@ -65,6 +137,30 @@ export const contract = c.router({
200: ssType._response.similarSongs,
},
},
getSong: {
method: 'GET',
path: 'getSong.view',
query: ssType._parameters.getSong,
responses: {
200: ssType._response.getSong,
},
},
getSongsByGenre: {
method: 'GET',
path: 'getSongsByGenre.view',
query: ssType._parameters.getSongsByGenre,
responses: {
200: ssType._response.getSongsByGenre,
},
},
getStarred: {
method: 'GET',
path: 'getStarred.view',
query: ssType._parameters.getStarred,
responses: {
200: ssType._response.getStarred,
},
},
getStructuredLyrics: {
method: 'GET',
path: 'getLyricsBySongId.view',
@@ -120,6 +216,14 @@ export const contract = c.router({
200: ssType._response.setRating,
},
},
updatePlaylist: {
method: 'GET',
path: 'updatePlaylist.view',
query: ssType._parameters.updatePlaylist,
responses: {
200: ssType._response.baseResponse,
},
},
});
const axiosClient = axios.create({});
@@ -151,7 +255,7 @@ axiosClient.interceptors.response.use(
const parsePath = (fullPath: string) => {
const [path, params] = fullPath.split('?');
const parsedParams = qs.parse(params);
const parsedParams = qs.parse(params, { arrayLimit: 99999, parameterLimit: 99999 });
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
return {
@@ -191,12 +295,12 @@ export const ssApiClient = (args: {
const token = server.credential;
const params = token.split(/&?\w=/gm);
authParams.u = server.username;
authParams.u = decodeURIComponent(server.username);
if (params?.length === 4) {
authParams.s = params[2];
authParams.t = params[3];
} else if (params?.length === 3) {
authParams.p = params[2];
authParams.p = decodeURIComponent(params[2]);
}
} else {
baseUrl = url;
@@ -242,7 +346,7 @@ export const ssApiClient = (args: {
return {
body: response?.data,
headers: response.headers as any,
headers: response?.headers as any,
status: response?.status,
};
}
File diff suppressed because it is too large Load Diff
+162 -57
View File
@@ -8,6 +8,9 @@ import {
Album,
ServerListItem,
ServerType,
Playlist,
Genre,
RelatedArtist,
} from '/@/renderer/api/types';
const getCoverArtUrl = (args: {
@@ -27,46 +30,119 @@ const getCoverArtUrl = (args: {
`?id=${args.coverArtId}` +
`&${args.credential}` +
'&v=1.13.0' +
'&c=feishin' +
'&c=Feishin' +
`&size=${size}`
);
};
const getArtists = (
item:
| z.infer<typeof ssType._response.song>
| z.infer<typeof ssType._response.album>
| z.infer<typeof ssType._response.albumListEntry>,
) => {
const albumArtists: RelatedArtist[] = item.albumArtists
? item.albumArtists.map((item) => ({
id: item.id.toString(),
imageUrl: null,
name: item.name,
}))
: [
{
id: item.artistId?.toString() || '',
imageUrl: null,
name: item.artist || '',
},
];
const artists: RelatedArtist[] = item.artists
? item.artists.map((item) => ({
id: item.id.toString(),
imageUrl: null,
name: item.name,
}))
: [
{
id: item.artistId?.toString() || '',
imageUrl: null,
name: item.artist || '',
},
];
let participants: Record<string, RelatedArtist[]> | null = null;
if (item.contributors) {
participants = {};
for (const contributor of item.contributors) {
const artist = {
id: contributor.artist.id?.toString() || '',
imageUrl: null,
name: contributor.artist.name || '',
};
const role = contributor.subRole
? `${contributor.role} (${contributor.subRole})`
: contributor.role;
if (role in participants) {
participants[role].push(artist);
} else {
participants[role] = [artist];
}
}
}
return { albumArtists, artists, participants };
};
const getGenres = (
item:
| z.infer<typeof ssType._response.song>
| z.infer<typeof ssType._response.album>
| z.infer<typeof ssType._response.albumListEntry>,
): Genre[] => {
return item.genres
? item.genres.map((genre) => ({
id: genre.name,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: genre.name,
}))
: item.genre
? [
{
id: item.genre,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.genre,
},
]
: [];
};
const normalizeSong = (
item: z.infer<typeof ssType._response.song>,
server: ServerListItem | null,
deviceId: string,
size?: number,
): QueueSong => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: 100,
size: size || 300,
}) || null;
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`;
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=Feishin&${server?.credential}`;
return {
album: item.album || '',
albumArtists: [
{
id: item.artistId || '',
imageUrl: null,
name: item.artist || '',
},
],
albumId: item.albumId || '',
albumId: item.albumId?.toString() || '',
artistName: item.artist || '',
artists: [
{
id: item.artistId || '',
imageUrl: null,
name: item.artist || '',
},
],
...getArtists(item),
bitRate: item.bitRate || 0,
bpm: null,
bpm: item.bpm || null,
channels: null,
comment: null,
compilation: null,
@@ -82,17 +158,8 @@ const normalizeSong = (
track: item.replayGain.trackGain,
}
: null,
genres: item.genre
? [
{
id: item.genre,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.genre,
},
]
: [],
id: item.id,
genres: getGenres(item),
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl,
itemType: LibraryItem.SONG,
@@ -123,15 +190,18 @@ const normalizeSong = (
};
const normalizeAlbumArtist = (
item: z.infer<typeof ssType._response.albumArtist>,
item:
| z.infer<typeof ssType._response.albumArtist>
| z.infer<typeof ssType._response.artistListEntry>,
server: ServerListItem | null,
imageSize?: number,
): AlbumArtist => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: 100,
size: imageSize || 100,
}) || null;
return {
@@ -140,7 +210,7 @@ const normalizeAlbumArtist = (
biography: null,
duration: null,
genres: [],
id: item.id,
id: item.id.toString(),
imageUrl,
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: null,
@@ -157,38 +227,27 @@ const normalizeAlbumArtist = (
};
const normalizeAlbum = (
item: z.infer<typeof ssType._response.album>,
item: z.infer<typeof ssType._response.album> | z.infer<typeof ssType._response.albumListEntry>,
server: ServerListItem | null,
imageSize?: number,
): Album => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: 300,
size: imageSize || 300,
}) || null;
return {
albumArtist: item.artist,
albumArtists: item.artistId
? [{ id: item.artistId, imageUrl: null, name: item.artist }]
: [],
artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
...getArtists(item),
backdropImageUrl: null,
comment: null,
createdAt: item.created,
duration: item.duration,
genres: item.genre
? [
{
id: item.genre,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.genre,
},
]
: [],
id: item.id,
duration: item.duration * 1000,
genres: getGenres(item),
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl,
isCompilation: null,
@@ -198,13 +257,16 @@ const normalizeAlbum = (
name: item.name,
originalDate: null,
playCount: null,
releaseDate: item.year ? new Date(item.year, 0, 1).toISOString() : null,
releaseDate: item.year ? new Date(Date.UTC(item.year, 0, 1)).toISOString() : null,
releaseYear: item.year ? Number(item.year) : null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: null,
songCount: item.songCount,
songs: [],
songs:
(item as z.infer<typeof ssType._response.album>).song?.map((song) =>
normalizeSong(song, server),
) || [],
uniqueId: nanoid(),
updatedAt: item.created,
userFavorite: item.starred || false,
@@ -212,8 +274,51 @@ const normalizeAlbum = (
};
};
const normalizePlaylist = (
item:
| z.infer<typeof ssType._response.playlist>
| z.infer<typeof ssType._response.playlistListEntry>,
server: ServerListItem | null,
): Playlist => {
return {
description: item.comment || null,
duration: item.duration,
genres: [],
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl: getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: 300,
}),
itemType: LibraryItem.PLAYLIST,
name: item.name,
owner: item.owner,
ownerId: item.owner,
public: item.public,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: null,
songCount: item.songCount,
};
};
const normalizeGenre = (item: z.infer<typeof ssType._response.genre>): Genre => {
return {
albumCount: item.albumCount,
id: item.value,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.value,
songCount: item.songCount,
};
};
export const ssNormalize = {
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
genre: normalizeGenre,
playlist: normalizePlaylist,
song: normalizeSong,
};
+294 -18
View File
@@ -19,6 +19,8 @@ const authenticateParameters = z.object({
v: z.string(),
});
const id = z.number().or(z.string());
const createFavoriteParameters = z.object({
albumId: z.array(z.string()).optional(),
artistId: z.array(z.string()).optional(),
@@ -43,7 +45,7 @@ const setRatingParameters = z.object({
const setRating = z.null();
const musicFolder = z.object({
id: z.string(),
id,
name: z.string(),
});
@@ -60,22 +62,43 @@ const songGain = z.object({
trackPeak: z.number().optional(),
});
const genreItem = z.object({
name: z.string(),
});
const simpleArtist = z.object({
id: z.string(),
name: z.string(),
});
const contributor = z.object({
artist: simpleArtist,
role: z.string(),
subRole: z.string().optional(),
});
const song = z.object({
album: z.string().optional(),
albumId: z.string().optional(),
albumArtists: z.array(simpleArtist),
albumId: id.optional(),
artist: z.string().optional(),
artistId: z.string().optional(),
artistId: id.optional(),
artists: z.array(simpleArtist),
averageRating: z.number().optional(),
bitRate: z.number().optional(),
bpm: z.number().optional(),
contentType: z.string(),
contributors: z.array(contributor).optional(),
coverArt: z.string().optional(),
created: z.string(),
discNumber: z.number(),
duration: z.number().optional(),
genre: z.string().optional(),
id: z.string(),
genres: z.array(genreItem).optional(),
id,
isDir: z.boolean(),
isVideo: z.boolean(),
musicBrainzId: z.string().optional(),
parent: z.string(),
path: z.string(),
playCount: z.number().optional(),
@@ -92,13 +115,18 @@ const song = z.object({
const album = z.object({
album: z.string(),
albumArtists: z.array(simpleArtist),
artist: z.string(),
artistId: z.string(),
artistId: id,
artists: z.array(simpleArtist),
contributors: z.array(contributor).optional(),
coverArt: z.string(),
created: z.string(),
duration: z.number(),
genre: z.string().optional(),
id: z.string(),
genres: z.array(genreItem).optional(),
id,
isCompilation: z.boolean().optional(),
isDir: z.boolean(),
isVideo: z.boolean(),
name: z.string(),
@@ -111,6 +139,10 @@ const album = z.object({
year: z.number().optional(),
});
const albumListEntry = album.omit({
song: true,
});
const albumListParameters = z.object({
fromYear: z.number().optional(),
genre: z.string().optional(),
@@ -124,11 +156,13 @@ const albumListParameters = z.object({
const albumList = z.array(album.omit({ song: true }));
const albumArtist = z.object({
album: z.array(album),
albumCount: z.string(),
artistImageUrl: z.string().optional(),
coverArt: z.string().optional(),
id: z.string(),
id,
name: z.string(),
starred: z.string().optional(),
});
const albumArtistList = z.object({
@@ -136,6 +170,14 @@ const albumArtistList = z.object({
name: z.string(),
});
const artistListEntry = albumArtist.pick({
albumCount: true,
coverArt: true,
id: true,
name: true,
starred: true,
});
const artistInfoParameters = z.object({
count: z.number().optional(),
id: z.string(),
@@ -168,9 +210,11 @@ const topSongsListParameters = z.object({
});
const topSongsList = z.object({
topSongs: z.object({
song: z.array(song),
}),
topSongs: z
.object({
song: z.array(song),
})
.optional(),
});
const scrobbleParameters = z.object({
@@ -182,11 +226,13 @@ const scrobbleParameters = z.object({
const scrobble = z.null();
const search3 = z.object({
searchResult3: z.object({
album: z.array(album),
artist: z.array(albumArtist),
song: z.array(song),
}),
searchResult3: z
.object({
album: z.array(album).optional(),
artist: z.array(albumArtist).optional(),
song: z.array(song).optional(),
})
.optional(),
});
const search3Parameters = z.object({
@@ -209,9 +255,11 @@ const randomSongListParameters = z.object({
});
const randomSongList = z.object({
randomSongs: z.object({
song: z.array(song),
}),
randomSongs: z
.object({
song: z.array(song),
})
.optional(),
});
const ping = z.object({
@@ -274,12 +322,223 @@ export enum SubsonicExtensions {
TRANSCODE_OFFSET = 'transcodeOffset',
}
const updatePlaylistParameters = z.object({
comment: z.string().optional(),
name: z.string().optional(),
playlistId: z.string(),
public: z.boolean().optional(),
songIdToAdd: z.array(z.string()).optional(),
songIndexToRemove: z.array(z.string()).optional(),
});
const getStarredParameters = z.object({
musicFolderId: z.string().optional(),
});
const getStarred = z.object({
starred: z
.object({
album: z.array(albumListEntry),
artist: z.array(artistListEntry),
song: z.array(song),
})
.optional(),
});
const getSongsByGenreParameters = z.object({
count: z.number().optional(),
genre: z.string(),
musicFolderId: z.string().optional(),
offset: z.number().optional(),
});
const getSongsByGenre = z.object({
songsByGenre: z
.object({
song: z.array(song),
})
.optional(),
});
const getAlbumParameters = z.object({
id: z.string(),
musicFolderId: z.string().optional(),
});
const getAlbum = z.object({
album,
});
const getArtistParameters = z.object({
id: z.string(),
});
const getArtist = z.object({
artist: albumArtist,
});
const getSongParameters = z.object({
id: z.string(),
});
const getSong = z.object({
song,
});
const getArtistsParameters = z.object({
musicFolderId: z.string().optional(),
});
const getArtists = z.object({
artists: z.object({
ignoredArticles: z.string(),
index: z.array(
z.object({
artist: z.array(artistListEntry),
name: z.string(),
}),
),
}),
});
const deletePlaylistParameters = z.object({
id: z.string(),
});
const createPlaylistParameters = z.object({
name: z.string(),
playlistId: z.string().optional(),
songId: z.array(z.string()).optional(),
});
const playlist = z.object({
changed: z.string().optional(),
comment: z.string().optional(),
coverArt: z.string().optional(),
created: z.string(),
duration: z.number(),
entry: z.array(song).optional(),
id,
name: z.string(),
owner: z.string(),
public: z.boolean(),
songCount: z.number(),
});
const createPlaylist = z.object({
playlist,
});
const getPlaylistsParameters = z.object({
username: z.string().optional(),
});
const playlistListEntry = playlist.omit({
entry: true,
});
const getPlaylists = z.object({
playlists: z
.object({
playlist: z.array(playlistListEntry),
})
.optional(),
});
const getPlaylistParameters = z.object({
id: z.string(),
});
const getPlaylist = z.object({
playlist,
});
const genre = z.object({
albumCount: z.number(),
songCount: z.number(),
value: z.string(),
});
const getGenresParameters = z.object({});
const getGenres = z.object({
genres: z
.object({
genre: z.array(genre),
})
.optional(),
});
export enum AlbumListSortType {
ALPHABETICAL_BY_ARTIST = 'alphabeticalByArtist',
ALPHABETICAL_BY_NAME = 'alphabeticalByName',
BY_GENRE = 'byGenre',
BY_YEAR = 'byYear',
FREQUENT = 'frequent',
NEWEST = 'newest',
RANDOM = 'random',
RECENT = 'recent',
STARRED = 'starred',
}
const getAlbumList2Parameters = z
.object({
fromYear: z.number().optional(),
genre: z.string().optional(),
musicFolderId: z.string().optional(),
offset: z.number().optional(),
size: z.number().optional(),
toYear: z.number().optional(),
type: z.nativeEnum(AlbumListSortType),
})
.refine(
(val) => {
if (val.type === AlbumListSortType.BY_YEAR) {
return val.fromYear !== undefined && val.toYear !== undefined;
}
return true;
},
{
message: 'Parameters "fromYear" and "toYear" are required when using sort "byYear"',
},
)
.refine(
(val) => {
if (val.type === AlbumListSortType.BY_GENRE) {
return val.genre !== undefined;
}
return true;
},
{ message: 'Parameter "genre" is required when using sort "byGenre"' },
);
const getAlbumList2 = z.object({
albumList2: z.object({
album: z.array(albumListEntry),
}),
});
export const ssType = {
_parameters: {
albumList: albumListParameters,
artistInfo: artistInfoParameters,
authenticate: authenticateParameters,
createFavorite: createFavoriteParameters,
createPlaylist: createPlaylistParameters,
deletePlaylist: deletePlaylistParameters,
getAlbum: getAlbumParameters,
getAlbumList2: getAlbumList2Parameters,
getArtist: getArtistParameters,
getArtists: getArtistsParameters,
getGenre: getGenresParameters,
getGenres: getGenresParameters,
getPlaylist: getPlaylistParameters,
getPlaylists: getPlaylistsParameters,
getSong: getSongParameters,
getSongsByGenre: getSongsByGenreParameters,
getStarred: getStarredParameters,
randomSongList: randomSongListParameters,
removeFavorite: removeFavoriteParameters,
scrobble: scrobbleParameters,
@@ -288,18 +547,35 @@ export const ssType = {
similarSongs: similarSongsParameters,
structuredLyrics: structuredLyricsParameters,
topSongsList: topSongsListParameters,
updatePlaylist: updatePlaylistParameters,
},
_response: {
album,
albumArtist,
albumArtistList,
albumList,
albumListEntry,
artistInfo,
artistListEntry,
authenticate,
baseResponse,
createFavorite,
createPlaylist,
genre,
getAlbum,
getAlbumList2,
getArtist,
getArtists,
getGenres,
getPlaylist,
getPlaylists,
getSong,
getSongsByGenre,
getStarred,
musicFolderList,
ping,
playlist,
playlistListEntry,
randomSongList,
removeFavorite,
scrobble,
+256 -37
View File
@@ -1,3 +1,6 @@
import orderBy from 'lodash/orderBy';
import reverse from 'lodash/reverse';
import shuffle from 'lodash/shuffle';
import { z } from 'zod';
import { ServerFeatures } from './features-types';
import { jfType } from './jellyfin/jellyfin-types';
@@ -128,7 +131,7 @@ export interface BasePaginatedResponse<T> {
error?: string | any;
items: T;
startIndex: number;
totalRecordCount: number;
totalRecordCount: number | null;
}
export type AuthenticationResponse = {
@@ -165,6 +168,7 @@ export type Album = {
mbzId: string | null;
name: string;
originalDate: string | null;
participants: Record<string, RelatedArtist[]> | null;
playCount: number | null;
releaseDate: string | null;
releaseYear: number | null;
@@ -209,6 +213,7 @@ export type Song = {
lastPlayedAt: string | null;
lyrics: string | null;
name: string;
participants: Record<string, RelatedArtist[]> | null;
path: string | null;
peak: GainInfo | null;
playCount: number;
@@ -309,6 +314,11 @@ type BaseEndpointArgs = {
};
};
export interface BaseQuery<T> {
sortBy: T;
sortOrder: SortOrder;
}
// Genre List
export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined;
@@ -318,7 +328,7 @@ export enum GenreListSort {
NAME = 'name',
}
export type GenreListQuery = {
export interface GenreListQuery extends BaseQuery<GenreListSort> {
_custom?: {
jellyfin?: null;
navidrome?: null;
@@ -326,10 +336,8 @@ export type GenreListQuery = {
limit?: number;
musicFolderId?: string;
searchTerm?: string;
sortBy: GenreListSort;
sortOrder: SortOrder;
startIndex: number;
};
}
type GenreListSortMap = {
jellyfin: Record<GenreListSort, JFGenreListSort | undefined>;
@@ -370,22 +378,22 @@ export enum AlbumListSort {
YEAR = 'year',
}
export type AlbumListQuery = {
export interface AlbumListQuery extends BaseQuery<AlbumListSort> {
_custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumList>> & {
maxYear?: number;
minYear?: number;
};
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumList>>;
navidrome?: Partial<z.infer<typeof ndType._parameters.albumList>>;
};
artistIds?: string[];
compilation?: boolean;
favorite?: boolean;
genres?: string[];
limit?: number;
maxYear?: number;
minYear?: number;
musicFolderId?: string;
searchTerm?: string;
sortBy: AlbumListSort;
sortOrder: SortOrder;
startIndex: number;
};
}
export type AlbumListArgs = { query: AlbumListQuery } & BaseEndpointArgs;
@@ -481,24 +489,23 @@ export enum SongListSort {
YEAR = 'year',
}
export type SongListQuery = {
export interface SongListQuery extends BaseQuery<SongListSort> {
_custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.songList>> & {
maxYear?: number;
minYear?: number;
};
jellyfin?: Partial<z.infer<typeof jfType._parameters.songList>>;
navidrome?: Partial<z.infer<typeof ndType._parameters.songList>>;
};
albumIds?: string[];
artistIds?: string[];
favorite?: boolean;
genreIds?: string[];
imageSize?: number;
limit?: number;
maxYear?: number;
minYear?: number;
musicFolderId?: string;
searchTerm?: string;
sortBy: SongListSort;
sortOrder: SortOrder;
startIndex: number;
};
}
export type SongListArgs = { query: SongListQuery } & BaseEndpointArgs;
@@ -595,7 +602,7 @@ export enum AlbumArtistListSort {
SONG_COUNT = 'songCount',
}
export type AlbumArtistListQuery = {
export interface AlbumArtistListQuery extends BaseQuery<AlbumArtistListSort> {
_custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumArtistList>>;
navidrome?: Partial<z.infer<typeof ndType._parameters.albumArtistList>>;
@@ -603,10 +610,8 @@ export type AlbumArtistListQuery = {
limit?: number;
musicFolderId?: string;
searchTerm?: string;
sortBy: AlbumArtistListSort;
sortOrder: SortOrder;
startIndex: number;
};
}
export type AlbumArtistListArgs = { query: AlbumArtistListQuery } & BaseEndpointArgs;
@@ -683,17 +688,15 @@ export enum ArtistListSort {
SONG_COUNT = 'songCount',
}
export type ArtistListQuery = {
export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
_custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumArtistList>>;
navidrome?: Partial<z.infer<typeof ndType._parameters.albumArtistList>>;
};
limit?: number;
musicFolderId?: string;
sortBy: ArtistListSort;
sortOrder: SortOrder;
startIndex: number;
};
}
export type ArtistListArgs = { query: ArtistListQuery } & BaseEndpointArgs;
@@ -879,17 +882,15 @@ export enum PlaylistListSort {
UPDATED_AT = 'updatedAt',
}
export type PlaylistListQuery = {
export interface PlaylistListQuery extends BaseQuery<PlaylistListSort> {
_custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.playlistList>>;
navidrome?: Partial<z.infer<typeof ndType._parameters.playlistList>>;
};
limit?: number;
searchTerm?: string;
sortBy: PlaylistListSort;
sortOrder: SortOrder;
startIndex: number;
};
}
export type PlaylistListArgs = { query: PlaylistListQuery } & BaseEndpointArgs;
@@ -963,7 +964,7 @@ export enum UserListSort {
NAME = 'name',
}
export type UserListQuery = {
export interface UserListQuery extends BaseQuery<UserListSort> {
_custom?: {
navidrome?: {
owner_id?: string;
@@ -971,10 +972,8 @@ export type UserListQuery = {
};
limit?: number;
searchTerm?: string;
sortBy: UserListSort;
sortOrder: SortOrder;
startIndex: number;
};
}
export type UserListArgs = { query: UserListQuery } & BaseEndpointArgs;
@@ -1228,3 +1227,223 @@ export type TranscodingQuery = {
export type TranscodingArgs = {
query: TranscodingQuery;
} & BaseEndpointArgs;
export type ControllerEndpoint = {
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
authenticate: (
url: string,
body: { legacy?: boolean; password: string; username: string },
) => Promise<AuthenticationResponse>;
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
getAlbumArtistListCount: (args: AlbumArtistListArgs) => Promise<number>;
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
// getArtistInfo?: (args: any) => void;
// getArtistList?: (args: ArtistListArgs) => Promise<ArtistListResponse>;
getDownloadUrl: (args: DownloadArgs) => string;
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
getLyrics?: (args: LyricsArgs) => Promise<LyricsResponse>;
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
getPlaylistListCount: (args: PlaylistListArgs) => Promise<number>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
getSimilarSongs: (args: SimilarSongsArgs) => Promise<Song[]>;
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
getSongListCount: (args: SongListArgs) => Promise<number>;
getStructuredLyrics?: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
getTranscodingUrl: (args: TranscodingArgs) => string;
getUserList?: (args: UserListArgs) => Promise<UserListResponse>;
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
search: (args: SearchArgs) => Promise<SearchResponse>;
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
shareItem?: (args: ShareItemArgs) => Promise<ShareItemResponse>;
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
};
export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: SortOrder) => {
let results = albums;
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case AlbumListSort.ALBUM_ARTIST:
results = orderBy(
results,
['albumArtist', (v) => v.name.toLowerCase()],
[order, 'asc'],
);
break;
case AlbumListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case AlbumListSort.FAVORITED:
results = orderBy(results, ['starred'], [order]);
break;
case AlbumListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case AlbumListSort.RANDOM:
results = shuffle(results);
break;
case AlbumListSort.RECENTLY_ADDED:
results = orderBy(results, ['createdAt'], [order]);
break;
case AlbumListSort.RECENTLY_PLAYED:
results = orderBy(results, ['lastPlayedAt'], [order]);
break;
case AlbumListSort.RATING:
results = orderBy(results, ['userRating'], [order]);
break;
case AlbumListSort.YEAR:
results = orderBy(results, ['releaseYear'], [order]);
break;
case AlbumListSort.SONG_COUNT:
results = orderBy(results, ['songCount'], [order]);
break;
default:
break;
}
return results;
};
export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder: SortOrder) => {
let results = songs;
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case SongListSort.ALBUM:
results = orderBy(
results,
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, 'asc', 'asc'],
);
break;
case SongListSort.ALBUM_ARTIST:
results = orderBy(
results,
['albumArtist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ARTIST:
results = orderBy(
results,
['artist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case SongListSort.FAVORITED:
results = orderBy(results, ['userFavorite', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.GENRE:
results = orderBy(
results,
[
(v) => v.genres?.[0].name.toLowerCase(),
(v) => v.album?.toLowerCase(),
'discNumber',
'trackNumber',
],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ID:
if (order === 'desc') {
results = reverse(results);
}
break;
case SongListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case SongListSort.RANDOM:
results = shuffle(results);
break;
case SongListSort.RATING:
results = orderBy(results, ['userRating', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.RECENTLY_ADDED:
results = orderBy(results, ['created'], [order]);
break;
case SongListSort.YEAR:
results = orderBy(
results,
['year', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
[order, 'asc', 'asc', 'asc'],
);
break;
default:
break;
}
return results;
};
export const sortAlbumArtistList = (
artists: AlbumArtist[],
sortBy: AlbumArtistListSort,
sortOrder: SortOrder,
) => {
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
let results = artists;
switch (sortBy) {
case AlbumArtistListSort.ALBUM_COUNT:
results = orderBy(artists, ['albumCount', (v) => v.name.toLowerCase()], [order, 'asc']);
break;
case AlbumArtistListSort.NAME:
results = orderBy(artists, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumArtistListSort.FAVORITED:
results = orderBy(artists, ['starred'], [order]);
break;
case AlbumArtistListSort.RATING:
results = orderBy(artists, ['userRating'], [order]);
break;
default:
break;
}
return results;
};
+13 -4
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react';
import { useEffect, useMemo, useState, useRef } from 'react';
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';
@@ -21,8 +21,9 @@ import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-han
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { PlayerState, useCssSettings, usePlayerStore, useQueueControls } from '/@/renderer/store';
import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types';
import { FontType, PlaybackType, PlayerStatus, WebAudio } from '/@/renderer/types';
import '@ag-grid-community/styles/ag-grid.css';
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
import i18n from '/@/i18n/i18n';
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
@@ -91,6 +92,8 @@ export const App = () => {
}
}, [builtIn, custom, system, type]);
const [webAudio, setWebAudio] = useState<WebAudio>();
useEffect(() => {
if (enabled && content) {
// Yes, CSS is sanitized here as well. Prevent a suer from changing the
@@ -125,6 +128,10 @@ export const App = () => {
return { handlePlayQueueAdd };
}, [handlePlayQueueAdd]);
const webAudioProvider = useMemo(() => {
return { setWebAudio, webAudio };
}, [webAudio]);
// Start the mpv instance on startup
useEffect(() => {
const initializeMpv = async () => {
@@ -136,7 +143,7 @@ export const App = () => {
if (!isRunning) {
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
const properties: Record<string, any> = {
speed: usePlayerStore.getState().current.speed,
speed: usePlayerStore.getState().speed,
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
};
@@ -278,7 +285,9 @@ export const App = () => {
>
<PlayQueueHandlerContext.Provider value={providerValue}>
<ContextMenuProvider>
<AppRouter />
<WebAudioContext.Provider value={webAudioProvider}>
<AppRouter />
</WebAudioContext.Provider>{' '}
</ContextMenuProvider>
</PlayQueueHandlerContext.Provider>
<IsUpdatedDialog />
+10 -11
View File
@@ -18,6 +18,7 @@ import {
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import type { CrossfadeStyle } from '/@/renderer/types';
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
import { getServerById, TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
import { toast } from '/@/renderer/components/toast';
import { api } from '/@/renderer/api';
@@ -44,11 +45,6 @@ const getDuration = (ref: any) => {
return ref.current?.player?.player?.player?.duration;
};
type WebAudio = {
context: AudioContext;
gain: GainNode;
};
// 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
@@ -116,7 +112,7 @@ export const AudioPlayer = forwardRef(
const [isTransitioning, setIsTransitioning] = useState(false);
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
const playback = useSettingsStore((state) => state.playback.mpvProperties);
const useWebAudio = useSettingsStore((state) => state.playback.webAudio);
const shouldUseWebAudio = useSettingsStore((state) => state.playback.webAudio);
const { resetSampleRate } = useSettingsStoreActions();
const playbackSpeed = useSpeed();
const { transcode } = usePlaybackSettings();
@@ -124,7 +120,7 @@ export const AudioPlayer = forwardRef(
const stream1 = useSongUrl(transcode, currentPlayer === 1, player1);
const stream2 = useSongUrl(transcode, currentPlayer === 2, player2);
const [webAudio, setWebAudio] = useState<WebAudio | null>(null);
const { webAudio, setWebAudio } = useWebAudio();
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
null,
);
@@ -181,7 +177,7 @@ export const AudioPlayer = forwardRef(
);
useEffect(() => {
if (useWebAudio && 'AudioContext' in window) {
if (shouldUseWebAudio && 'AudioContext' in window) {
let context: AudioContext;
try {
@@ -200,7 +196,7 @@ export const AudioPlayer = forwardRef(
const gain = context.createGain();
gain.connect(context.destination);
setWebAudio({ context, gain });
setWebAudio!({ context, gain });
return () => {
return context.close();
@@ -345,7 +341,7 @@ export const AudioPlayer = forwardRef(
// Set the current replaygain
if (current) {
const newVolume = calculateReplayGain(current) * volume;
webAudio.gain.gain.setValueAtTime(newVolume, 0);
webAudio.gain.gain.setValueAtTime(Math.max(0, newVolume), 0);
}
// Set the next track replaygain right before the end of this track
@@ -353,7 +349,10 @@ export const AudioPlayer = forwardRef(
const next = sources[3 - currentPlayer];
if (next && current) {
const newVolume = calculateReplayGain(next) * volume;
webAudio.gain.gain.setValueAtTime(newVolume, (current.duration - 1) / 1000);
webAudio.gain.gain.setValueAtTime(
Math.max(0, newVolume),
Math.max(0, (current.duration - 1) / 1000),
);
}
}, [
calculateReplayGain,
+1 -1
View File
@@ -202,7 +202,7 @@ export const AlbumCard = ({
<ImageSection />
</Skeleton>
<DetailSection style={{ width: '100%' }}>
{cardRows.map((_row: CardRow<Album>, index: number) => (
{(cardRows || []).map((_row: CardRow<Album>, index: number) => (
<Skeleton
visible
height={15}
+1 -1
View File
@@ -294,7 +294,7 @@ export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow<Playlist> } = {
name: {
property: 'name',
route: {
route: AppRoute.PLAYLISTS_DETAIL,
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
},
},
+1 -1
View File
@@ -191,7 +191,7 @@ export const PosterCard = ({
</Skeleton>
<DetailContainer>
<Stack spacing="sm">
{controls.cardRows.map((row, index) => (
{(controls?.cardRows || []).map((row, index) => (
<Skeleton
key={`${index}-${row.arrayProperty}`}
visible
@@ -207,7 +207,11 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
</Badge>
))}
<Badge size="lg">{currentItem?.releaseYear}</Badge>
<Badge size="lg">{currentItem?.songCount} tracks</Badge>
<Badge size="lg">
{t('entity.trackWithCount', {
count: currentItem?.songCount || 0,
})}
</Badge>
</Group>
<Group position="apart">
<Button
@@ -11,7 +11,7 @@ const Container = styled(motion(Flex))<{
$position?: string;
}>`
position: ${(props) => props.$position || 'relative'};
z-index: 200;
z-index: 190;
width: 100%;
height: ${(props) => props.$height || '65px'};
background: var(--titlebar-bg);
+1 -1
View File
@@ -28,7 +28,7 @@ export const Spoiler = ({ maxHeight, defaultOpened, children, ...props }: Spoile
ref={ref}
className={spoilerClassNames}
role="button"
style={{ maxHeight: maxHeight ?? '100px' }}
style={{ maxHeight: maxHeight ?? '100px', whiteSpace: 'pre-wrap' }}
tabIndex={-1}
onClick={handleToggleExpand}
{...props}
@@ -234,7 +234,7 @@ export const DefaultCard = ({
</ImageContainer>
<DetailContainer>
<Stack spacing="sm">
{controls.cardRows.map((row, index) => (
{(controls?.cardRows || []).map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}
visible
@@ -219,7 +219,7 @@ export const PosterCard = ({
</Skeleton>
<DetailContainer>
<Stack spacing="sm">
{controls.cardRows.map((row, index) => (
{(controls?.cardRows || []).map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}
visible
@@ -0,0 +1,97 @@
import React, { MouseEvent } from 'react';
import type { UnstyledButtonProps } from '@mantine/core';
import { RiPlayFill } from 'react-icons/ri';
import styled from 'styled-components';
import { Play } from '/@/renderer/types';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { LibraryItem } from '/@/renderer/api/types';
import { usePlayQueueAdd } from '/@/renderer/features/player';
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
const PlayButton = styled.button<PlayButtonType>`
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
background-color: rgb(255 255 255);
border: none;
border-radius: 50%;
opacity: 0.8;
transition: scale 0.1s ease-in-out;
&:hover {
opacity: 1;
scale: 1.1;
}
&:active {
opacity: 1;
scale: 1;
}
svg {
fill: rgb(0 0 0);
stroke: rgb(0 0 0);
}
`;
const ListConverControlsContainer = styled.div`
position: absolute;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
`;
export const ListCoverControls = ({
itemData,
itemType,
context,
uniqueId,
}: {
context: Record<string, any>;
itemData: any;
itemType: LibraryItem;
uniqueId?: string;
}) => {
const playButtonBehavior = usePlayButtonBehavior();
const handlePlayQueueAdd = usePlayQueueAdd();
const isQueue = Boolean(context?.isQueue);
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
e.preventDefault();
e.stopPropagation();
handlePlayQueueAdd?.({
byItemType: {
id: [itemData.id],
type: itemType,
},
playType: playType || playButtonBehavior,
});
};
const handlePlayFromQueue = () => {
context.handleDoubleClick({
data: {
uniqueId,
},
});
};
return (
<>
<ListConverControlsContainer className="card-controls">
<PlayButton onClick={isQueue ? handlePlayFromQueue : handlePlay}>
<RiPlayFill size={20} />
</PlayButton>
</ListConverControlsContainer>
</>
);
};
@@ -7,11 +7,12 @@ import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import { SimpleImg } from 'react-simple-img';
import styled from 'styled-components';
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
import { AlbumArtist, Artist } from '/@/renderer/api/types';
import { Text } from '/@/renderer/components/text';
import { AppRoute } from '/@/renderer/router/routes';
import { Skeleton } from '/@/renderer/components/skeleton';
import { SEPARATOR_STRING } from '/@/renderer/api/utils';
import { ListCoverControls } from '/@/renderer/components/virtual-table/cells/combined-title-cell-controls';
const CellContainer = styled(motion.div)<{ height: number }>`
display: grid;
@@ -24,9 +25,20 @@ const CellContainer = styled(motion.div)<{ height: number }>`
max-width: 100%;
height: 100%;
letter-spacing: 0.5px;
.card-controls {
opacity: 0;
}
&:hover {
.card-controls {
opacity: 1;
}
}
`;
const ImageWrapper = styled.div`
position: relative;
display: flex;
grid-area: image;
align-items: center;
@@ -48,7 +60,13 @@ const StyledImage = styled(SimpleImg)`
}
`;
export const CombinedTitleCell = ({ value, rowIndex, node }: ICellRendererParams) => {
export const CombinedTitleCell = ({
value,
rowIndex,
node,
context,
data,
}: ICellRendererParams) => {
const artists = useMemo(() => {
if (!value) return null;
return value.artists?.length ? value.artists : value.albumArtists;
@@ -102,6 +120,12 @@ export const CombinedTitleCell = ({ value, rowIndex, node }: ICellRendererParams
/>
</Center>
)}
<ListCoverControls
context={context}
itemData={value}
itemType={context.itemType}
uniqueId={data?.uniqueId}
/>
</ImageWrapper>
<MetadataWrapper>
<Text
@@ -2,95 +2,95 @@ import type { ICellRendererParams } from '@ag-grid-community/core';
import { Text } from '/@/renderer/components/text';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
const AnimatedSvg = () => {
return (
<div style={{ height: '1rem', transform: 'rotate(180deg)', width: '1rem' }}>
<svg
viewBox="100 130 57 80"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<rect
fill="var(--primary-color)"
height="80"
id="bar-1"
width="12"
x="100"
y="130"
>
<animate
attributeName="height"
begin="0.1s"
calcMode="spline"
dur="0.95s"
keySplines="0.42 0 0.58 1; 0.42 0 0.58 1"
keyTimes="0; 0.47368; 1"
repeatCount="indefinite"
values="80;15;80"
/>
</rect>
<rect
fill="var(--primary-color)"
height="80"
id="bar-2"
width="12"
x="115"
y="130"
>
<animate
attributeName="height"
begin="0.1s"
calcMode="spline"
dur="0.95s"
keySplines="0.45 0 0.55 1; 0.45 0 0.55 1"
keyTimes="0; 0.44444; 1"
repeatCount="indefinite"
values="25;80;25"
/>
</rect>
<rect
fill="var(--primary-color)"
height="80"
id="bar-3"
width="12"
x="130"
y="130"
>
<animate
attributeName="height"
begin="0.1s"
calcMode="spline"
dur="0.85s"
keySplines="0.65 0 0.35 1; 0.65 0 0.35 1"
keyTimes="0; 0.42105; 1"
repeatCount="indefinite"
values="80;10;80"
/>
</rect>
<rect
fill="var(--primary-color)"
height="80"
id="bar-4"
width="12"
x="145"
y="130"
>
<animate
attributeName="height"
begin="0.1s"
calcMode="spline"
dur="1.05s"
keySplines="0.42 0 0.58 1; 0.42 0 0.58 1"
keyTimes="0; 0.31579; 1"
repeatCount="indefinite"
values="30;80;30"
/>
</rect>
</g>
</svg>
</div>
);
};
// const AnimatedSvg = () => {
// return (
// <div style={{ height: '1rem', transform: 'rotate(180deg)', width: '1rem' }}>
// <svg
// viewBox="100 130 57 80"
// xmlns="http://www.w3.org/2000/svg"
// >
// <g>
// <rect
// fill="var(--primary-color)"
// height="80"
// id="bar-1"
// width="12"
// x="100"
// y="130"
// >
// <animate
// attributeName="height"
// begin="0.1s"
// calcMode="spline"
// dur="0.95s"
// keySplines="0.42 0 0.58 1; 0.42 0 0.58 1"
// keyTimes="0; 0.47368; 1"
// repeatCount="indefinite"
// values="80;15;80"
// />
// </rect>
// <rect
// fill="var(--primary-color)"
// height="80"
// id="bar-2"
// width="12"
// x="115"
// y="130"
// >
// <animate
// attributeName="height"
// begin="0.1s"
// calcMode="spline"
// dur="0.95s"
// keySplines="0.45 0 0.55 1; 0.45 0 0.55 1"
// keyTimes="0; 0.44444; 1"
// repeatCount="indefinite"
// values="25;80;25"
// />
// </rect>
// <rect
// fill="var(--primary-color)"
// height="80"
// id="bar-3"
// width="12"
// x="130"
// y="130"
// >
// <animate
// attributeName="height"
// begin="0.1s"
// calcMode="spline"
// dur="0.85s"
// keySplines="0.65 0 0.35 1; 0.65 0 0.35 1"
// keyTimes="0; 0.42105; 1"
// repeatCount="indefinite"
// values="80;10;80"
// />
// </rect>
// <rect
// fill="var(--primary-color)"
// height="80"
// id="bar-4"
// width="12"
// x="145"
// y="130"
// >
// <animate
// attributeName="height"
// begin="0.1s"
// calcMode="spline"
// dur="1.05s"
// keySplines="0.42 0 0.58 1; 0.42 0 0.58 1"
// keyTimes="0; 0.31579; 1"
// repeatCount="indefinite"
// values="30;80;30"
// />
// </rect>
// </g>
// </svg>
// </div>
// );
// };
const StaticSvg = () => {
return (
@@ -134,19 +134,14 @@ const StaticSvg = () => {
export const RowIndexCell = ({ value, eGridCell }: ICellRendererParams) => {
const classList = eGridCell.classList;
const isFocused = classList.contains('focused');
// const isFocused = classList.contains('focused');
const isPlaying = classList.contains('playing');
const isCurrentSong =
classList.contains('current-song-cell') || classList.contains('current-playlist-song-cell');
return (
<CellContainer $position="right">
{isPlaying &&
(isFocused && isCurrentSong ? (
<AnimatedSvg />
) : isCurrentSong ? (
<StaticSvg />
) : null)}
{isPlaying && (isCurrentSong ? <StaticSvg /> : null)}
<Text
$secondary
align="right"
@@ -7,7 +7,6 @@ import {
IDatasource,
PaginationChangedEvent,
RowDoubleClickedEvent,
RowModelType,
} from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { QueryKey, useQueryClient } from '@tanstack/react-query';
@@ -16,7 +15,12 @@ import orderBy from 'lodash/orderBy';
import { generatePath, useNavigate } from 'react-router';
import { api } from '/@/renderer/api';
import { QueryPagination, queryKeys } from '/@/renderer/api/query-keys';
import { BasePaginatedResponse, LibraryItem, ServerListItem } from '/@/renderer/api/types';
import {
BasePaginatedResponse,
BaseQuery,
LibraryItem,
ServerListItem,
} from '/@/renderer/api/types';
import { getColumnDefs, VirtualTableProps } from '/@/renderer/components/virtual-table';
import { SetContextMenuItems, useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import { AppRoute } from '/@/renderer/router/routes';
@@ -34,6 +38,7 @@ interface UseAgGridProps<TFilter> {
columnType?: 'albumDetail' | 'generic';
contextMenu: SetContextMenuItems;
customFilters?: Partial<TFilter>;
isClientSide?: boolean;
isClientSideSort?: boolean;
isSearchParams?: boolean;
itemCount?: number;
@@ -43,7 +48,9 @@ interface UseAgGridProps<TFilter> {
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const useVirtualTable = <TFilter>({
const BLOCK_SIZE = 500;
export const useVirtualTable = <TFilter extends BaseQuery<any>>({
server,
tableRef,
pageKey,
@@ -52,13 +59,14 @@ export const useVirtualTable = <TFilter>({
itemCount,
customFilters,
isSearchParams,
isClientSide,
isClientSideSort,
columnType,
}: UseAgGridProps<TFilter>) => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { setTable, setTablePagination } = useListStoreActions();
const properties = useListStoreByKey({ filter: customFilters, key: pageKey });
const properties = useListStoreByKey<TFilter>({ filter: customFilters, key: pageKey });
const [searchParams, setSearchParams] = useSearchParams();
const scrollOffset = searchParams.get('scrollOffset');
@@ -182,6 +190,19 @@ export const useVirtualTable = <TFilter>({
return;
}
if (results.totalRecordCount === null) {
const hasMoreRows = results?.items?.length === BLOCK_SIZE;
const lastRowIndex = hasMoreRows
? undefined
: params.startRow + results.items.length;
params.successCallback(
results?.items || [],
hasMoreRows ? undefined : lastRowIndex,
);
return;
}
params.successCallback(results?.items || [], results?.totalRecordCount || 0);
},
rowCount: undefined,
@@ -313,6 +334,7 @@ export const useVirtualTable = <TFilter>({
const onCellContextMenu = useHandleTableContextMenu(itemType, contextMenu);
const context = {
itemType,
onCellContextMenu,
};
@@ -321,6 +343,7 @@ export const useVirtualTable = <TFilter>({
alwaysShowHorizontalScroll: true,
autoFitColumns: properties.table.autoFit,
blockLoadDebounceMillis: 200,
cacheBlockSize: BLOCK_SIZE,
getRowId: (data: GetRowIdParams<any>) => data.data.id,
infiniteInitialRowCount: itemCount || 100,
pagination: isPaginationEnabled,
@@ -335,10 +358,11 @@ export const useVirtualTable = <TFilter>({
: undefined,
rowBuffer: 20,
rowHeight: properties.table.rowHeight || 40,
rowModelType: 'infinite' as RowModelType,
rowModelType: isClientSide ? 'clientSide' : 'infinite',
suppressRowDrag: true,
};
}, [
isClientSide,
isPaginationEnabled,
isSearchParams,
itemCount,
@@ -370,7 +394,9 @@ export const useVirtualTable = <TFilter>({
);
break;
case LibraryItem.PLAYLIST:
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL, { playlistId: e.data.id }));
navigate(
generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: e.data.id }),
);
break;
default:
break;
@@ -41,7 +41,12 @@ import { useFixedTableHeader } from '/@/renderer/components/virtual-table/hooks/
import { NoteCell } from '/@/renderer/components/virtual-table/cells/note-cell';
import { RowIndexCell } from '/@/renderer/components/virtual-table/cells/row-index-cell';
import i18n from '/@/i18n/i18n';
import { formatDateAbsolute, formatDateRelative, formatSizeString } from '/@/renderer/utils/format';
import {
formatDateAbsolute,
formatDateAbsoluteUTC,
formatDateRelative,
formatSizeString,
} from '/@/renderer/utils/format';
import { useTableChange } from '/@/renderer/hooks/use-song-change';
export * from './table-config-dropdown';
@@ -253,7 +258,7 @@ const tableColumns: { [key: string]: ColDef } = {
GenericTableHeader(params, { position: 'center' }),
headerName: i18n.t('table.column.releaseDate'),
suppressSizeToFit: true,
valueFormatter: (params: ValueFormatterParams) => formatDateAbsolute(params.value),
valueFormatter: (params: ValueFormatterParams) => formatDateAbsoluteUTC(params.value),
valueGetter: (params: ValueGetterParams) =>
params.data ? params.data.releaseDate : undefined,
width: 130,
@@ -356,6 +361,7 @@ const tableColumns: { [key: string]: ColDef } = {
? {
albumArtists: params.data?.albumArtists,
artists: params.data?.artists,
id: params.data?.id,
imagePlaceholderUrl: params.data?.imagePlaceholderUrl,
imageUrl: params.data?.imageUrl,
name: params.data?.name,
@@ -6,6 +6,7 @@ import { useSettingsStoreActions, useSettingsStore } from '/@/renderer/store/set
import { TableColumn, TableType } from '/@/renderer/types';
import { Option } from '/@/renderer/components/option';
import i18n from '/@/i18n/i18n';
import { useTranslation } from 'react-i18next';
export const SONG_TABLE_COLUMNS = [
{
@@ -285,6 +286,7 @@ interface TableConfigDropdownProps {
}
export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
const { t } = useTranslation();
const { setSettings } = useSettingsStoreActions();
const tableConfig = useSettingsStore((state) => state.tables);
@@ -374,7 +376,9 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
return (
<>
<Option>
<Option.Label>Auto-fit Columns</Option.Label>
<Option.Label>
{t('table.config.general.autoFitColumns', { postProcess: 'sentenceCase' })}
</Option.Label>
<Option.Control>
<Switch
defaultChecked={tableConfig[type]?.autoFit}
@@ -384,7 +388,11 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
</Option>
{type !== 'albumDetail' && (
<Option>
<Option.Label>Follow current song</Option.Label>
<Option.Label>
{t('table.config.general.followCurrentSong', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Switch
defaultChecked={tableConfig[type]?.followCurrentSong}
@@ -37,7 +37,7 @@ const ActionRequiredRoute = () => {
const handleManageServersModal = () => {
openModal({
children: <ServerList />,
title: 'Manage Servers',
title: t('page.appMenu.manageServers', { postProcess: 'sentenceCase' }),
});
};
@@ -11,7 +11,13 @@ import { generatePath, useParams } from 'react-router';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/api/types';
import {
AlbumListQuery,
AlbumListSort,
LibraryItem,
QueueSong,
SortOrder,
} from '/@/renderer/api/types';
import { Button, Popover, Spoiler } from '/@/renderer/components';
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel';
import {
@@ -164,13 +170,12 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
query: {
_custom: {
jellyfin: {
AlbumArtistIds: detailQuery?.data?.albumArtists[0]?.id,
ExcludeItemIds: detailQuery?.data?.id,
},
navidrome: {
artist_id: detailQuery?.data?.albumArtists[0]?.id,
},
},
artistIds: detailQuery?.data?.albumArtists.length
? [detailQuery?.data?.albumArtists[0].id]
: undefined,
limit: 15,
sortBy: AlbumListSort.YEAR,
sortOrder: SortOrder.DESC,
@@ -179,15 +184,8 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
serverId: server?.id,
});
const relatedAlbumGenresRequest = {
_custom: {
jellyfin: {
GenreIds: detailQuery?.data?.genres?.[0]?.id,
},
navidrome: {
genre_id: detailQuery?.data?.genres?.[0]?.id,
},
},
const relatedAlbumGenresRequest: AlbumListQuery = {
genres: detailQuery.data?.genres.length ? [detailQuery.data.genres[0].id] : undefined,
limit: 15,
sortBy: AlbumListSort.RANDOM,
sortOrder: SortOrder.ASC,
@@ -466,6 +464,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
context={{
currentSong,
isFocused,
itemType: LibraryItem.SONG,
onCellContextMenu,
status,
}}
@@ -1,16 +1,19 @@
import { forwardRef, Fragment, Ref } from 'react';
import { forwardRef, Fragment, Ref, useCallback, useMemo } from 'react';
import { Group, Stack } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { generatePath, useParams } from 'react-router';
import { Link } from 'react-router-dom';
import { LibraryItem, ServerType } from '/@/renderer/api/types';
import { AlbumDetailResponse, LibraryItem, ServerType } from '/@/renderer/api/types';
import { Rating, Text } from '/@/renderer/components';
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query';
import { LibraryHeader, useSetRating } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { formatDateAbsolute, formatDurationString } from '/@/renderer/utils';
import { formatDateAbsoluteUTC, formatDurationString } from '/@/renderer/utils';
import { useSongChange } from '/@/renderer/hooks/use-song-change';
import { queryKeys } from '/@/renderer/api/query-keys';
import { queryClient } from '/@/renderer/lib/react-query';
interface AlbumDetailHeaderProps {
background: {
@@ -37,16 +40,48 @@ export const AlbumDetailHeader = forwardRef(
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
: '♫';
const songIds = useMemo(() => {
return new Set(detailQuery.data?.songs?.map((song) => song.id));
}, [detailQuery.data?.songs]);
const handleSongChange = useCallback(
(id: string) => {
if (songIds.has(id)) {
const queryKey = queryKeys.albums.detail(server?.id, { id: albumId });
queryClient.setQueryData<AlbumDetailResponse | undefined>(
queryKey,
(previous) => {
if (!previous) return undefined;
return {
...previous,
playCount: previous.playCount ? previous.playCount + 1 : 1,
};
},
);
}
},
[albumId, server?.id, songIds],
);
useSongChange((ids, event) => {
if (event.event === 'play') {
handleSongChange(ids[0]);
}
}, detailQuery.data !== undefined);
const metadataItems = [
{
id: 'releaseDate',
value:
detailQuery?.data?.releaseDate &&
`${releasePrefix} ${formatDateAbsolute(detailQuery?.data?.releaseDate)}`,
`${releasePrefix} ${formatDateAbsoluteUTC(detailQuery?.data?.releaseDate)}`,
},
{
id: 'songCount',
value: `${detailQuery?.data?.songCount} songs`,
value: `${detailQuery?.data?.songCount} ${t('entity.track_other', {
count: detailQuery?.data?.songCount as number,
})}`,
},
{
id: 'duration',
@@ -62,7 +97,7 @@ export const AlbumDetailHeader = forwardRef(
];
if (originalDifferentFromRelease) {
const formatted = `${formatDateAbsolute(detailQuery!.data!.originalDate)}`;
const formatted = `${formatDateAbsoluteUTC(detailQuery!.data!.originalDate)}`;
metadataItems.splice(0, 0, {
id: 'originalDate',
value: formatted,
@@ -29,7 +29,7 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
const server = useCurrentServer();
const handlePlayQueueAdd = usePlayQueueAdd();
const { pageKey, customFilters, id } = useListContext();
const { grid, display, filter } = useListStoreByKey({ key: pageKey });
const { grid, display, filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
const { setGrid } = useListStoreActions();
const [searchParams, setSearchParams] = useSearchParams();
@@ -162,9 +162,9 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
const query: AlbumListQuery = {
limit: take,
startIndex: skip,
...filter,
...customFilters,
startIndex: skip,
};
const queryKey = queryKeys.albums.list(server?.id || '', query, id);
@@ -15,13 +15,20 @@ import {
RiSettings3Fill,
} from 'react-icons/ri';
import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
import {
AlbumListQuery,
AlbumListSort,
LibraryItem,
ServerType,
SortOrder,
} from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { useListContext } from '/@/renderer/context/list-context';
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
import { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters';
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks';
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
@@ -139,26 +146,74 @@ const FILTERS = {
value: AlbumListSort.YEAR,
},
],
subsonic: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: AlbumListSort.ALBUM_ARTIST,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.PLAY_COUNT,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumListSort.RANDOM,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_ADDED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_PLAYED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
value: AlbumListSort.FAVORITED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
value: AlbumListSort.YEAR,
},
],
};
interface AlbumListHeaderFiltersProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount: number | undefined;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFiltersProps) => {
export const AlbumListHeaderFilters = ({
gridRef,
itemCount,
tableRef,
}: AlbumListHeaderFiltersProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { pageKey, customFilters, handlePlay } = useListContext();
const server = useCurrentServer();
const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions();
const { display, filter, table, grid } = useListStoreByKey({
const { display, filter, table, grid } = useListStoreByKey<AlbumListQuery>({
filter: customFilters,
key: pageKey,
});
const cq = useContainerQuery();
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
itemCount,
itemType: LibraryItem.ALBUM,
server,
});
@@ -191,27 +246,35 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
);
const handleOpenFiltersModal = () => {
let FilterComponent;
switch (server?.type) {
case ServerType.NAVIDROME:
FilterComponent = NavidromeAlbumFilters;
break;
case ServerType.JELLYFIN:
FilterComponent = JellyfinAlbumFilters;
break;
case ServerType.SUBSONIC:
FilterComponent = SubsonicAlbumFilters;
break;
default:
break;
}
if (!FilterComponent) {
return;
}
openModal({
children: (
<>
{server?.type === ServerType.NAVIDROME ? (
<NavidromeAlbumFilters
customFilters={customFilters}
disableArtistFilter={!!customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
) : (
<JellyfinAlbumFilters
customFilters={customFilters}
disableArtistFilter={!!customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
)}
</>
<FilterComponent
customFilters={customFilters}
disableArtistFilter={!!customFilters}
pageKey={pageKey}
serverId={server?.id}
onFilterChange={onFilterChange}
/>
),
title: 'Album Filters',
});
@@ -347,8 +410,20 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
filter?._custom?.jellyfin &&
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined);
return isNavidromeFilterApplied || isJellyfinFilterApplied;
}, [filter?._custom?.jellyfin, filter?._custom?.navidrome, server?.type]);
const isSubsonicFilterApplied =
server?.type === ServerType.SUBSONIC &&
(filter.maxYear || filter.minYear || filter.genres?.length || filter.favorite);
return isNavidromeFilterApplied || isJellyfinFilterApplied || isSubsonicFilterApplied;
}, [
filter?._custom?.jellyfin,
filter?._custom?.navidrome,
filter.favorite,
filter.genres?.length,
filter.maxYear,
filter.minYear,
server?.type,
]);
const isFolderFilterApplied = useMemo(() => {
return filter.musicFolderId !== undefined;
@@ -436,7 +511,7 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
},
}}
tooltip={{
label: t('common.filter', { count: 2, postProcess: 'sentenceCase' }),
label: t('common.filters', { count: 2, postProcess: 'sentenceCase' }),
}}
variant="subtle"
onClick={handleOpenFiltersModal}
@@ -514,7 +589,9 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Label>Display type</DropdownMenu.Label>
<DropdownMenu.Label>
{t('table.config.general.displayType', { postProcess: 'sentenceCase' })}
</DropdownMenu.Label>
<DropdownMenu.Item
$isActive={display === ListDisplayType.CARD}
value={ListDisplayType.CARD}
@@ -3,7 +3,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { Flex, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
import { LibraryItem } from '/@/renderer/api/types';
import { AlbumListQuery, LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
@@ -33,8 +33,9 @@ export const AlbumListHeader = ({
const cq = useContainerQuery();
const playButtonBehavior = usePlayButtonBehavior();
const genreRef = useRef<string>();
const { filter, handlePlay, refresh, search } = useDisplayRefresh({
const { filter, handlePlay, refresh, search } = useDisplayRefresh<AlbumListQuery>({
gridRef,
itemCount,
itemType: LibraryItem.ALBUM,
server,
tableRef,
@@ -90,6 +91,7 @@ export const AlbumListHeader = ({
<FilterBar>
<AlbumListHeaderFilters
gridRef={gridRef}
itemCount={itemCount}
tableRef={tableRef}
/>
</FilterBar>
@@ -3,7 +3,13 @@ import { Divider, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
import { useListFilterByKey } from '../../../store/list.store';
import { AlbumArtistListSort, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import {
AlbumArtistListSort,
AlbumListQuery,
GenreListSort,
LibraryItem,
SortOrder,
} from '/@/renderer/api/types';
import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { useGenreList } from '/@/renderer/features/genres';
@@ -25,7 +31,7 @@ export const JellyfinAlbumFilters = ({
serverId,
}: JellyfinAlbumFiltersProps) => {
const { t } = useTranslation();
const filter = useListFilterByKey({ key: pageKey });
const filter = useListFilterByKey<AlbumListQuery>({ key: pageKey });
const { setFilter } = useListStoreActions();
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
@@ -47,10 +53,6 @@ export const JellyfinAlbumFilters = ({
}));
}, [genreListQuery.data]);
const selectedGenres = useMemo(() => {
return filter?._custom?.jellyfin?.GenreIds?.split(',');
}, [filter?._custom?.jellyfin?.GenreIds]);
const toggleFilters = [
{
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
@@ -58,20 +60,15 @@ export const JellyfinAlbumFilters = ({
const updatedFilters = setFilter({
customFilters,
data: {
_custom: {
...filter?._custom,
jellyfin: {
...filter?._custom?.jellyfin,
IsFavorite: e.currentTarget.checked ? true : undefined,
},
},
_custom: filter?._custom,
favorite: e.currentTarget.checked ? true : undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
},
value: filter?._custom?.jellyfin?.IsFavorite,
value: filter?.favorite,
},
];
@@ -80,13 +77,8 @@ export const JellyfinAlbumFilters = ({
const updatedFilters = setFilter({
customFilters,
data: {
_custom: {
...filter?._custom,
jellyfin: {
...filter?._custom?.jellyfin,
minYear: e === '' ? undefined : (e as number),
},
},
_custom: filter?._custom,
minYear: e === '' ? undefined : (e as number),
},
itemType: LibraryItem.ALBUM,
key: pageKey,
@@ -99,13 +91,8 @@ export const JellyfinAlbumFilters = ({
const updatedFilters = setFilter({
customFilters,
data: {
_custom: {
...filter?._custom,
jellyfin: {
...filter?._custom?.jellyfin,
maxYear: e === '' ? undefined : (e as number),
},
},
_custom: filter?._custom,
maxYear: e === '' ? undefined : (e as number),
},
itemType: LibraryItem.ALBUM,
key: pageKey,
@@ -114,17 +101,11 @@ export const JellyfinAlbumFilters = ({
}, 500);
const handleGenresFilter = debounce((e: string[] | undefined) => {
const genreFilterString = e?.length ? e.join(',') : undefined;
const updatedFilters = setFilter({
customFilters,
data: {
_custom: {
...filter?._custom,
jellyfin: {
...filter?._custom?.jellyfin,
GenreIds: genreFilterString,
},
},
_custom: filter?._custom,
genres: e,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
@@ -157,17 +138,11 @@ export const JellyfinAlbumFilters = ({
}, [albumArtistListQuery?.data?.items]);
const handleAlbumArtistFilter = (e: string[] | null) => {
const albumArtistFilterString = e?.length ? e.join(',') : undefined;
const updatedFilters = setFilter({
customFilters,
data: {
_custom: {
...filter?._custom,
jellyfin: {
...filter?._custom?.jellyfin,
AlbumArtistIds: albumArtistFilterString,
},
},
_custom: filter?._custom,
artistIds: e || undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
@@ -193,21 +168,21 @@ export const JellyfinAlbumFilters = ({
<Divider my="0.5rem" />
<Group grow>
<NumberInput
defaultValue={filter?._custom?.jellyfin?.minYear}
defaultValue={filter?.minYear}
hideControls={false}
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
max={2300}
min={1700}
required={!!filter?._custom?.jellyfin?.maxYear}
required={!!filter?.maxYear}
onChange={(e) => handleMinYearFilter(e)}
/>
<NumberInput
defaultValue={filter?._custom?.jellyfin?.maxYear}
defaultValue={filter?.maxYear}
hideControls={false}
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
max={2300}
min={1700}
required={!!filter?._custom?.jellyfin?.minYear}
required={!!filter?.minYear}
onChange={(e) => handleMaxYearFilter(e)}
/>
</Group>
@@ -216,7 +191,7 @@ export const JellyfinAlbumFilters = ({
clearable
searchable
data={genreList}
defaultValue={selectedGenres}
defaultValue={filter.genres}
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
onChange={handleGenresFilter}
/>
@@ -5,7 +5,13 @@ import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/rend
import debounce from 'lodash/debounce';
import { useGenreList } from '/@/renderer/features/genres';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { AlbumArtistListSort, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import {
AlbumArtistListSort,
AlbumListQuery,
GenreListSort,
LibraryItem,
SortOrder,
} from '/@/renderer/api/types';
import { useTranslation } from 'react-i18next';
interface NavidromeAlbumFiltersProps {
@@ -24,7 +30,7 @@ export const NavidromeAlbumFilters = ({
serverId,
}: NavidromeAlbumFiltersProps) => {
const { t } = useTranslation();
const { filter } = useListStoreByKey({ key: pageKey });
const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
const { setFilter } = useListStoreActions();
const genreListQuery = useGenreList({
@@ -48,13 +54,8 @@ export const NavidromeAlbumFilters = ({
const updatedFilters = setFilter({
customFilters,
data: {
_custom: {
...filter._custom,
navidrome: {
...filter._custom?.navidrome,
genre_id: e || undefined,
},
},
_custom: filter._custom,
genres: e ? [e] : undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
@@ -90,20 +91,15 @@ export const NavidromeAlbumFilters = ({
const updatedFilters = setFilter({
customFilters,
data: {
_custom: {
...filter._custom,
navidrome: {
...filter._custom?.navidrome,
starred: e.currentTarget.checked ? true : undefined,
},
},
_custom: filter._custom,
favorite: e.currentTarget.checked ? true : undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
},
value: filter._custom?.navidrome?.starred,
value: filter.favorite,
},
{
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
@@ -111,20 +107,15 @@ export const NavidromeAlbumFilters = ({
const updatedFilters = setFilter({
customFilters,
data: {
_custom: {
...filter._custom,
navidrome: {
...filter._custom?.navidrome,
compilation: e.currentTarget.checked ? true : undefined,
},
},
_custom: filter._custom,
compilation: e.currentTarget.checked ? true : undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
},
value: filter._custom?.navidrome?.compilation,
value: filter.compilation,
},
{
label: t('filter.isRecentlyPlayed', { postProcess: 'sentenceCase' }),
@@ -0,0 +1,141 @@
import { Divider, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { ChangeEvent, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AlbumListQuery, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import { NumberInput, Select, Switch, Text } from '/@/renderer/components';
import { useGenreList } from '/@/renderer/features/genres';
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
interface SubsonicAlbumFiltersProps {
onFilterChange: (filters: AlbumListFilter) => void;
pageKey: string;
serverId?: string;
}
export const SubsonicAlbumFilters = ({
onFilterChange,
pageKey,
serverId,
}: SubsonicAlbumFiltersProps) => {
const { t } = useTranslation();
const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
const { setFilter } = useListStoreActions();
const genreListQuery = useGenreList({
query: {
sortBy: GenreListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
serverId,
});
const genreList = useMemo(() => {
if (!genreListQuery?.data) return [];
return genreListQuery.data.items.map((genre) => ({
label: genre.name,
value: genre.id,
}));
}, [genreListQuery.data]);
const handleGenresFilter = debounce((e: string | null) => {
const updatedFilters = setFilter({
data: {
genres: e ? [e] : undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
}, 250);
const toggleFilters = [
{
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({
data: {
favorite: e.target.checked ? true : undefined,
},
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
},
value: filter.favorite,
},
];
const handleYearFilter = debounce((e: number | string, type: 'min' | 'max') => {
let data: Partial<AlbumListQuery> = {};
if (type === 'min') {
data = {
minYear: e ? Number(e) : undefined,
};
} else {
data = {
maxYear: e ? Number(e) : undefined,
};
}
const updatedFilters = setFilter({
data,
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
onFilterChange(updatedFilters);
}, 500);
return (
<Stack p="0.8rem">
{toggleFilters.map((filter) => (
<Group
key={`nd-filter-${filter.label}`}
position="apart"
>
<Text>{filter.label}</Text>
<Switch
checked={filter?.value || false}
onChange={filter.onChange}
/>
</Group>
))}
<Divider my="0.5rem" />
<Group grow>
<NumberInput
defaultValue={filter.minYear}
disabled={filter.genres?.length !== undefined}
hideControls={false}
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
max={5000}
min={0}
onChange={(e) => handleYearFilter(e, 'min')}
/>
<NumberInput
defaultValue={filter.maxYear}
disabled={filter.genres?.length !== undefined}
hideControls={false}
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
max={5000}
min={0}
onChange={(e) => handleYearFilter(e, 'max')}
/>
</Group>
<Group grow>
<Select
clearable
searchable
data={genreList}
defaultValue={filter.genres?.length ? filter.genres[0] : undefined}
disabled={Boolean(filter.minYear || filter.maxYear)}
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
onChange={handleGenresFilter}
/>
</Group>
</Stack>
);
};
@@ -0,0 +1,30 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import type { AlbumListQuery } from '/@/renderer/api/types';
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
export const useAlbumListCount = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumListCount({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.albums.count(
serverId || '',
Object.keys(query).length === 0 ? undefined : query,
),
...options,
});
};
@@ -5,12 +5,11 @@ import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'react-router-dom';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import { AlbumListQuery, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context';
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AnimatedPage } from '/@/renderer/features/shared';
import { queryClient } from '/@/renderer/lib/react-query';
@@ -18,6 +17,7 @@ import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
import { Play } from '/@/renderer/types';
import { useGenreList } from '/@/renderer/features/genres';
import { titleCase } from '/@/renderer/utils';
import { useAlbumListCount } from '/@/renderer/features/albums/queries/album-list-count-query';
const AlbumListRoute = () => {
const { t } = useTranslation();
@@ -33,14 +33,7 @@ const AlbumListRoute = () => {
const value = {
...(albumArtistId && { artistIds: [albumArtistId] }),
...(genreId && {
_custom: {
jellyfin: {
GenreIds: genreId,
},
navidrome: {
genre_id: genreId,
},
},
genres: [genreId],
}),
};
@@ -51,7 +44,7 @@ const AlbumListRoute = () => {
return value;
}, [albumArtistId, genreId]);
const albumListFilter = useListFilterByKey({
const albumListFilter = useListFilterByKey<AlbumListQuery>({
filter: customFilters,
key: pageKey,
});
@@ -78,32 +71,27 @@ const AlbumListRoute = () => {
return genre?.name;
}, [genreId, genreList.data]);
const itemCountCheck = useAlbumList({
const itemCountCheck = useAlbumListCount({
options: {
cacheTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: {
limit: 1,
startIndex: 0,
...albumListFilter,
},
serverId: server?.id,
});
const itemCount =
itemCountCheck.data?.totalRecordCount === null
? undefined
: itemCountCheck.data?.totalRecordCount;
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
const handlePlay = useCallback(
async (args: { initialSongId?: string; playType: Play }) => {
if (!itemCount || itemCount === 0) return;
const { playType } = args;
const query = {
startIndex: 0,
...albumListFilter,
...customFilters,
startIndex: 0,
};
const queryKey = queryKeys.albums.list(server?.id || '', query);
@@ -111,18 +111,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
enabled: enabledItem.recentAlbums,
},
query: {
_custom: {
jellyfin: {
...(server?.type === ServerType.JELLYFIN
? { AlbumArtistIds: albumArtistId }
: undefined),
},
navidrome: {
...(server?.type === ServerType.NAVIDROME
? { artist_id: albumArtistId, compilation: false }
: undefined),
},
},
artistIds: [albumArtistId],
limit: 15,
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
@@ -133,21 +122,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
const compilationAlbumsQuery = useAlbumList({
options: {
enabled: enabledItem.compilations,
enabled: enabledItem.compilations && server?.type !== ServerType.SUBSONIC,
},
query: {
_custom: {
jellyfin: {
...(server?.type === ServerType.JELLYFIN
? { ContributingArtistIds: albumArtistId }
: undefined),
},
navidrome: {
...(server?.type === ServerType.NAVIDROME
? { artist_id: albumArtistId, compilation: true }
: undefined),
},
},
artistIds: [albumArtistId],
compilation: true,
limit: 15,
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
@@ -254,7 +233,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
},
{
data: compilationAlbumsQuery?.data?.items,
isHidden: !compilationAlbumsQuery?.data?.items?.length || !enabledItem.compilations,
isHidden:
!compilationAlbumsQuery?.data?.items?.length ||
!enabledItem.compilations ||
server?.type === ServerType.SUBSONIC,
itemType: LibraryItem.ALBUM,
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
order: itemOrder.compilations,
@@ -301,6 +283,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
recentAlbumsQuery?.data?.items,
recentAlbumsQuery.isFetching,
recentAlbumsQuery?.isLoading,
server?.type,
t,
]);
@@ -567,8 +550,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
suppressLoadingOverlay
suppressRowDrag
columnDefs={topSongsColumnDefs}
context={{
itemType: LibraryItem.SONG,
}}
enableCellChangeFlash={false}
getRowId={(data) => data.data.id}
getRowId={(data) => data.data.uniqueId}
rowData={topSongs}
rowHeight={60}
rowSelection="multiple"
@@ -26,16 +26,19 @@ export const AlbumArtistDetailHeader = forwardRef(
const metadataItems = [
{
enabled: detailQuery?.data?.albumCount,
id: 'albumCount',
secondary: false,
value: t('entity.albumWithCount', { count: detailQuery?.data?.albumCount || 0 }),
},
{
enabled: detailQuery?.data?.songCount,
id: 'songCount',
secondary: false,
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
},
{
enabled: detailQuery.data?.duration,
id: 'duration',
secondary: true,
value:
@@ -70,7 +73,7 @@ export const AlbumArtistDetailHeader = forwardRef(
<Stack>
<Group>
{metadataItems
.filter((i) => i.value)
.filter((i) => i.enabled)
.map((item, index) => (
<Fragment key={`item-${item.id}-${index}`}>
{index > 0 && <Text $noSelect></Text>}
@@ -66,7 +66,7 @@ export const AlbumArtistDetailTopSongsListContent = ({
ref={tableRef}
shouldUpdateSong
{...tableProps}
getRowId={(data) => data.data.id}
getRowId={(data) => data.data.uniqueId}
rowClassRules={rowClassRules}
rowData={data}
rowModelType="clientSide"
@@ -11,7 +11,6 @@ import {
AlbumArtistListQuery,
AlbumArtistListResponse,
AlbumArtistListSort,
ArtistListQuery,
LibraryItem,
} from '/@/renderer/api/types';
import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components';
@@ -34,7 +33,7 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG
const handlePlayQueueAdd = usePlayQueueAdd();
const { pageKey } = useListContext();
const { grid, display, filter } = useListStoreByKey({ key: pageKey });
const { grid, display, filter } = useListStoreByKey<AlbumArtistListQuery>({ key: pageKey });
const { setGrid } = useListStoreActions();
const handleFavorite = useHandleFavorite({ gridRef, server });
@@ -73,7 +72,7 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG
const fetch = useCallback(
async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => {
const query: ArtistListQuery = {
const query: AlbumArtistListQuery = {
...filter,
limit,
startIndex,
@@ -89,11 +88,7 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG
server,
signal,
},
query: {
limit,
startIndex,
...filter,
},
query,
}),
{ cacheTime: 1000 * 60 * 1 },
);
@@ -9,7 +9,13 @@ import { RiFolder2Line, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react
import { useListContext } from '../../../context/list-context';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumArtistListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
import {
AlbumArtistListQuery,
AlbumArtistListSort,
LibraryItem,
ServerType,
SortOrder,
} from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
@@ -85,6 +91,28 @@ const FILTERS = {
value: AlbumArtistListSort.SONG_COUNT,
},
],
subsonic: [
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.ALBUM_COUNT,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.FAVORITED,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.NAME,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.RATING,
},
],
};
interface AlbumArtistListHeaderFiltersProps {
@@ -100,7 +128,9 @@ export const AlbumArtistListHeaderFilters = ({
const queryClient = useQueryClient();
const server = useCurrentServer();
const { pageKey } = useListContext();
const { display, table, grid, filter } = useListStoreByKey({ key: pageKey });
const { display, table, grid, filter } = useListStoreByKey<AlbumArtistListQuery>({
key: pageKey,
});
const { setFilter, setTable, setTablePagination, setDisplayType, setGrid } =
useListStoreActions();
const cq = useContainerQuery();
@@ -416,7 +446,9 @@ export const AlbumArtistListHeaderFilters = ({
icon={<RiRefreshLine />}
onClick={handleRefresh}
>
Refresh
{t('common.refresh', {
postProcess: 'titleCase',
})}
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
@@ -436,7 +468,9 @@ export const AlbumArtistListHeaderFilters = ({
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Label>Display type</DropdownMenu.Label>
<DropdownMenu.Label>
{t('table.config.general.displayType', { postProcess: 'sentenceCase' })}
</DropdownMenu.Label>
<DropdownMenu.Item
$isActive={display === ListDisplayType.CARD}
value={ListDisplayType.CARD}
@@ -4,7 +4,7 @@ import { Flex, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
import { FilterBar } from '../../shared/components/filter-bar';
import { LibraryItem } from '/@/renderer/api/types';
import { AlbumArtistListQuery, LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
@@ -28,8 +28,9 @@ export const AlbumArtistListHeader = ({
const server = useCurrentServer();
const cq = useContainerQuery();
const { filter, refresh, search } = useDisplayRefresh({
const { filter, refresh, search } = useDisplayRefresh<AlbumArtistListQuery>({
gridRef,
itemCount,
itemType: LibraryItem.ALBUM_ARTIST,
server,
tableRef,
@@ -0,0 +1,30 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumArtistListQuery } from '/@/renderer/api/types';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { getServerById } from '/@/renderer/store';
export const useAlbumArtistListCount = (args: QueryHookArgs<AlbumArtistListQuery>) => {
const { options, query, serverId } = args;
const server = getServerById(serverId);
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getAlbumArtistListCount({
apiClientProps: {
server,
signal,
},
query,
});
},
queryKey: queryKeys.albumArtists.count(
serverId || '',
Object.keys(query).length === 0 ? undefined : query,
),
...options,
});
};
@@ -13,7 +13,7 @@ export const useTopSongsList = (args: QueryHookArgs<TopSongListQuery>) => {
enabled: !!server?.id,
queryFn: ({ signal }) => {
if (!server) throw new Error('Server not found');
return api.controller.getTopSongList({ apiClientProps: { server, signal }, query });
return api.controller.getTopSongs({ apiClientProps: { server, signal }, query });
},
queryKey: queryKeys.albumArtists.topSongs(server?.id || '', query),
...options,
@@ -2,13 +2,13 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { useMemo, useRef } from 'react';
import { useCurrentServer } from '../../../store/auth.store';
import { useListFilterByKey } from '../../../store/list.store';
import { LibraryItem } from '/@/renderer/api/types';
import { AlbumArtistListQuery, LibraryItem } from '/@/renderer/api/types';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context';
import { AlbumArtistListContent } from '/@/renderer/features/artists/components/album-artist-list-content';
import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { AnimatedPage } from '/@/renderer/features/shared';
import { useAlbumArtistListCount } from '/@/renderer/features/artists/queries/album-artist-list-count-query';
const AlbumArtistListRoute = () => {
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
@@ -16,25 +16,18 @@ const AlbumArtistListRoute = () => {
const pageKey = LibraryItem.ALBUM_ARTIST;
const server = useCurrentServer();
const albumArtistListFilter = useListFilterByKey({ key: pageKey });
const albumArtistListFilter = useListFilterByKey<AlbumArtistListQuery>({ key: pageKey });
const itemCountCheck = useAlbumArtistList({
const itemCountCheck = useAlbumArtistListCount({
options: {
cacheTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: {
limit: 1,
startIndex: 0,
...albumArtistListFilter,
},
query: albumArtistListFilter,
serverId: server?.id,
});
const itemCount =
itemCountCheck.data?.totalRecordCount === null
? undefined
: itemCountCheck.data?.totalRecordCount;
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
const providerValue = useMemo(() => {
return {
@@ -2,6 +2,7 @@ import { SetContextMenuItems } from '/@/renderer/features/context-menu/events';
export const QUEUE_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ divider: true, id: 'removeFromQueue' },
{ id: 'moveToNextOfQueue' },
{ id: 'moveToBottomOfQueue' },
{ divider: true, id: 'moveToTopOfQueue' },
{ divider: true, id: 'addToPlaylist' },
@@ -18,6 +19,7 @@ export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ id: 'playNext' },
{ id: 'playShuffled' },
{ divider: true, id: 'playSimilarSongs' },
{ divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' },
@@ -31,7 +33,8 @@ export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
export const SONG_ALBUM_PAGE: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ divider: true, id: 'playNext' },
{ id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'addToPlaylist' },
];
@@ -39,6 +42,7 @@ export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ id: 'playNext' },
{ id: 'playShuffled' },
{ divider: true, id: 'playSimilarSongs' },
{ id: 'addToPlaylist' },
{ divider: true, id: 'removeFromPlaylist' },
@@ -54,6 +58,7 @@ export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'playSimilarSongs' },
{ divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' },
@@ -67,7 +72,8 @@ export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ divider: true, id: 'playNext' },
{ id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' },
{ id: 'removeFromFavorites' },
@@ -79,14 +85,16 @@ export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
export const GENRE_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ divider: true, id: 'playNext' },
{ id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'addToPlaylist' },
];
export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ divider: true, id: 'playNext' },
{ id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' },
{ divider: true, id: 'removeFromFavorites' },
@@ -98,7 +106,8 @@ export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
export const PLAYLIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ divider: true, id: 'playNext' },
{ id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'shareItem' },
{ id: 'deletePlaylist' },
];
@@ -18,6 +18,7 @@ import {
RiAddBoxFill,
RiAddCircleFill,
RiArrowDownLine,
RiArrowGoForwardLine,
RiArrowRightSFill,
RiArrowUpLine,
RiDeleteBinFill,
@@ -31,6 +32,7 @@ import {
RiInformationFill,
RiRadio2Fill,
RiDownload2Line,
RiShuffleFill,
} from 'react-icons/ri';
import { AnyLibraryItems, LibraryItem, ServerType, AnyLibraryItem } from '/@/renderer/api/types';
import {
@@ -493,17 +495,24 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
const removeFromPlaylistMutation = useRemoveFromPlaylist();
const handleRemoveFromPlaylist = useCallback(() => {
const songId =
(serverType === ServerType.NAVIDROME || ServerType.JELLYFIN
? ctx.dataNodes?.map((node) => node.data.playlistItemId)
: ctx.dataNodes?.map((node) => node.data.id)) || [];
let songId: string[] | undefined;
switch (serverType) {
case ServerType.NAVIDROME:
case ServerType.JELLYFIN:
songId = ctx.dataNodes?.map((node) => node.data.playlistItemId);
break;
case ServerType.SUBSONIC:
songId = ctx.dataNodes?.map((node) => node.rowIndex!.toString());
break;
}
const confirm = () => {
removeFromPlaylistMutation.mutate(
{
query: {
id: ctx.context.playlistId,
songId,
songId: songId || [],
},
serverId: ctx.data?.[0]?.serverId,
},
@@ -601,7 +610,19 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
);
const playbackType = usePlaybackType();
const { moveToBottomOfQueue, moveToTopOfQueue, removeFromQueue } = useQueueControls();
const { moveToNextOfQueue, moveToBottomOfQueue, moveToTopOfQueue, removeFromQueue } =
useQueueControls();
const handleMoveToNext = useCallback(() => {
const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId);
if (!uniqueIds?.length) return;
const playerData = moveToNextOfQueue(uniqueIds);
if (playbackType === PlaybackType.LOCAL) {
setQueueNext(playerData);
}
}, [ctx.dataNodes, moveToNextOfQueue, playbackType]);
const handleMoveToBottom = useCallback(() => {
const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId);
@@ -750,6 +771,12 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
leftIcon: <RiArrowDownLine size="1.1rem" />,
onClick: handleMoveToBottom,
},
moveToNextOfQueue: {
id: 'moveToNext',
label: t('page.contextMenu.moveToNext', { postProcess: 'sentenceCase' }),
leftIcon: <RiArrowGoForwardLine size="1.1rem" />,
onClick: handleMoveToNext,
},
moveToTopOfQueue: {
id: 'moveToTopOfQueue',
label: t('page.contextMenu.moveToTop', { postProcess: 'sentenceCase' }),
@@ -774,6 +801,12 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
leftIcon: <RiAddCircleFill size="1.1rem" />,
onClick: () => handlePlay(Play.NEXT),
},
playShuffled: {
id: 'playShuffled',
label: t('page.contextMenu.playShuffled', { postProcess: 'sentenceCase' }),
leftIcon: <RiShuffleFill size="1.1rem" />,
onClick: () => handlePlay(Play.SHUFFLE),
},
playSimilarSongs: {
id: 'playSimilarSongs',
label: t('page.contextMenu.playSimilarSongs', { postProcess: 'sentenceCase' }),
@@ -862,7 +895,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
},
],
id: 'setRating',
label: 'Set rating',
label: t('action.setRating', { postProcess: 'sentenceCase' }),
leftIcon: <RiStarFill size="1.1rem" />,
onClick: () => {},
rightIcon: <RiArrowRightSFill size="1.2rem" />,
@@ -890,6 +923,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
handleDeselectAll,
ctx.data,
handleDownload,
handleMoveToNext,
handleMoveToBottom,
handleMoveToTop,
handleSimilar,
@@ -23,6 +23,7 @@ export type ContextMenuItemType =
| 'play'
| 'playLast'
| 'playNext'
| 'playShuffled'
| 'addToPlaylist'
| 'removeFromPlaylist'
| 'addToFavorites'
@@ -31,6 +32,7 @@ export type ContextMenuItemType =
| 'shareItem'
| 'deletePlaylist'
| 'createPlaylist'
| 'moveToNextOfQueue'
| 'moveToBottomOfQueue'
| 'moveToTopOfQueue'
| 'removeFromQueue'
@@ -45,6 +47,7 @@ export const CONFIGURABLE_CONTEXT_MENU_ITEMS: ContextMenuItemType[] = [
'play',
'playLast',
'playNext',
'playShuffled',
'playSimilarSongs',
'addToPlaylist',
'removeFromPlaylist',
@@ -5,6 +5,7 @@ import {
useCurrentSong,
useCurrentStatus,
useDiscordSetttings,
useGeneralSettings,
usePlayerStore,
} from '/@/renderer/store';
import { SetActivity } from '@xhayper/discord-rpc';
@@ -16,6 +17,7 @@ const discordRpc = isElectron() ? window.electron.discordRpc : null;
export const useDiscordRpc = () => {
const intervalRef = useRef(0);
const discordSettings = useDiscordSetttings();
const generalSettings = useGeneralSettings();
const currentSong = useCurrentSong();
const currentStatus = useCurrentStatus();
@@ -67,6 +69,19 @@ export const useDiscordRpc = () => {
activity.largeImageKey = song?.imageUrl;
}
if (generalSettings.lastfmApiKey && song?.album && song?.albumArtists.length) {
console.log('Fetching album info for', song.album, song.albumArtists[0].name);
const albumInfo = await fetch(
`https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=${generalSettings.lastfmApiKey}&artist=${encodeURIComponent(song.albumArtists[0].name)}&album=${encodeURIComponent(song.album)}&format=json`,
);
const albumInfoJson = await albumInfo.json();
if (albumInfoJson.album?.image?.[3]['#text']) {
activity.largeImageKey = albumInfoJson.album.image[3]['#text'];
}
}
// Fall back to default icon if not set
if (!activity.largeImageKey) {
activity.largeImageKey = 'icon';
@@ -79,6 +94,7 @@ export const useDiscordRpc = () => {
discordSettings.enableIdle,
discordSettings.showAsListening,
discordSettings.showServerImage,
generalSettings.lastfmApiKey,
]);
useEffect(() => {
@@ -22,7 +22,7 @@ export const GenreListGridView = ({ gridRef, itemCount }: any) => {
const server = useCurrentServer();
const handlePlayQueueAdd = usePlayQueueAdd();
const { pageKey, id } = useListContext();
const { grid, display, filter } = useListStoreByKey({ key: pageKey });
const { grid, display, filter } = useListStoreByKey<GenreListQuery>({ key: pageKey });
const { setGrid } = useListStoreActions();
const genrePath = useGenreRoute();
@@ -12,7 +12,13 @@ import {
RiSettings3Fill,
} from 'react-icons/ri';
import { queryKeys } from '/@/renderer/api/query-keys';
import { GenreListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
import {
GenreListQuery,
GenreListSort,
LibraryItem,
ServerType,
SortOrder,
} from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
@@ -47,25 +53,38 @@ const FILTERS = {
value: GenreListSort.NAME,
},
],
subsonic: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: GenreListSort.NAME,
},
],
};
interface GenreListHeaderFiltersProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount: number | undefined;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFiltersProps) => {
export const GenreListHeaderFilters = ({
gridRef,
itemCount,
tableRef,
}: GenreListHeaderFiltersProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { pageKey, customFilters } = useListContext();
const server = useCurrentServer();
const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions();
const { display, filter, table, grid } = useListStoreByKey({ key: pageKey });
const { display, filter, table, grid } = useListStoreByKey<GenreListQuery>({ key: pageKey });
const cq = useContainerQuery();
const { genreTarget } = useGeneralSettings();
const { setGenreBehavior } = useSettingsStoreActions();
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
itemCount,
itemType: LibraryItem.GENRE,
server,
});
@@ -367,7 +386,7 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Label>
{t('table.config.general.displayType', { postProcess: 'titleCase' })}
{t('table.config.general.displayType', { postProcess: 'sentenceCase' })}
</DropdownMenu.Label>
<DropdownMenu.Item
$isActive={display === ListDisplayType.CARD}
@@ -404,7 +423,11 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
</DropdownMenu.Item>
{isGrid && (
<>
<DropdownMenu.Label>Item gap</DropdownMenu.Label>
<DropdownMenu.Label>
{t('table.config.general.itemGap', {
postProcess: 'sentenceCase',
})}
</DropdownMenu.Label>
<DropdownMenu.Item closeMenuOnClick={false}>
<Slider
defaultValue={grid?.itemGap || 0}
@@ -2,7 +2,7 @@ import { ChangeEvent, MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Flex, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { LibraryItem } from '/@/renderer/api/types';
import { GenreListQuery, LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { GenreListHeaderFilters } from '/@/renderer/features/genres/components/genre-list-header-filters';
@@ -22,7 +22,7 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade
const { t } = useTranslation();
const cq = useContainerQuery();
const server = useCurrentServer();
const { filter, refresh, search } = useDisplayRefresh({
const { filter, refresh, search } = useDisplayRefresh<GenreListQuery>({
gridRef,
itemType: LibraryItem.GENRE,
server,
@@ -66,6 +66,7 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade
<FilterBar>
<GenreListHeaderFilters
gridRef={gridRef}
itemCount={itemCount}
tableRef={tableRef}
/>
</FilterBar>
@@ -8,19 +8,20 @@ import { useGenreList } from '/@/renderer/features/genres/queries/genre-list-que
import { AnimatedPage } from '/@/renderer/features/shared';
import { useCurrentServer } from '/@/renderer/store';
import { useListStoreByKey } from '../../../store/list.store';
import { GenreListQuery } from '/@/renderer/api/types';
const GenreListRoute = () => {
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
const tableRef = useRef<AgGridReactType | null>(null);
const server = useCurrentServer();
const pageKey = 'genre';
const { filter } = useListStoreByKey({ key: pageKey });
const { filter } = useListStoreByKey<GenreListQuery>({ key: pageKey });
const itemCountCheck = useGenreList({
query: {
...filter,
limit: 1,
startIndex: 0,
...filter,
},
serverId: server?.id,
});
@@ -2,7 +2,14 @@ import { Group, Table } from '@mantine/core';
import { RiCheckFill, RiCloseFill } from 'react-icons/ri';
import { TFunction, useTranslation } from 'react-i18next';
import { ReactNode } from 'react';
import { Album, AlbumArtist, AnyLibraryItem, LibraryItem, Song } from '/@/renderer/api/types';
import {
Album,
AlbumArtist,
AnyLibraryItem,
LibraryItem,
RelatedArtist,
Song,
} from '/@/renderer/api/types';
import { formatDurationString, formatSizeString } from '/@/renderer/utils';
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
import { Spoiler, Text } from '/@/renderer/components';
@@ -46,8 +53,8 @@ const handleRow = <T extends AnyLibraryItem>(t: TFunction, item: T, rule: ItemDe
);
};
const formatArtists = (isAlbumArtist: boolean) => (item: Album | Song) =>
(isAlbumArtist ? item.albumArtists : item.artists)?.map((artist, index) => (
const formatArtists = (artists: RelatedArtist[] | undefined | null) =>
artists?.map((artist, index) => (
<span key={artist.id || artist.name}>
{index > 0 && <Separator />}
{artist.id ? (
@@ -106,7 +113,7 @@ const BoolField = (key: boolean) =>
const AlbumPropertyMapping: ItemDetailRow<Album>[] = [
{ key: 'name', label: 'common.title' },
{ label: 'entity.albumArtist_one', render: formatArtists(true) },
{ label: 'entity.albumArtist_one', render: (item) => formatArtists(item.albumArtists) },
{ label: 'entity.genre_other', render: FormatGenre },
{
label: 'common.duration',
@@ -198,8 +205,8 @@ const AlbumArtistPropertyMapping: ItemDetailRow<AlbumArtist>[] = [
const SongPropertyMapping: ItemDetailRow<Song>[] = [
{ key: 'name', label: 'common.title' },
{ key: 'path', label: 'common.path', render: SongPath },
{ label: 'entity.albumArtist_one', render: formatArtists(true) },
{ key: 'artists', label: 'entity.artist_other', render: formatArtists(false) },
{ label: 'entity.albumArtist_one', render: (item) => formatArtists(item.albumArtists) },
{ key: 'artists', label: 'entity.artist_other', render: (item) => formatArtists(item.artists) },
{
key: 'album',
label: 'entity.album_one',
@@ -270,22 +277,42 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [
{ label: 'filter.comment', render: formatComment },
];
const handleParticipants = (item: Album | Song) => {
if (item.participants) {
return Object.entries(item.participants).map(([role, participants]) => {
return (
<tr key={role}>
<td>
{role.slice(0, 1).toLocaleUpperCase()}
{role.slice(1)}
</td>
<td>{formatArtists(participants)}</td>
</tr>
);
});
}
return [];
};
export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => {
const { t } = useTranslation();
let body: ReactNode;
let body: ReactNode[] = [];
switch (item.itemType) {
case LibraryItem.ALBUM:
body = AlbumPropertyMapping.map((rule) => handleRow(t, item, rule));
body.push(...handleParticipants(item));
break;
case LibraryItem.ALBUM_ARTIST:
body = AlbumArtistPropertyMapping.map((rule) => handleRow(t, item, rule));
break;
case LibraryItem.SONG:
body = SongPropertyMapping.map((rule) => handleRow(t, item, rule));
body.push(...handleParticipants(item));
break;
default:
body = null;
body = [];
}
return (
@@ -19,6 +19,7 @@ interface LyricsActionsProps {
onRemoveLyric: () => void;
onResetLyric: () => void;
onSearchOverride: (params: LyricsOverride) => void;
onTranslateLyric: () => void;
setIndex: (idx: number) => void;
}
@@ -28,6 +29,7 @@ export const LyricsActions = ({
onRemoveLyric,
onResetLyric,
onSearchOverride,
onTranslateLyric,
setIndex,
}: LyricsActionsProps) => {
const { t } = useTranslation();
@@ -120,7 +122,6 @@ export const LyricsActions = ({
{isDesktop && sources.length ? (
<Button
uppercase
color="red"
disabled={isActionsDisabled}
variant="subtle"
onClick={onRemoveLyric}
@@ -129,6 +130,19 @@ export const LyricsActions = ({
</Button>
) : null}
</Box>
<Box style={{ position: 'absolute', right: 0, top: -50 }}>
{isDesktop && sources.length ? (
<Button
uppercase
disabled={isActionsDisabled}
variant="subtle"
onClick={onTranslateLyric}
>
{t('common.translation', { postProcess: 'sentenceCase' })}
</Button>
) : null}
</Box>
</Box>
);
};
+50 -16
View File
@@ -2,9 +2,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { Center, Group } from '@mantine/core';
import { AnimatePresence, motion } from 'framer-motion';
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { RiInformationFill } from 'react-icons/ri';
import styled from 'styled-components';
import { useSongLyricsByRemoteId, useSongLyricsBySong } from './queries/lyric-query';
import { translateLyrics } from './queries/lyric-translate';
import { SynchronizedLyrics, SynchronizedLyricsProps } from './synchronized-lyrics';
import { Spinner, TextTitle } from '/@/renderer/components';
import { ErrorFallback } from '/@/renderer/features/action-required';
@@ -12,7 +14,7 @@ import {
UnsynchronizedLyrics,
UnsynchronizedLyricsProps,
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
import { useCurrentSong, usePlayerStore } from '/@/renderer/store';
import { useCurrentSong, usePlayerStore, useLyricsSettings } from '/@/renderer/store';
import { FullLyricsMetadata, LyricSource, LyricsOverride } from '/@/renderer/api/types';
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
import { queryKeys } from '/@/renderer/api/query-keys';
@@ -84,7 +86,11 @@ const ScrollContainer = styled(motion.div)`
export const Lyrics = () => {
const currentSong = useCurrentSong();
const lyricsSettings = useLyricsSettings();
const { t } = useTranslation();
const [index, setIndex] = useState(0);
const [translatedLyrics, setTranslatedLyrics] = useState<string | null>(null);
const [showTranslation, setShowTranslation] = useState(false);
const { data, isInitialLoading } = useSongLyricsBySong(
{
@@ -96,6 +102,19 @@ export const Lyrics = () => {
const [override, setOverride] = useState<LyricsOverride | undefined>(undefined);
const [lyrics, synced] = useMemo(() => {
if (Array.isArray(data)) {
if (data.length > 0) {
const selectedLyric = data[Math.min(index, data.length)];
return [selectedLyric, selectedLyric.synced];
}
} else if (data?.lyrics) {
return [data, Array.isArray(data.lyrics)];
}
return [undefined, false];
}, [data, index]);
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
setOverride(params);
}, []);
@@ -123,6 +142,27 @@ export const Lyrics = () => {
);
}, [currentSong?.id, currentSong?.serverId]);
const handleOnTranslateLyric = useCallback(async () => {
if (translatedLyrics) {
setShowTranslation(!showTranslation);
return;
}
if (!lyrics) return;
const originalLyrics = Array.isArray(lyrics.lyrics)
? lyrics.lyrics.map(([, line]) => line).join('\n')
: lyrics.lyrics;
const { translationApiKey, translationApiProvider, translationTargetLanguage } =
lyricsSettings;
const TranslatedText: string | null = await translateLyrics(
originalLyrics,
translationApiKey,
translationApiProvider,
translationTargetLanguage,
);
setTranslatedLyrics(TranslatedText);
setShowTranslation(true);
}, [lyrics, lyricsSettings, translatedLyrics, showTranslation]);
const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({
options: {
enabled: !!override,
@@ -150,19 +190,6 @@ export const Lyrics = () => {
};
}, []);
const [lyrics, synced] = useMemo(() => {
if (Array.isArray(data)) {
if (data.length > 0) {
const selectedLyric = data[Math.min(index, data.length)];
return [selectedLyric, selectedLyric.synced];
}
} else if (data?.lyrics) {
return [data, Array.isArray(data.lyrics)];
}
return [undefined, false];
}, [data, index]);
const languages = useMemo(() => {
if (Array.isArray(data)) {
return data.map((lyric, idx) => ({ label: lyric.lang, value: idx.toString() }));
@@ -192,7 +219,9 @@ export const Lyrics = () => {
order={3}
weight={700}
>
No lyrics found
{t('page.fullscreenPlayer.noLyrics', {
postProcess: 'sentenceCase',
})}
</TextTitle>
</Group>
</Center>
@@ -203,10 +232,14 @@ export const Lyrics = () => {
transition={{ duration: 0.5 }}
>
{synced ? (
<SynchronizedLyrics {...(lyrics as SynchronizedLyricsProps)} />
<SynchronizedLyrics
{...(lyrics as SynchronizedLyricsProps)}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
) : (
<UnsynchronizedLyrics
{...(lyrics as UnsynchronizedLyricsProps)}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
)}
</ScrollContainer>
@@ -221,6 +254,7 @@ export const Lyrics = () => {
onRemoveLyric={handleOnRemoveLyric}
onResetLyric={handleOnResetLyric}
onSearchOverride={handleOnSearchOverride}
onTranslateLyric={handleOnTranslateLyric}
/>
</ActionsContainer>
</LyricsContainer>
@@ -0,0 +1,50 @@
import axios from 'axios';
export const translateLyrics = async (
originalLyrics: string,
translationApiKey: string,
translationApiProvider: string | null,
translationTargetLanguage: string | null,
) => {
let TranslatedText = '';
if (translationApiProvider === 'Microsoft Azure') {
try {
const response = await axios({
data: [
{
Text: originalLyrics,
},
],
headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': translationApiKey,
},
method: 'post',
url: `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${translationTargetLanguage as string}`,
});
TranslatedText = response.data[0].translations[0].text;
} catch (e) {
console.error('Microsoft Azure translate request got an error!', e);
return null;
}
} else if (translationApiProvider === 'Google Cloud') {
try {
const response = await axios({
data: {
format: 'text',
q: originalLyrics,
},
headers: {
'Content-Type': 'application/json',
},
method: 'post',
url: `https://translation.googleapis.com/language/translate/v2?target=${translationTargetLanguage as string}&key=${translationApiKey}`,
});
TranslatedText = response.data.data.translations[0].translatedText;
} catch (e) {
console.error('Google Cloud translate request got an error!', e);
return null;
}
}
return TranslatedText;
};
@@ -1,11 +1,13 @@
import { useCallback, useEffect, useRef } from 'react';
import {
useCurrentPlayer,
useCurrentStatus,
useCurrentTime,
useLyricsSettings,
usePlaybackType,
usePlayerData,
useSeeked,
useSetCurrentTime,
} from '/@/renderer/store';
import { PlaybackType, PlayerStatus } from '/@/renderer/types';
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
@@ -13,9 +15,11 @@ import isElectron from 'is-electron';
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/renderer/api/types';
import styled from 'styled-components';
import { useCenterControls } from '/@/renderer/features/player/hooks/use-center-controls';
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const utils = isElectron() ? window.electron.utils : null;
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
const SynchronizedLyricsContainer = styled.div<{ $gap: number }>`
display: flex;
@@ -51,6 +55,7 @@ const SynchronizedLyricsContainer = styled.div<{ $gap: number }>`
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: SynchronizedLyricsArray;
translatedLyrics?: string | null;
}
export const SynchronizedLyrics = ({
@@ -59,6 +64,7 @@ export const SynchronizedLyrics = ({
name,
remote,
source,
translatedLyrics,
}: SynchronizedLyricsProps) => {
const playersRef = PlayersRef;
const status = useCurrentStatus();
@@ -66,7 +72,25 @@ export const SynchronizedLyrics = ({
const playerData = usePlayerData();
const now = useCurrentTime();
const settings = useLyricsSettings();
const centerControls = useCenterControls({ playersRef });
const currentPlayer = useCurrentPlayer();
const currentPlayerRef =
currentPlayer === 1 ? playersRef.current?.player1 : playersRef.current?.player2;
const setCurrentTime = useSetCurrentTime();
const { handleScrobbleFromSeek } = useScrobble();
const handleSeek = useCallback(
(time: number) => {
if (playbackType === PlaybackType.LOCAL && mpvPlayer) {
mpvPlayer.seekTo(time);
} else {
setCurrentTime(time, true);
handleScrobbleFromSeek(time);
mpris?.updateSeek(time);
currentPlayerRef?.seekTo(time);
}
},
[currentPlayerRef, handleScrobbleFromSeek, playbackType, setCurrentTime],
);
const seeked = useSeeked();
@@ -342,15 +366,25 @@ export const SynchronizedLyrics = ({
/>
)}
{lyrics.map(([time, text], idx) => (
<LyricLine
key={idx}
alignment={settings.alignment}
className="lyric-line synchronized"
fontSize={settings.fontSize}
id={`lyric-${idx}`}
text={text}
onClick={() => centerControls.handleSeekSlider(time / 1000)}
/>
<div key={idx}>
<LyricLine
alignment={settings.alignment}
className="lyric-line synchronized"
fontSize={settings.fontSize}
id={`lyric-${idx}`}
text={text}
onClick={() => handleSeek(time / 1000)}
/>
{translatedLyrics && (
<LyricLine
alignment={settings.alignment}
className="lyric-line synchronized translation"
fontSize={settings.fontSize * 0.8}
text={translatedLyrics.split('\n')[idx]}
onClick={() => handleSeek(time / 1000)}
/>
)}
</div>
))}
</SynchronizedLyricsContainer>
);
@@ -6,6 +6,7 @@ import { useLyricsSettings } from '/@/renderer/store';
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: string;
translatedLyrics?: string | null;
}
const UnsynchronizedLyricsContainer = styled.div<{ $gap: number }>`
@@ -45,12 +46,17 @@ export const UnsynchronizedLyrics = ({
name,
remote,
source,
translatedLyrics,
}: UnsynchronizedLyricsProps) => {
const settings = useLyricsSettings();
const lines = useMemo(() => {
return lyrics.split('\n');
}, [lyrics]);
const translatedLines = useMemo(() => {
return translatedLyrics ? translatedLyrics.split('\n') : [];
}, [translatedLyrics]);
return (
<UnsynchronizedLyricsContainer
$gap={settings.gapUnsync}
@@ -73,14 +79,23 @@ export const UnsynchronizedLyrics = ({
/>
)}
{lines.map((text, idx) => (
<LyricLine
key={idx}
alignment={settings.alignment}
className="lyric-line unsynchronized"
fontSize={settings.fontSizeUnsync}
id={`lyric-${idx}`}
text={text}
/>
<div key={idx}>
<LyricLine
alignment={settings.alignment}
className="lyric-line unsynchronized"
fontSize={settings.fontSizeUnsync}
id={`lyric-${idx}`}
text={text}
/>
{translatedLines[idx] && (
<LyricLine
alignment={settings.alignment}
className="lyric-line unsynchronized translation"
fontSize={settings.fontSizeUnsync * 0.8}
text={translatedLines[idx]}
/>
)}
</div>
))}
</UnsynchronizedLyricsContainer>
);
@@ -6,6 +6,7 @@ import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import {
RiArrowDownLine,
RiArrowGoForwardLine,
RiArrowUpLine,
RiShuffleLine,
RiDeleteBinLine,
@@ -30,14 +31,32 @@ interface PlayQueueListOptionsProps {
export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsProps) => {
const { t } = useTranslation();
const { clearQueue, moveToBottomOfQueue, moveToTopOfQueue, shuffleQueue, removeFromQueue } =
useQueueControls();
const {
clearQueue,
moveToBottomOfQueue,
moveToNextOfQueue,
moveToTopOfQueue,
shuffleQueue,
removeFromQueue,
} = useQueueControls();
const { pause } = usePlayerControls();
const playbackType = usePlaybackType();
const setCurrentTime = useSetCurrentTime();
const handleMoveToNext = () => {
const selectedRows = tableRef?.current?.grid.api.getSelectedRows();
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
if (!uniqueIds?.length) return;
const playerData = moveToNextOfQueue(uniqueIds);
if (playbackType === PlaybackType.LOCAL) {
setQueueNext(playerData);
}
};
const handleMoveToBottom = () => {
const selectedRows = tableRef?.current?.grid.api.getSelectedRows();
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
@@ -124,6 +143,15 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
>
<RiShuffleLine size="1.1rem" />
</Button>
<Button
compact
size="md"
tooltip={{ label: t('action.moveToNext', { postProcess: 'sentenceCase' }) }}
variant="default"
onClick={handleMoveToNext}
>
<RiArrowGoForwardLine size="1.1rem" />
</Button>
<Button
compact
size="md"
@@ -256,7 +256,10 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
columnDefs={columnDefs}
context={{
currentSong,
handleDoubleClick,
isFocused,
isQueue: true,
itemType: LibraryItem.SONG,
onCellContextMenu,
status,
}}
@@ -11,8 +11,17 @@ import {
useFullScreenPlayerStoreActions,
} from '/@/renderer/store/full-screen-player.store';
import { Lyrics } from '/@/renderer/features/lyrics/lyrics';
import { lazy, Suspense, useMemo } from 'react';
import { usePlaybackSettings } from '/@/renderer/store';
import { PlaybackType } from '/@/renderer/types';
import { FullScreenSimilarSongs } from '/@/renderer/features/player/components/full-screen-similar-songs';
const Visualizer = lazy(() =>
import('/@/renderer/features/player/components/visualizer').then((module) => ({
default: module.Visualizer,
})),
);
const QueueContainer = styled.div`
position: relative;
display: flex;
@@ -61,27 +70,41 @@ export const FullScreenPlayerQueue = () => {
const { t } = useTranslation();
const { activeTab, opacity } = useFullScreenPlayerStore();
const { setStore } = useFullScreenPlayerStoreActions();
const { type, webAudio } = usePlaybackSettings();
const headerItems = [
{
active: activeTab === 'queue',
icon: <RiFileMusicLine size="1.5rem" />,
label: t('page.fullscreenPlayer.upNext'),
onClick: () => setStore({ activeTab: 'queue' }),
},
{
active: activeTab === 'related',
icon: <HiOutlineQueueList size="1.5rem" />,
label: t('page.fullscreenPlayer.related'),
onClick: () => setStore({ activeTab: 'related' }),
},
{
active: activeTab === 'lyrics',
icon: <RiFileTextLine size="1.5rem" />,
label: t('page.fullscreenPlayer.lyrics'),
onClick: () => setStore({ activeTab: 'lyrics' }),
},
];
const headerItems = useMemo(() => {
const items = [
{
active: activeTab === 'queue',
icon: <RiFileMusicLine size="1.5rem" />,
label: t('page.fullscreenPlayer.upNext'),
onClick: () => setStore({ activeTab: 'queue' }),
},
{
active: activeTab === 'related',
icon: <HiOutlineQueueList size="1.5rem" />,
label: t('page.fullscreenPlayer.related'),
onClick: () => setStore({ activeTab: 'related' }),
},
{
active: activeTab === 'lyrics',
icon: <RiFileTextLine size="1.5rem" />,
label: t('page.fullscreenPlayer.lyrics'),
onClick: () => setStore({ activeTab: 'lyrics' }),
},
];
if (type === PlaybackType.WEB && webAudio) {
items.push({
active: activeTab === 'visualizer',
icon: <RiFileTextLine size="1.5rem" />,
label: t('page.fullscreenPlayer.visualizer', { postProcess: 'titleCase' }),
onClick: () => setStore({ activeTab: 'visualizer' }),
});
}
return items;
}, [activeTab, setStore, t, type, webAudio]);
return (
<GridContainer
@@ -91,6 +114,7 @@ export const FullScreenPlayerQueue = () => {
<Group
grow
align="center"
className="full-screen-player-queue-header"
position="center"
>
{headerItems.map((item) => (
@@ -127,6 +151,10 @@ export const FullScreenPlayerQueue = () => {
</QueueContainer>
) : activeTab === 'lyrics' ? (
<Lyrics />
) : activeTab === 'visualizer' && type === PlaybackType.WEB && webAudio ? (
<Suspense fallback={<></>}>
<Visualizer />
</Suspense>
) : null}
</GridContainer>
);
@@ -382,7 +382,11 @@ const Controls = () => {
</Option.Control>
</Option>
<Option>
<Option.Label>Lyrics offset (ms)</Option.Label>
<Option.Label>
{t('page.fullscreenPlayer.config.lyricOffset', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<NumberInput
defaultValue={lyricConfig.delayMs}
@@ -244,8 +244,8 @@ export const LeftControls = () => {
<React.Fragment key={`bar-${artist.id}`}>
{index > 0 && <Separator />}
<Text
$link
component={Link}
$link={artist.id !== ''}
component={artist.id ? Link : undefined}
overflow="hidden"
size="md"
to={
@@ -253,7 +253,7 @@ export const LeftControls = () => {
? generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: artist.id,
})
: ''
: undefined
}
weight={500}
>
@@ -293,7 +293,10 @@ export const RightControls = () => {
{!isMinWidth ? (
<PlayerButton
icon={<HiOutlineQueueList size="1.1rem" />}
tooltip={{ label: 'View queue', openDelay: 500 }}
tooltip={{
label: t('player.viewQueue', { postProcess: 'titleCase' }),
openDelay: 500,
}}
variant="secondary"
onClick={handleToggleQueue}
/>
@@ -145,7 +145,7 @@ export const ShuffleAllModal = ({
max={500}
min={1}
value={limit}
onChange={(e) => setStore({ limit: e ? Number(e) : 0 })}
onChange={(e) => setStore({ limit: e ? Number(e) : 500 })}
/>
<Group grow>
<NumberInput
@@ -208,6 +208,7 @@ export const ShuffleAllModal = ({
<Divider />
<Group grow>
<Button
disabled={!limit}
leftIcon={<RiAddBoxFill size="1rem" />}
type="submit"
variant="default"
@@ -216,6 +217,7 @@ export const ShuffleAllModal = ({
Add
</Button>
<Button
disabled={!limit}
leftIcon={<RiAddCircleFill size="1rem" />}
type="submit"
variant="default"
@@ -225,6 +227,7 @@ export const ShuffleAllModal = ({
</Button>
</Group>
<Button
disabled={!limit}
leftIcon={<RiPlayFill size="1rem" />}
type="submit"
variant="filled"
@@ -0,0 +1,72 @@
import { createRef, useCallback, useEffect, useState } from 'react';
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
import AudioMotionAnalyzer from 'audiomotion-analyzer';
import styled from 'styled-components';
import { useSettingsStore } from '/@/renderer/store';
const StyledContainer = styled.div`
max-width: 100%;
margin: auto;
canvas {
width: 100%;
margin: auto;
}
`;
export const Visualizer = () => {
const { webAudio } = useWebAudio();
const canvasRef = createRef<HTMLDivElement>();
const accent = useSettingsStore((store) => store.general.accent);
const [motion, setMotion] = useState<AudioMotionAnalyzer>();
const [length, setLength] = useState(500);
useEffect(() => {
const { context, gain } = webAudio || {};
if (gain && context && canvasRef.current && !motion) {
const audioMotion = new AudioMotionAnalyzer(canvasRef.current, {
ansiBands: true,
audioCtx: context,
connectSpeakers: false,
gradient: 'prism',
mode: 4,
showPeaks: false,
smoothing: 0.8,
});
setMotion(audioMotion);
audioMotion.connectInput(gain);
}
return () => {};
}, [accent, canvasRef, motion, webAudio]);
const resize = useCallback(() => {
const body = document.querySelector('.full-screen-player-queue-container');
const header = document.querySelector('.full-screen-player-queue-header');
if (body && header) {
const width = body.clientWidth - 30;
const height = body.clientHeight - header.clientHeight - 30;
setLength(Math.min(width, height));
}
}, []);
useEffect(() => {
resize();
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
};
}, [resize]);
return (
<StyledContainer
ref={canvasRef}
style={{ height: length, width: length }}
/>
);
};

Some files were not shown because too many files have changed in this diff Show More