Compare commits

...

528 Commits

Author SHA1 Message Date
jeffvli fb022891fe update to v0.12.6 2025-05-08 00:44:13 -07:00
Kendall Garner 5d9906b8f2 include album artist song/album count for jellyfin, and disable playing/adding playinsts for artists with no albums 2025-05-07 21:16:47 -07:00
jeffvli 6f7cb468b2 fix regression on subsonic album artist play 2025-05-07 20:59:16 -07:00
Kendall Garner 076693e969 Merge branch 'development' of github.com:jeffvli/feishin into development 2025-05-07 20:01:04 -07:00
Kendall Garner 781d8055b5 minor artist count fixes 2025-05-07 19:53:23 -07:00
jeffvli 960bb5c660 fix navigation to detail page on artist list 2025-05-07 19:40:54 -07:00
jeffvli 42bb2bf66f fix regression on album artist play button 2025-05-07 19:25:25 -07:00
jeffvli f03d88cd8c batch jellyfin song list requests when fetching by albumId (#922) 2025-05-07 01:42:32 -07:00
jeffvli 58f6535ba6 revert electron to gtk 3 (#923) 2025-05-07 01:28:54 -07:00
jeffvli 9a59ce3613 fix casing on artist albums title 2025-05-07 01:15:00 -07:00
jeffvli 6f37e13611 additional fix to sticky table header to account for native 2025-05-06 18:54:10 -07:00
jeffvli 3c494f1c72 update to v0.12.5 2025-05-06 18:27:02 -07:00
jeffvli ec0e7256cb fix set rating click on context menu (#900)
- star rating icon overriddes the pointer click event
2025-05-06 18:23:14 -07:00
jeffvli 262203b62d handle arrow keys in global hotkey (#913) 2025-05-06 15:19:34 -07:00
jeffvli 41bdc1a7b7 disable toast overlay blocking playerbar (#888) 2025-05-06 15:00:43 -07:00
jeffvli d35e73792f fix item playback buttons on search list (#885) 2025-05-06 14:50:59 -07:00
jeffvli 4a3604b1a8 handle playback on new artist list 2025-05-06 14:43:42 -07:00
jeffvli b9611589ba fix casing on some translation strings 2025-05-06 13:23:29 -07:00
jeffvli 12c517f0ff update to v0.12.4 2025-05-06 03:40:42 -07:00
Hosted Weblate 01884ab656 Translated using Weblate (Finnish)
Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Finnish)

Currently translated at 77.3% (508 of 657 strings)

Translated using Weblate (Finnish)

Currently translated at 69.2% (455 of 657 strings)

Translated using Weblate (Finnish)

Currently translated at 64.9% (427 of 657 strings)

Translated using Weblate (Finnish)

Currently translated at 64.9% (427 of 657 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Lunya <Lunya2@protonmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: jonoafi <joona@jonottaa.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fi/
Translation: feishin/Translation
2025-05-06 12:28:46 +02:00
Hosted Weblate 0ba830d5d7 Translated using Weblate (Persian)
Currently translated at 74.8% (492 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-05-06 12:28:46 +02:00
Hosted Weblate b08a0d178c Translated using Weblate (Spanish)
Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2025-05-06 12:28:46 +02:00
Hosted Weblate 9afa64b537 Translated using Weblate (Polish)
Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Polish)

Currently translated at 93.4% (614 of 657 strings)

Translated using Weblate (Polish)

Currently translated at 92.2% (606 of 657 strings)

Co-authored-by: Adam Perkowski <adas1per@protonmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: skajmer <skajmer@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translation: feishin/Translation
2025-05-06 12:28:46 +02:00
Hosted Weblate be8bc74ab5 Translated using Weblate (Russian)
Currently translated at 95.7% (629 of 657 strings)

Translated using Weblate (Russian)

Currently translated at 95.2% (626 of 657 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rachel Podya <homicide@disroot.org>
Co-authored-by: zv0r <mount.dev.brain@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ru/
Translation: feishin/Translation
2025-05-06 12:28:46 +02:00
Hosted Weblate 2f4e228fa1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 61.1% (402 of 657 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Joao <joaohirasawaa@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/
Translation: feishin/Translation
2025-05-06 12:28:46 +02:00
jeffvli 35ee7e4606 fix position of sticky table header when titlebar present 2025-05-06 03:27:30 -07:00
jeffvli b265f2817b bump to electron 36 (#921) 2025-05-06 02:58:22 -07:00
Kendall Garner 48af447838 fix entities version 2025-04-25 10:35:52 -07:00
Kendall Garner 397df0c9c6 npm update 2025-04-25 09:53:15 -07:00
Kendall Garner 68759a2613 artists fixes 2025-04-25 09:30:17 -07:00
Kendall Garner c376293f2f always add all artists to subsonic 2025-04-23 23:33:58 -07:00
Kendall Garner e84a4b20bc add artist list 2025-04-23 23:27:06 -07:00
Kendall Garner 14e9f6ac41 bump electronVersion 2025-04-22 07:41:56 -07:00
Kendall Garner 0115ecb59b fix repeat one/repeat all scrobble 2025-04-20 10:54:44 -07:00
Kendall Garner 1555b827ee fix: normalize artists with no album count 2025-03-10 14:20:19 -07:00
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
jeffvli 4a8cd63046 Update to v0.8.0 2024-09-02 22:48:52 -07:00
Hosted Weblate 549b53b4a4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (595 of 595 strings)

Co-authored-by: ENDzZ <godzmichael@outlook.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-09-03 05:31:30 +00:00
Hosted Weblate f33d13f574 Translated using Weblate (French)
Currently translated at 100.0% (593 of 593 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
2024-09-03 05:31:29 +00:00
Hosted Weblate 4da51a16c9 Translated using Weblate (Spanish)
Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (622 of 622 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (612 of 612 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (604 of 604 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (599 of 599 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (595 of 595 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-03 05:31:29 +00:00
Hosted Weblate 8d138ff974 Translated using Weblate (Dutch)
Currently translated at 40.1% (245 of 610 strings)

Translated using Weblate (Dutch)

Currently translated at 40.1% (245 of 610 strings)

Translated using Weblate (Dutch)

Currently translated at 40.0% (244 of 610 strings)

Translated using Weblate (Dutch)

Currently translated at 40.7% (244 of 599 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Idris Saklou <idrissaklou@hotmail.com>
Co-authored-by: Joren Vansteenkiste <vansteenkiste.joren@telenet.be>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/nl/
Translation: feishin/Translation
2024-09-03 05:31:28 +00:00
Hosted Weblate 9373937436 Translated using Weblate (Czech)
Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (621 of 621 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (612 of 612 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (604 of 604 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (599 of 599 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (595 of 595 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-03 05:31:27 +00:00
Hosted Weblate 46bbe6b95f Translated using Weblate (German)
Currently translated at 88.2% (525 of 595 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: elia <me@elia.li>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/de/
Translation: feishin/Translation
2024-09-03 05:31:26 +00:00
Kendall Garner 56c229a5e0 [slightly less scuffed bugfix]: Update table rating/favorite when updated anywhere … (#707)
* [scuffed bugfix]: Update table rating/favorite when updated anywhere else

Modify player store to have temporary state for favorite/rating update
Add effect handler for `virtual-table` to update rating/favorite for players

Note that this does not handle song grid view.
Using a similar handler for gird view did not work, as it appeared to result in inconsistent state.

Finally, this is probably not the optimal solution.
Performance appears fine for ~20k items, but no guarantees.

* restore should update song

* update song rating/favorite/played everywhere except playlist

* special rule for playlists

* use iterator instead
2024-09-02 22:31:20 -07:00
Kendall Garner 9d44f0fc08 [bugfix]: don't be loading if top songs disabled 2024-09-02 19:26:47 -07:00
Benjamin 903d1479a4 adjust rules for user selection (#723) 2024-09-03 00:48:52 +00:00
Kendall Garner 7299bcefb2 Merge branch 'development' of github.com:jeffvli/feishin into development 2024-09-02 10:57:21 -07:00
Kendall Garner 6b7c69e90a fix seeking between 0-1 seconds 2024-09-02 10:56:46 -07:00
dependabot[bot] 4601838afe Bump electron-updater in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [electron-updater](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-updater).


Updates `electron-updater` from 6.3.0 to 6.3.1
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-updater@6.3.1/packages/electron-updater)

---
updated-dependencies:
- dependency-name: electron-updater
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 17:09:50 -07:00
Kendall Garner f7dd634f67 reorder album artist page 2024-09-01 16:48:43 -07:00
Pyx eb50c69a35 Album blur, allow clicking the playerbar to toggle the player, misc changes (#717)
* Album blur, allow clicking the playerbar to toggle the player

* Fix stopProporagion, sync package with upsteam, update translation

* recommit my existing changes

* Update default albumBackgroundBlur to 6

* according to git this commit resets the package files

* merge with our fork because pyx forgot to add it

* try adding a setting

* change the playerbar animation

* make the animation quicker bc its choppy

* change playerbar to use a bool instead

* requested opacity fix

* Refactor classes to use clsx

---------

Co-authored-by: iiPython <ben@iipython.dev>
Co-authored-by: Jeff <42182408+jeffvli@users.noreply.github.com>
2024-09-01 23:42:01 +00:00
Kendall Garner b93ad40571 Merge pull request #720 from pyxfluff/patch-1
Add play count to albums
2024-09-01 12:50:19 -07:00
Kendall Garner 748db032c7 add translation 2024-09-01 12:48:11 -07:00
Kendall Garner 80931d1b19 jellyfin random play filter 2024-09-01 12:25:50 -07:00
Kendall Garner 93377dcc4f fix jellyfin playlists, add is public 2024-09-01 09:37:37 -07:00
Kendall Garner 528bef01f0 provide transcoding support 2024-09-01 08:26:30 -07:00
Kendall Garner da95a644c8 upgrade webpack 2024-08-29 21:42:44 -07:00
Pyx f5a04980a4 Add play count to albums 2024-08-29 23:03:00 -04:00
Kendall Garner 93055b3bf1 allow disabling web audio 2024-08-29 19:44:24 -07:00
Kendall Garner e68847f50a slightly better error handling 2024-08-27 21:27:49 -07:00
Kendall Garner 43fe1a235e fix context menu original item 2024-08-27 21:21:44 -07:00
Kendall Garner 62c372d0c7 dont't move to 0 when removing current item from queue 2024-08-27 21:14:08 -07:00
Kendall Garner 279842b894 bump node mpv 2024-08-27 20:11:08 -07:00
Kendall Garner 6125901023 [enhancement]: custom css 2024-08-27 08:26:34 -07:00
Kendall Garner 004c9a8d06 allow hiding context menu items 2024-08-26 21:35:12 -07:00
Kendall Garner f746114041 increase metadata size of library header 2024-08-26 20:26:37 -07:00
Kendall Garner 9f4861a78a use context menu instead of button 2024-08-25 22:17:11 -07:00
Kendall Garner 32b984a18b add playlist context menu button to sidebar 2024-08-25 22:08:07 -07:00
Kendall Garner 8a8542ddb1 simplify disc/subtitle for album list 2024-08-25 21:34:43 -07:00
Kendall Garner b41a1a8b15 [bugfix]: properly update song when restoring queue 2024-08-25 20:02:44 -07:00
Kendall Garner 9923c021fa better album dates 2024-08-25 19:52:44 -07:00
Kendall Garner 8c929d0dc3 fixed size for different sizes 2024-08-25 18:07:51 -07:00
Kendall Garner fb1e33fad5 autosize library item text 2024-08-25 17:50:46 -07:00
Kendall Garner c4677a63f6 [enhancement]: allow downloading individual tracks for external use 2024-08-25 17:11:24 -07:00
Kendall Garner 10fca2dc12 enable reordering non-smart playlists 2024-08-25 15:21:56 -07:00
Kendall Garner 0b383b758e support collapsing shared playlists 2024-08-24 21:09:44 -07:00
Kendall Garner ccb6f2c8b0 very niche error handling for no audio device id but still have error checking 2024-08-24 20:36:04 -07:00
Kendall Garner a44071fedd add error checking for set sink id (case of no devices at all) 2024-08-24 20:13:30 -07:00
Kendall Garner b527d579fd Revert upgrade of discord-rpc
Some horrible magic can result in this upgrade causing compiler errors.
No idea why.
2024-08-24 15:22:55 -07:00
Kendall Garner 5b2977e5e8 [enhancement]: support viewing current/setting current time in remote 2024-08-24 13:26:45 -07:00
Kendall Garner b347b794b9 fix micromatch 2024-08-23 19:54:39 -07:00
Kendall Garner ad81790c90 add more places for share in context menu 2024-08-23 19:53:40 -07:00
Kendall Garner 906ffee8bc thanks discord [support changing listen type] 2024-08-23 10:34:18 -07:00
Kendall Garner 284db988c9 [enhancement]: use discord activity type listening 2024-08-23 08:27:40 -07:00
Kendall Garner 271be93a96 fix prettier/lint 2024-08-23 08:19:27 -07:00
Kendall Garner 121b036aaf bump i18next-parser 2024-08-23 08:00:59 -07:00
Kendall Garner 028ccfb1cd fix album art res 0 and allow resizing volume bar 2024-08-22 21:57:58 -07:00
Kendall Garner 37b0407188 simplify webaudio replaygain to reduce pop-in 2024-08-21 23:04:37 -07:00
Kendall Garner 616fd45734 add minimum duration check for crossfade 2024-08-21 22:47:35 -07:00
Kendall Garner a537642990 [bugfix]: set index to current track when unshuffling 2024-08-20 19:10:05 -07:00
Kendall Garner 9c6abcb32c Use break-word over break-all 2024-08-20 16:35:08 +00:00
Kendall Garner af69a58418 [bugfix]: use chrome-specific implementation for web audio sink 2024-08-19 22:38:51 -07:00
Kendall Garner ebebdc1e03 forcefully break long lines 2024-08-19 22:12:32 -07:00
Kendall Garner 7d185f6563 clarify text 2024-08-19 22:02:54 -07:00
Kendall Garner 88741a8616 add ability to configure double click behavior 2024-08-19 21:56:55 -07:00
Kendall Garner 94edda1856 better handling of grid refresh 2024-08-19 21:36:56 -07:00
Kendall Garner 886786d428 [enhancement]: add background opacity to buttons in full screen player 2024-08-19 19:03:19 -07:00
Kendall Garner e4ca0164fa [bugfix]: Jellyfin do not force width = height for images 2024-08-15 23:35:29 -07:00
Jeff 08db18359a Merge pull request #687 from jeffvli/dependabot/npm_and_yarn/npm_and_yarn-bb2c7e2e3f
Bump electron-updater from 4.6.5 to 6.3.0 in the npm_and_yarn group across 1 directory
2024-07-30 11:53:25 -07:00
jeffvli f2beeef0e9 Bump to v0.7.3 2024-07-30 03:26:21 -07:00
dependabot[bot] 6daba77bae Bump electron-updater in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [electron-updater](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-updater).


Updates `electron-updater` from 4.6.5 to 6.3.0
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-updater@6.3.0/packages/electron-updater)

---
updated-dependencies:
- dependency-name: electron-updater
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-30 10:19:42 +00:00
jeffvli fd893224b3 Bump electronVersion in build configuration (#686) 2024-07-30 03:17:32 -07:00
Hosted Weblate 8815246221 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (593 of 593 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-07-25 19:36:24 +00:00
Hosted Weblate 5e353649a4 Translated using Weblate (Korean)
Currently translated at 1.6% (10 of 593 strings)

Co-authored-by: 박용현 <pyh5523@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ko/
Translation: feishin/Translation
2024-07-25 19:36:24 +00:00
Jeff 4ca7b4221c Merge pull request #680 from dragonish/metadata
Fix full screen player metadata may not change when switching queue
2024-07-25 12:36:19 -07:00
dragonish 6c61ea898f Fix full screen player metadata may not change when switching queue 2024-07-26 01:52:40 +08:00
Kendall Garner 0b786b025f Merge pull request #676 from dragonish/lyrics
Fix synchronized lyrics that may become unaligned during playback after re-rendering
2024-07-23 04:09:11 +00:00
dragonish e106fb324f Fix synchronized lyrics that may become unaligned during playback after re-rendering 2024-07-22 21:33:01 +08:00
Jeff 3edc6bab04 Merge pull request #668 from yuygfgg/patch-1
fix blank screen when reopening window on macos
2024-07-21 04:16:17 -07:00
yuygfgg 0113ef2582 restore comments 2024-07-14 21:15:58 +08:00
yuygfgg 493b81875a fix blank screen when reopening window on macos 2024-07-14 20:46:04 +08:00
Jeff ed8e5e69ba Merge pull request #664 from sel10ut/bugfix/fix-minimize-setting
Fix minimize to tray setting toggle
2024-07-14 02:53:48 -07:00
jeffvli d27a656568 Bump electron to v31 (again) 2024-07-13 01:30:18 -07:00
sel10ut 582739a091 fix(settings): set proper default minimize to tray check 2024-07-04 13:29:47 +03:00
Jeff fb930f1197 Merge pull request #654 from minicoz/feat/docker-compose-readme
Adding in docker compose instructions to README
2024-07-03 16:30:45 -07:00
Jeff 849aa97e63 Merge pull request #663 from jeffvli/dependabot/npm_and_yarn/release/app/npm_and_yarn-99150a289a
Bump ws from 8.17.1 to 8.18.0 in /release/app in the npm_and_yarn group across 1 directory
2024-07-03 16:29:51 -07:00
dependabot[bot] bc5abe3ec1 Bump ws in /release/app in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the /release/app directory: [ws](https://github.com/websockets/ws).


Updates `ws` from 8.17.1 to 8.18.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.17.1...8.18.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-03 23:21:13 +00:00
Jeff 46cd783fa3 Merge pull request #662 from jeffvli/dependabot/npm_and_yarn/npm_and_yarn-8fcff2be61
Bump the npm_and_yarn group across 2 directories with 2 updates
2024-07-03 16:20:05 -07:00
jeffvli a93173f55a Bump to v0.7.2 2024-07-03 15:57:57 -07:00
jeffvli bd04168209 Add languages 2024-07-03 15:56:48 -07:00
dependabot[bot] 76ffdb6627 Bump the npm_and_yarn group across 2 directories with 2 updates
Bumps the npm_and_yarn group with 2 updates in the / directory: [ws](https://github.com/websockets/ws) and [braces](https://github.com/micromatch/braces).
Bumps the npm_and_yarn group with 1 update in the /release/app directory: [ws](https://github.com/websockets/ws).


Updates `ws` from 7.5.7 to 7.5.10
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.7...7.5.10)

Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

Updates `ws` from 8.13.0 to 8.17.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.7...7.5.10)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: braces
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: ws
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-03 22:31:09 +00:00
Hosted Weblate f674260df3 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.4% (588 of 591 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: SunSpring <yearnsun@gmail.com>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-07-03 22:29:28 +00:00
Hosted Weblate e9315886b7 Translated using Weblate (Korean)
Currently translated at 1.1% (7 of 591 strings)

Added translation using Weblate (Korean)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: hubag <pyosy17@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ko/
Translation: feishin/Translation
2024-07-03 22:29:28 +00:00
Hosted Weblate 4741dd0d77 Translated using Weblate (Finnish)
Currently translated at 19.6% (116 of 591 strings)

Added translation using Weblate (Finnish)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jonne Saloranta <saloranta.jonne@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fi/
Translation: feishin/Translation
2024-07-03 22:29:27 +00:00
Hosted Weblate 7a1c4f5082 Translated using Weblate (Spanish)
Currently translated at 100.0% (593 of 593 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (586 of 586 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (586 of 586 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-07-03 22:29:27 +00:00
Hosted Weblate abf339bb58 Translated using Weblate (Polish)
Currently translated at 100.0% (586 of 586 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translation: feishin/Translation
2024-07-03 22:29:26 +00:00
Hosted Weblate bb8f67c4c1 Translated using Weblate (Czech)
Currently translated at 100.0% (593 of 593 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (586 of 586 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-07-03 22:29:26 +00:00
Hosted Weblate e416c2a3b6 Translated using Weblate (Russian)
Currently translated at 97.2% (575 of 591 strings)

Translated using Weblate (Russian)

Currently translated at 97.2% (575 of 591 strings)

Translated using Weblate (Russian)

Currently translated at 95.0% (562 of 591 strings)

Translated using Weblate (Russian)

Currently translated at 95.0% (562 of 591 strings)

Translated using Weblate (Russian)

Currently translated at 70.7% (418 of 591 strings)

Translated using Weblate (Russian)

Currently translated at 70.7% (418 of 591 strings)

Co-authored-by: Blueberry <igory.ygr200@gmail.com>
Co-authored-by: Eugeniy <zamelane@vk.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ShaDream <mogilnikovshadream@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ru/
Translation: feishin/Translation
2024-07-03 22:29:25 +00:00
Hosted Weblate 5af4344168 Translated using Weblate (English)
Currently translated at 100.0% (586 of 586 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-07-03 22:29:25 +00:00
Hosted Weblate 55e958b5da Translated using Weblate (Portuguese (Brazil))
Currently translated at 34.3% (201 of 586 strings)

Co-authored-by: Cyber Hippie <neves.j@protonmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/
Translation: feishin/Translation
2024-07-03 22:29:24 +00:00
Jeff 2a231ed7af Merge pull request #659 from kgarner7/fix-electron-31-build
[bugfix]: use tsx instead of ts-node, update @electron/rebuild
2024-07-03 15:29:17 -07:00
jeffvli a26f7feb31 Readd transpile-only on dev 2024-07-03 15:13:54 -07:00
jeffvli 90e267d9c7 Bump node version from 16 -> 18 in builders 2024-07-03 15:08:14 -07:00
jeffvli 1ab09c5488 Move tsx and electron/rebuild to devDependencies 2024-07-03 15:05:01 -07:00
jeffvli eadbf3ad7b Readd ts-node 2024-07-03 15:00:56 -07:00
Jeff 42bcc4190c Merge pull request #660 from sel10ut/bugfix/jellyfin-multiple-sessions
(Jellyfin) Allow multiple sessions from the same user with different instances
2024-07-03 14:49:41 -07:00
jeffvli 3c44db377b Revert "Bump electron to v31 (#643)"
This reverts commit 0c14427bdb.
2024-07-03 14:40:09 -07:00
sel10ut ba64f4c467 refactor(jellyfin): migrate auth method 2024-07-03 22:41:35 +03:00
sel10ut 596bf3a378 fix(jellyfin): allow multiple sessions from the same client type
Allow multiple sessions from the same user with different instances.
Instead of sending a hard-coded string, send a randomly generated
string `deviceId`, which already exists and is created for each
new installation.
2024-07-03 22:12:15 +03:00
Kendall Garner 91e6119afa use tsx instead of ts-node, update @electron/rebuild 2024-07-03 11:53:59 -07:00
mlnl b053538f94 Update README.md
removed unused volume
2024-07-03 18:51:46 +00:00
jeffvli 0c14427bdb Bump electron to v31 (#643) 2024-07-03 02:10:13 -07:00
Kendall Garner 110a1a63f0 simplify remote/media session (#632) 2024-07-03 01:47:26 -07:00
Kendall Garner d57b4b4b68 [bugfix]: properly clean up MPV on quit, use pid for socket (#627)
* fix cleanup

* don't delete file if windows
2024-07-03 01:36:01 -07:00
Benjamin 4191edb88c fix cache settings not being sentence cased (#657) 2024-07-03 01:29:53 -07:00
sel10ut b38930a277 tweak(jellyfin): fetch actual recent album releases (#629) 2024-07-03 01:29:34 -07:00
Gelaechter ea865f44b1 Allow jumping to lyrics (#656) 2024-07-03 01:24:31 -07:00
isaiahfuller 0768ce80a7 Add option to play similar tracks from the context menu (#650)
* Add option to play similar songs from context menu

* remove console.log
2024-07-03 01:17:56 -07:00
minicoz c960cc44b7 adding env options 2024-06-28 09:10:47 -07:00
minicoz a84276579b adding in docker compose instructions 2024-06-28 09:08:09 -07:00
Kendall Garner b30fadd149 navidrome album artist covoer art bodge 2024-05-29 02:53:41 -07:00
Kendall Garner aa89c5e80e [enhancement]: apply formatting to card values 2024-05-26 12:20:01 -07:00
Kendall Garner 38ed083693 [enhancement]: support using native image aspect ratio 2024-05-25 11:15:30 -07:00
Kendall Garner 961d1838c0 reopen window if exit to tray 2024-05-25 08:14:01 -07:00
Kendall Garner 67deb3e3d8 [bugfix]: only restart time when now for web 2024-05-23 20:41:01 -07:00
Kendall Garner 79384fa4ed add songCount to table localization 2024-05-17 23:17:31 -07:00
Kendall Garner bb2f8461ed [enhancement]: support toggling feature carousel 2024-05-15 21:48:20 -07:00
Kendall Garner 168153b211 [bugfix]: restart timestamp when adding to queue 2024-05-10 12:27:33 -07:00
Kendall Garner c5e8472746 [bugfix]: handle song grid sparse array 2024-05-09 00:14:59 -07:00
Kendall Garner a9e0689619 activate tray on single click 2024-05-08 19:10:46 -07:00
jeffvli 69acbc9a28 Bump to v0.7.1 2024-05-06 22:52:16 -07:00
jeffvli 58484b87f4 Split macOs and Windows builders 2024-05-06 22:31:22 -07:00
jeffvli 6858485e41 Add new languages 2024-05-06 19:56:16 -07:00
Hosted Weblate ebd97c253b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (586 of 586 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: HanaO00 <lwin24452@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <KosmoMoustache@users.noreply.hosted.weblate.org>
Co-authored-by: Leonardo Pizio <pizio.leonardo@gmail.com>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-05-07 04:22:27 +02:00
Hosted Weblate 2c834cd3a8 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: HanaO00 <lwin24452@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <KosmoMoustache@users.noreply.hosted.weblate.org>
Co-authored-by: Leonardo Pizio <pizio.leonardo@gmail.com>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-05-07 04:22:27 +02:00
Hosted Weblate dff6d27c23 Translated using Weblate (Spanish)
Currently translated at 100.0% (586 of 586 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: HanaO00 <lwin24452@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <KosmoMoustache@users.noreply.hosted.weblate.org>
Co-authored-by: Leonardo Pizio <pizio.leonardo@gmail.com>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-05-07 04:22:27 +02:00
Hosted Weblate 3aed97c139 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: HanaO00 <lwin24452@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <KosmoMoustache@users.noreply.hosted.weblate.org>
Co-authored-by: Leonardo Pizio <pizio.leonardo@gmail.com>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-05-07 04:22:27 +02:00
Hosted Weblate 8fe93b4b2e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: HanaO00 <lwin24452@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <KosmoMoustache@users.noreply.hosted.weblate.org>
Co-authored-by: Leonardo Pizio <pizio.leonardo@gmail.com>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-05-07 04:22:27 +02:00
Hosted Weblate c634a07c5d Translated using Weblate (Czech)
Currently translated at 100.0% (586 of 586 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 89.7% (525 of 585 strings)

Translated using Weblate (French)

Currently translated at 100.0% (570 of 570 strings)

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (585 of 585 strings)

Translated using Weblate (Italian)

Currently translated at 98.6% (576 of 584 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (585 of 585 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: HanaO00 <lwin24452@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <KosmoMoustache@users.noreply.hosted.weblate.org>
Co-authored-by: Leonardo Pizio <pizio.leonardo@gmail.com>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-05-07 04:22:27 +02:00
Hosted Weblate 0235a569a0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 32.0% (188 of 586 strings)

Co-authored-by: Cyber Hippie <neves.j@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/
Translation: feishin/Translation
2024-05-07 04:22:27 +02:00
jeffvli cbe1c878e7 Bump to v0.7.0 2024-05-06 19:21:48 -07:00
Kendall Garner 4afb893ce5 remove stray log 2024-05-05 19:56:35 -07:00
Kendall Garner 645697367d [enhancement]: support serach on settings page 2024-05-05 13:25:05 -07:00
jeffvli 683bb0222c Filter out current playlist on playlist operators 2024-05-02 23:04:57 -07:00
jeffvli ce0c07ebdb Add JSON preview for smart playlist query 2024-05-02 23:04:57 -07:00
jeffvli 785f0ef77f Add inPlaylist and notInPlaylist operators 2024-05-02 23:04:57 -07:00
jeffvli 7bfdbb5d92 Reduce min grid size on remaining list pages 2024-05-02 23:04:31 -07:00
Dylan Lathrum abdb2fee85 Allow smaller album covers in card/poster display 2024-05-02 23:04:31 -07:00
Kendall Garner d1bcd2b2fb [bugfix]: fix jellyfin add to playlist 2024-05-02 18:42:49 -07:00
Vukanović Stefan 297d6f0d2e LrcLib.net expects durations in seconds, not ms (#603) 2024-05-02 14:14:10 +00:00
dependabot[bot] 78ac5af178 Bump ejs in the npm_and_yarn group across 1 directory (#602)
Bumps the npm_and_yarn group with 1 update in the / directory: [ejs](https://github.com/mde/ejs).


Updates `ejs` from 3.1.9 to 3.1.10
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 14:03:29 +00:00
Kendall Garner 9cd8807a75 [bugfix]: Handle top-level songs for Jellyfin (#553)
* [bugfix]: Handle top-level songs for Jellyfin

If a song is at the top level of a music folder, Jellyfin will not
group that into an album (See https://jellyfin.org/docs/general/server/media/music/).

This PR introduces a few changes:
- Gives tracks with no album ID a special route (`/dummy/${id}`)
- Gives a new route for dummy albums, warning about the error. This is designed to look _like_ the album detail page

* `are are` > `are`

* revert name changes
2024-04-30 03:18:43 +00:00
Kendall Garner 620cca9ce3 Revert "Upgrade dependencies"
This reverts commit 89688455e0.
2024-04-28 21:03:31 -07:00
Kendall Garner 89688455e0 Upgrade dependencies
- mpris-service: migrate to @jellybrick/mpris-service, which has upgraded dependencies and uses class
- i18next-parser: 6 -> 8. This requires a small change to i18next-parser.config.js
2024-04-28 20:50:52 -07:00
Kendall Garner 5259f2401b upgrade framer-motion to 11 2024-04-28 18:59:05 -07:00
Kendall Garner c36f0a055d [enhancement]: parse replaygain from subsonic endpoints where available 2024-04-27 22:20:42 -07:00
Kendall Garner ef87a8c2a7 remove security from config 2024-04-24 07:38:59 -07:00
Kendall Garner 5da68d4243 Use issue templates 2024-04-24 07:37:38 -07:00
Kendall Garner dc95a3c66b [bugfix]: use persistent columns def instead of default merge behavior 2024-04-23 23:25:32 -07:00
Kendall Garner 087ea44737 [enhancement]: use jellyfin 10.9.0 lyrics 2024-04-22 19:44:10 -07:00
Benjamin cb2597d2c8 Implement Navidrome sharing (#575)
* add share item feature

* take care of (mostly) everything

* bugfixes

* allow clicking on notification to open url

* readd the missing modal after router migration

* remove unnecessary extension

---------

Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2024-04-22 03:03:22 +00:00
Kendall Garner 0d03b66fe5 prevent change of media state with empty queue 2024-04-20 22:01:29 -07:00
Kendall Garner ba531505af [enhancement]: Support toggling Album/Track view for gneres (#591)
* [enhancement]: Support toggling Album/Track view for gneres

The _primary_ purpose of this PR is to enable viewing tracks OR albums for genres.
This has a few requirements:
1. Ability to set default route for genres, **except** when already on song/album page
2. Ability to toggle between album and genre view
3. Fixed refresh for genre ID

Additionally, there was some refactoring:
- Since the *-list-headers had very similar functions for search, export that as a hook instead

* also use hook for album artist

* support switching albumartist tracks/albums

* remove toggle on song/album list, simplify logic
2024-04-20 06:14:31 +00:00
Kendall Garner 595eba152a [jellyfin]: prefer sort name over name 2024-04-19 23:11:26 -07:00
Kendall Garner ebd2f07447 show macOS warning one, don't show artist link if invalid 2024-04-17 22:44:35 -07:00
Kendall Garner 5d6503c1f4 [bugfix]: do not show now playing for duplicate item in play queue 2024-04-17 21:51:39 -07:00
Kendall Garner d03a3a11eb [enhancement]: Support react-router links in Modal (#586) 2024-04-17 14:29:46 +00:00
Kaydax 04b4d92f69 Fix portrait mode detection (#582)
* Fix portrait mode detection

* Revert changes done on playbar
2024-04-17 06:21:49 +00:00
Kendall Garner ec69cc22f9 use clearer separator character 2024-04-14 21:58:25 -07:00
Kendall Garner 19a88fea86 [bodge]: deal with Jellyfin returning dupliate tracks for album query 2024-04-13 16:28:36 -07:00
Kendall Garner 729538d885 [bugfix]: restart synchronized lyrics on repeat one (or track queued multiple times) 2024-04-12 20:52:10 -07:00
Kendall Garner 9f86a8179f fix clipping description, update docker compose sample 2024-04-12 19:44:52 -07:00
Kendall Garner 3976f5e5bf don't assume ref exists 2024-04-12 09:33:48 -07:00
Kendall Garner 90d3fb219d [bugfix]: restart track in queue for web player 2024-04-12 09:29:36 -07:00
Kendall Garner cabd69772e [bugfix]: mantine bodge 2024-04-11 08:25:53 -07:00
Kendall Garner 9339c08777 [bugfix]: handle unclean MPV exit with existing content 2024-04-10 21:18:47 -07:00
Kendall Garner f5e047c7f5 update readme 2024-04-10 20:03:59 -07:00
Kendall Garner f79f9cc79e [bugfix]: deal with broken jellyfin 2024-04-09 22:49:44 -07:00
Kendall Garner c3fcb7487c [bugfix]: fix album artist order and mild race protection 2024-04-09 22:11:29 -07:00
Kendall Garner 15c6ef382a [bugfix]: fix combined title for artist, favoriting on grid pages 2024-04-08 23:15:59 -07:00
Kendall Garner 14086ebc9c improve similar items fallback, make ND album artist for song actually album artist, fix full screen race 2024-04-08 08:49:55 -07:00
Kendall Garner 2257e439a4 [navidrome]: prefer gerArtistInfo higher quality image 2024-04-06 21:36:30 -07:00
Kendall Garner 6824a5db7a [enhancement]: also save fullscreen/maximize 2024-04-06 21:14:05 -07:00
Kendall Garner c0110eff82 [enhancement]: save/restore screen position 2024-04-06 19:05:20 -07:00
Kendall Garner 2c17458fdf [enhancement]: allow copying/opening path in song modal 2024-04-06 16:13:09 -07:00
Kendall Garner c1345802aa bump size cell size 2024-04-03 21:28:27 -07:00
Kendall Garner 197497df05 [enhancement]: Show item details (#573)
* start

* More details, don't show manage server when other modal
2024-04-04 04:19:46 +00:00
Kendall Garner 7bebe286d5 sanitize album artist biography 2024-04-03 07:36:13 -07:00
Kendall Garner 24394fa858 Merge pull request #571 from iiPythonx/dynamicbg
[bugfix]: Add a fallback image to the dynamic background url
2024-04-03 01:54:15 +00:00
iiPython f7c6088cca add a fallback image to the dynamic background url 2024-04-02 12:58:26 -05:00
Kendall Garner 65eca32de3 [bugfix]: do not update mpris status unnecessarily 2024-04-02 08:46:38 -07:00
Kendall Garner ae167e63fd [bugfix]: shared only if owner exists 2024-04-01 22:31:59 -07:00
Kendall Garner ab17ba8add [bugfix]: fix scrobble race conditions 2024-04-01 22:13:06 -07:00
Kendall Garner 2854a91700 [bugfix]: actually implement size column 2024-04-01 20:53:00 -07:00
Kendall Garner 6bc778fa53 [bugfix]: fix smart playlist, do not error when trying to edit playlist as non-admin 2024-03-31 19:34:33 -07:00
Kendall Garner 44fcc33825 [enhancement]: add server menu on Navidrome error page 2024-03-31 17:47:17 -07:00
Kendall Garner e0e967385f Merge pull request #566 from kgarner7/fix-mpv-race-and-make-consistent
[bugfix]: Resolve MPV next/prev race condition
2024-03-31 23:12:03 +00:00
Kendall Garner 8900d8126c [bugfix]: queue all songs on search, consistent sort, clearing
- Previously, the search page would render initial page in one order, but search itself would be different order
This is resolved by having both virtual-table and search-header using listStoreKey
- When double clicking, now enqueue all the songs using the same sort
- Reset the search when clearing
2024-03-31 13:20:01 -07:00
Kendall Garner 65b045df03 [bugfix]: Resolve MPV next/prev race condition
Resolves #536.

With the previous implementation, next/previous would first update
the current queue and then call next/previous. However, since these were
asynchronous calls it was very likely that the second calls would fail
(and a test of adding delay showed that it actually caused a double skip).
This PR resolves this by just removing the prev/next.

Small other fixes:
- setQueue + pause -> setQueue(..., true)
- make MPV and web player have the same behavior for (pause/stop) where appropriate
2024-03-30 21:48:09 -07:00
Kendall Garner 918842e3a5 [bugfix]: use proper check for OS lyric existence 2024-03-30 20:36:49 -07:00
Kendall Garner a3573d4f9a add light theme for non-native titlebar 2024-03-30 14:11:57 -07:00
Kendall Garner 46fdacad81 Make home page modal play button use default behavior 2024-03-27 21:15:23 -07:00
Kendall Garner 67b8c7f1c0 Merge pull request #561 from jeffvli/dependabot/npm_and_yarn/npm_and_yarn-security-group-e0cd778f82
Bump the npm_and_yarn group across 1 directory with 1 update
2024-03-27 06:14:49 +00:00
dependabot[bot] 43f28317f6 Bump the npm_and_yarn group across 1 directory with 1 update
Bumps the npm_and_yarn group with 1 update in the / directory: [express](https://github.com/expressjs/express).


Updates `express` from 4.18.0 to 4.19.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.0...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-27 02:11:54 +00:00
dependabot[bot] f61cf8c331 Bump the npm_and_yarn group across 1 directory with 1 update (#557)
Bumps the npm_and_yarn group with 1 update in the / directory: [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware).


Updates `webpack-dev-middleware` from 5.3.1 to 5.3.4
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.1...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 02:10:24 +00:00
dependabot[bot] 340344b791 Bump the npm_and_yarn group across 1 directory with 1 update (#551)
Bumps the npm_and_yarn group with 1 update in the / directory: [follow-redirects](https://github.com/follow-redirects/follow-redirects).


Updates `follow-redirects` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 06:08:14 +00:00
dependabot[bot] ba1a2d5495 Bump the npm_and_yarn group across 1 directory with 2 updates (#542)
Bumps the npm_and_yarn group with 2 updates in the / directory: [app-builder-lib](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/app-builder-lib) and [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder).


Updates `app-builder-lib` from 24.9.0 to 24.13.3
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/v24.13.3/packages/app-builder-lib)

Updates `electron-builder` from 24.9.0 to 24.13.3
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/v24.13.3/packages/electron-builder)

---
updated-dependencies:
- dependency-name: app-builder-lib
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
- dependency-name: electron-builder
  dependency-type: direct:development
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 06:00:54 +00:00
jeffvli a177061f18 Bump to v0.6.1 2024-03-13 00:02:46 -07:00
jeffvli d806ade84c Potentially fix application stuck when Navidrome credential expires (#538) 2024-03-12 23:59:35 -07:00
jeffvli 854b76702b Downgrade to electron v26 (#534) 2024-03-12 23:33:33 -07:00
Hosted Weblate 1b08dfc5a5 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (549 of 549 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (543 of 543 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (519 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TianMiao <tianmiao.work@foxmail.com>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-03-06 00:55:52 +01:00
Hosted Weblate 60fc552088 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (549 of 549 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (543 of 543 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TianMiao <tianmiao.work@foxmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hant/
Translation: feishin/Translation
2024-03-06 00:55:52 +01:00
Hosted Weblate 86438fdb3d Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translation: feishin/Translation
2024-03-06 00:55:51 +01:00
Hosted Weblate 92beae6665 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Persian)

Currently translated at 50.0% (260 of 519 strings)

Added translation using Weblate (Persian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kaambiz <kambizx@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fa/
Translation: feishin/Translation
2024-03-06 00:55:51 +01:00
Hosted Weblate b110cfb94b Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translation: feishin/Translation
2024-03-06 00:55:50 +01:00
Hosted Weblate 08d4e80777 Translated using Weblate (Spanish)
Currently translated at 100.0% (549 of 549 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Spanish)

Currently translated at 100.0% (519 of 519 strings)

Co-authored-by: Fordas <fordas15@gmail.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/es/
Translation: feishin/Translation
2024-03-06 00:55:49 +01:00
Hosted Weblate ba03c19439 Translated using Weblate (Dutch)
Currently translated at 46.6% (242 of 519 strings)

Translated using Weblate (Dutch)

Currently translated at 39.4% (205 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Idris Saklou <idrissaklou@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/nl/
Translation: feishin/Translation
2024-03-06 00:55:49 +01:00
Hosted Weblate 1c6346572b Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translation: feishin/Translation
2024-03-06 00:55:48 +01:00
Hosted Weblate b91c715b13 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Polish)

Currently translated at 99.8% (518 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translation: feishin/Translation
2024-03-06 00:55:48 +01:00
Hosted Weblate f22434a50c Translated using Weblate (Czech)
Currently translated at 100.0% (549 of 549 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Czech)

Currently translated at 100.0% (519 of 519 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (519 of 519 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (519 of 519 strings)

Co-authored-by: Fjuro <ifjuro@proton.me>
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/cs/
Translation: feishin/Translation
2024-03-06 00:55:47 +01:00
Hosted Weblate 2086b57fb2 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translation: feishin/Translation
2024-03-06 00:55:47 +01:00
Hosted Weblate 2247ba08ea Translated using Weblate (German)
Currently translated at 98.5% (548 of 556 strings)

Translated using Weblate (German)

Currently translated at 98.5% (548 of 556 strings)

Translated using Weblate (German)

Currently translated at 98.5% (548 of 556 strings)

Translated using Weblate (German)

Currently translated at 93.3% (519 of 556 strings)

Translated using Weblate (German)

Currently translated at 93.3% (519 of 556 strings)

Translated using Weblate (German)

Currently translated at 92.2% (513 of 556 strings)

Translated using Weblate (German)

Currently translated at 92.2% (513 of 556 strings)

Translated using Weblate (German)

Currently translated at 91.5% (509 of 556 strings)

Translated using Weblate (German)

Currently translated at 91.5% (509 of 556 strings)

Translated using Weblate (German)

Currently translated at 91.1% (507 of 556 strings)

Translated using Weblate (German)

Currently translated at 91.1% (507 of 556 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (German)

Currently translated at 89.9% (467 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Lokke <accounts.lokke@googlemail.com>
Co-authored-by: Maik <maikguenes2003@gmail.com>
Co-authored-by: Rudi Mentaire <stoertebecker@byom.de>
Co-authored-by: full name <hosted-weblate.5w160@dralias.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/de/
Translation: feishin/Translation
2024-03-06 00:55:46 +01:00
jeffvli 5601613c3d Bump to v0.6.0 2024-03-05 15:55:28 -08:00
jeffvli d04d786951 Center auth loading spinner 2024-03-05 15:52:14 -08:00
jeffvli 613bfa7ae6 Various cleanup/fixes 2024-03-05 14:15:32 -08:00
jeffvli a7a5b92011 Adjust server feature naming convention 2024-03-05 14:12:37 -08:00
jeffvli a9315be259 Rename features types file 2024-03-05 14:05:01 -08:00
jeffvli e7b2f30718 Replace songId with albumId on dynamic image check 2024-03-05 00:45:45 -08:00
Kendall Garner 73845a9432 [enhancement]: better version checks for lyrics, Navidrome (#529)
- Actually make serverfeatures partial
- Navidrome: only set multiple structured lyrics if extension exists
- Navidrome/Subsonic: minor type checking of OS extension (Navidrome implementation detail)
- Jellyfin: add separate knob for lyrics. Note, this should also probably be behind some version check...
2024-03-05 00:31:51 -08:00
Benjamin d52d9136b8 [feat] Add a dynamic image option to the fullscreen player (#526)
* Add an option for a dynamic background image in the fullscreen player

* Center the background image and fix some more bugs

* More cleaning up the background image

* Add option for customizable blur amount

* Fix missing translation key for image blur

* Fix dynamic image shifting when player is opened

* Hide image blur size config if dynamic background is disabled

---------

Co-authored-by: Jeff <42182408+jeffvli@users.noreply.github.com>
2024-03-05 00:30:37 -08:00
Kendall Garner a45e7f24e4 [enhancements]: fix carousel, full screen metadata improvements (#524)
* fix carousel, full screen metadata improvements

* Add missing mapped keys on multiple artist

* Remove bg opacity on metadata section

* Improve visibility of currently playing song

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
2024-03-04 18:53:00 -08:00
Jeff 742cef3d81 Merge pull request #519 from kgarner7/related-similar-songs
[enhancement]: Make related tab on full screen player useful
2024-03-04 05:29:27 -08:00
Jeff 132b0e173f Merge branch 'development' into related-similar-songs 2024-03-04 05:04:54 -08:00
Jeff f7f3c5fe30 Merge pull request #517 from kgarner7/navidrome-shared-playlists
[enhancement]: Differentiate shared and owner playlists for Navidrome
2024-03-04 02:41:40 -08:00
jeffvli 83b5afb187 Remove ServerType check on shared playlist display 2024-03-04 02:37:37 -08:00
Kendall Garner 237fb91a60 [enhancement]: Differentiate shared and owner playlists for Navidrome
Resolves #368. If Navidrome, post-process the playlist list and separate
into owned and shared (in that order).
2024-03-04 02:29:35 -08:00
Jeff 8265ce48c4 Merge pull request #510 from kgarner7/improve-navidrome-auth
[bugfix]: Check for Navidrome authentication on startup
2024-03-04 02:27:10 -08:00
Jeff bf3431cbc6 Merge pull request #514 from kgarner7/navidrome-version
[enhancement]: Better version checks
2024-03-04 01:49:33 -08:00
Jeff cc6cad1d70 Merge branch 'development' into navidrome-version 2024-03-04 01:49:13 -08:00
jeffvli c8b1e2312a Change smart playlist feature check 2024-03-04 01:44:11 -08:00
jeffvli 84837a6887 Prevent version check from running on every query in Navidrome 2024-03-03 22:16:25 -08:00
jeffvli f1f6ccfd02 Normalize server feature set 2024-03-03 22:15:49 -08:00
Kendall Garner f50d1e0a8c support opacity 0, spellcheck 2024-03-01 19:54:30 -08:00
Kendall Garner 753ca01d41 [bugfix]: fix update rating for items with no data node 2024-02-29 17:28:46 -08:00
jeffvli 960c126283 Fix incorrect album artist sidebar icon 2024-02-28 21:07:50 -08:00
Kendall Garner 94b649fefe don't transform artist/album for fullscreen player 2024-02-26 22:47:17 -08:00
Alberto Rodríguez 28bb699024 Add a pre-defined server for the docker version (#413)
* Moved build to docker stage.

* Do not copy node_modules to the docker image

* Optimize Docker builds

* Lock a predefined server with enviroment variables

* Added a example docker compose file

* Removed useless layer

* Fix error with empty server type

* pass process via preload, use file, strict server check

* remove duplicate content-type

* update readme, docker compose

* bugfix: server lock false, not jellyfin

* fix preload type definition

* fix docker, web server lock check

---------

Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2024-02-24 06:55:23 +00:00
Kendall Garner 5caf0d439f [enhancement]: Start minimized (#522)
* [enhancement]: support starting minimized

* show window when dock clicked macos
2024-02-23 16:31:17 +00:00
dependabot[bot] 77fa723cf8 Bump the npm_and_yarn group across 2 directories with 1 update (#521)
Bumps the npm_and_yarn group with 1 update in the /. directory: [ip](https://github.com/indutny/node-ip).


Updates `ip` from 1.1.5 to 1.1.9
- [Commits](https://github.com/indutny/node-ip/compare/v1.1.5...v1.1.9)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 03:49:44 +00:00
Kendall Garner 77e220c873 [enhancement]: add codec column for tracks 2024-02-19 23:04:41 -08:00
Kendall Garner 860dd8b499 [enhancement]: add codec column for tracks 2024-02-19 20:34:36 -08:00
Kendall Garner 9113c6cc2e [bugfix]: carousel fixes 2024-02-19 13:55:11 -08:00
Kendall Garner 12d0eca2dd remember that current song can be an object 2024-02-19 10:27:00 -08:00
Kendall Garner 3a116e938e omit song from similar list, handle subsonic error 2024-02-19 10:09:05 -08:00
Kendall Garner f81bea339b don't pass song all the way down 2024-02-19 09:55:37 -08:00
Kendall Garner c947d09615 show nothing if no song exists 2024-02-19 09:01:44 -08:00
Kendall Garner af90d07414 bugfix 2024-02-19 08:56:06 -08:00
Kendall Garner 025124c379 [enhancement]: Make related tab on full screen player useful
Resolves #50. Adds a new set of components for fetching similar songs
from the current playing song. For Jellyfin, use the `/items/{itemId}/similar`
endpoint (may not work well for small libraries), and for Navidrome/Subsonic
use `getSimilarSongs`. _In theory_, this component can be used to get similar
songs anywhere.
2024-02-19 08:53:50 -08:00
Kendall Garner 74075fc374 [bugfix]: remove window clear cache handler in main for MacOS 2024-02-18 17:05:45 -08:00
Kendall Garner dae2f9bd0a [feat]: actually include version checks 2024-02-17 00:57:10 -08:00
jeffvli 9a43ea0e4a Attempt to fix player unresponsive on first start 2024-02-16 22:12:28 -08:00
Kendall Garner 2f105956b9 Improve grid size, language 2024-02-16 21:42:15 -08:00
Kendall Garner ce9c03b0e1 [bugfix]: Macos trusted accessibility (#512)
* [bugfix]: macos trusted accessibility

* update readme
2024-02-17 03:55:57 +00:00
Kendall Garner 1e5d446ced [bugfix]: song list play count -> genre 2024-02-16 13:53:42 -08:00
Kendall Garner b2fce071a9 [bugfix]: Check for Navidrome authentication on startup
Resolves #403.
This PR introduces a startup check for Navidrome that tries a simple API request (/songs) before loading homepage.
If the check fails, Navidrome API will fallback to trying saved password (if available).

Notes:
- It might also be worthwhile to do a periodic poll?
2024-02-16 13:37:49 -08:00
Kendall Garner 20b161ee86 [enhancement]: prreserve current song position when shuffling queue 2024-02-14 22:59:44 -08:00
darkpixlz 6e677d7454 Image Resolution Setting (#492)
* Add customizable resolution for the fullscreen player image

---------

Co-authored-by: iiPython <ben@iipython.dev>
Co-authored-by: Benjamin <iipython@proton.me>
2024-02-13 18:00:59 -08:00
Kendall Garner f796a35f5c [enhancement]: support reordering homepage (#494)
* [enhancement]: support reordering homepage

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
2024-02-13 17:56:08 -08:00
Jeff 83d5fee442 Merge pull request #484 from kgarner7/fix-structured-lyrics
[bugfix/enhancement]: Support Navidrome structured lyrics
2024-02-13 16:17:24 -08:00
Jeff eab11658bb Merge pull request #501 from kgarner7/allow-no-mpv
- OOBE default to web player
- Allow mpv to run using PATH env
- Add improved mpv error logging
- Add web player fallback on mpv error
2024-02-13 16:16:01 -08:00
jeffvli b4092c394a Set playback type to WEB by default 2024-02-13 05:50:38 -08:00
jeffvli 9b0c9ba3ac Fallback to web player if mpv fails to run 2024-02-13 02:05:59 -08:00
Kendall Garner e6b01d4e2b [bugfix]: set MPV queue when now or queue is empty 2024-02-13 00:04:28 -08:00
jeffvli fb08502e51 Add mpv path reload and clear functionality 2024-02-12 21:21:17 -08:00
jeffvli 8f4ff9286a Allow deletion on local settings keys 2024-02-12 20:50:50 -08:00
jeffvli dcd130fb6c Refactor mpv log to allow for custom toast 2024-02-12 20:50:09 -08:00
jeffvli 60105103f3 Improve mpv error logging 2024-02-12 20:11:55 -08:00
jeffvli ff4ce89bc9 Remove "disable mpv" setting and desktop requirement 2024-02-12 14:14:08 -08:00
jeffvli eb4d099804 Increase toast duration defaults 2024-02-12 14:04:19 -08:00
jeffvli b69290f9f2 Add listeners and preload for log/toast from main 2024-02-12 14:02:59 -08:00
jeffvli 69f82a9427 Add logger functions and move player functions to feature 2024-02-12 13:58:50 -08:00
jeffvli adf5fc348a Bump electron-log 2024-02-12 13:50:02 -08:00
Kendall Garner f82da2e76b [enhancement]: Support disabling MPV entirely
Supports running Feishin solely using web audio (useful for clients with problems with MPV).
Also moves save/restore queue to utils, as MPV object is now optional
2024-02-11 13:56:29 -08:00
Kendall Garner 8de21a707c Merge pull request #500 from jeffvli/dependabot/npm_and_yarn/npm_and_yarn-security-group-593a984358
Bump the npm_and_yarn group across 2 directories with 2 updates
2024-02-10 09:23:52 +00:00
dependabot[bot] 1f1916f005 Bump the npm_and_yarn group across 2 directories with 2 updates
Bumps the npm_and_yarn group with 2 updates in the /. directory: [semver](https://github.com/npm/node-semver) and [eslint-plugin-compat](https://github.com/amilajack/eslint-plugin-compat).
Bumps the npm_and_yarn group with 1 update in the /release/app directory: [semver](https://github.com/npm/node-semver).


Updates `semver` from 7.3.8 to 7.6.0
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.3.8...v7.6.0)

Updates `eslint-plugin-compat` from 4.1.4 to 4.2.0
- [Release notes](https://github.com/amilajack/eslint-plugin-compat/releases)
- [Changelog](https://github.com/amilajack/eslint-plugin-compat/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amilajack/eslint-plugin-compat/compare/v4.1.4...v4.2.0)

Updates `semver` from 6.3.0 to 6.3.1
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.3.8...v7.6.0)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
- dependency-name: eslint-plugin-compat
  dependency-type: direct:development
  dependency-group: npm_and_yarn-security-group
- dependency-name: semver
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-10 08:43:06 +00:00
Kendall Garner ae8fc6df13 [bugfix]: improve play behavior
- when adding songs to queue, only `play()` if the queue was empty
- when adding next/last to empty queue, behavior should be same as now
2024-02-10 00:38:21 -08:00
Kendall Garner 0a658e3a22 [bugfix]: default go libsecret, support changing secret store (#493)
* [bugfix]: default go libsecret, support changing secret store

* update readme and rename libsecret
2024-02-09 23:20:01 +00:00
Kendall Garner 92478b5ca5 [bugfix]: update mpris volume when set by mpris 2024-02-07 20:51:07 -08:00
Kendall Garner 49cbef729b encode uri component for authorization header 2024-02-07 20:29:29 -08:00
Kendall Garner 73c6ddd116 Merge branch 'development' of github.com:jeffvli/feishin into development 2024-02-04 10:24:24 -08:00
Kendall Garner e3553074a3 [bugfix]: use accent color for cache clearing 2024-02-04 10:23:45 -08:00
lymnyx 29df2a6215 [bugfix/enhancement] Fixes german translations being sentence-case (#491)
* Fixes german translations' case (#490).

* Smaller improvements to german translations.

* Changes to sentenceCasePostProcessor.
2024-02-04 06:47:52 -08:00
Kendall Garner aba7cb302f add navidrome version check for smart playlists 2024-02-03 22:47:57 -08:00
Kendall Garner 46cc1a635f address changes 2024-02-03 21:22:03 -08:00
Kendall Garner 6520a105d2 Merge pull request #489 from lymnyx/mostplayed-bugfix
[bugfix] Fixes route of most played songs on home page
2024-02-04 02:29:16 +00:00
lymnyx 69cb63a8b0 Fixes route of most played songs in home (#434). 2024-02-04 02:27:26 +01:00
Benjamin 1fb7290603 [bugfix] Fix global hotkeys not working on startup (#488)
* Simple one line fix for global hotkeys

* Clean up codebase via default values
2024-02-04 00:37:53 +00:00
Kendall Garner 24bf7ae31f [enhancement/localization]: sort navidrome albums by year, add more language keys 2024-02-03 15:05:33 -08:00
Benjamin 86a93866d0 [enhancement] Add a button size setting (#486)
* Add a button size setting

* Reduce setting size and add px suffix

* Looks like I don't need || inside of control-settings

* Update translation

* Bump settings version to 7
2024-02-03 21:22:26 +00:00
lymnyx 933573b57f Order artist's Top Songs by play count (#410). (#487) 2024-02-03 21:21:15 +00:00
Jeff ccb0e14e48 Merge pull request #450 from kgarner7/more-metadata
[feature]: Show album comment, Last.fm/MusicBrainz links
2024-02-02 14:56:46 -08:00
jeffvli aca6826221 Refactor artist biography to use spoiler 2024-02-02 01:39:19 -08:00
jeffvli 095edfd49f Add custom spoiler component 2024-02-02 01:38:58 -08:00
Kendall Garner 73cd647486 os lyrics 2024-02-01 23:53:10 -08:00
Kendall Garner 9e4664a54c Merge branch 'development' into fix-structured-lyrics 2024-02-01 22:13:41 -08:00
Kendall Garner efa0d9ec35 [enhancement]: larger player buttons, clearer random icon 2024-02-01 20:59:00 -08:00
Kendall Garner 9720fcc202 add server info query 2024-02-01 08:17:31 -08:00
jeffvli 7c25d12639 Enable external links by default 2024-02-01 04:14:02 -08:00
jeffvli 3daa1aef4b Normalize section spacing on album/artist detail pages 2024-02-01 04:06:56 -08:00
jeffvli 6aba41c3d9 Add spoiler UI component 2024-02-01 03:58:36 -08:00
Kendall Garner eff1cee6a3 [bugfix]: Fix repeated track scrobbling (#480)
* [bugfix]: Fix repeated track scrobbling

This commit fixes scrobbling tracks that are repeated in the queue (either due
to a REPEAT_ONE or just enqueued multiple times). There are two extra cases:

1. The current player changes. The player alternates from 1 -> 2 -> 1 whenever playback
finishes normally, so this is a sort of epoch that can be used to detect both repeat one
and track being enqueued multiple times.
2. The current index changes. The player gets set back to 1 in a variety of cases (most often
previous/next), so the check in (1) is not enough. However, the index changing will help identify
that this is a new song.

* use unique id instead
2024-02-01 04:12:39 +00:00
Kendall Garner 9995b2e774 [enhancement]: support clearing query and http cache (#475)
* [enhancement]: support clearing query and http cache

  - Adds the ability to invalidate all queries (useful for forcing refresh, and clearing lyrics which are cached forever)
  - [Desktop only] adds the ability to clear the Electron HTTP cache (e.g. cached images)

* use clearer language

* move cache settings to general
2024-01-31 06:27:56 +00:00
Kendall Garner 04a468f8c9 [bugfix]: fetch higher resolution artist images from Navidrome 2024-01-28 20:01:57 -08:00
Kendall Garner afb8510cd7 [enhancement]: increase padding for synchronizer lyric container 2024-01-28 19:42:23 -08:00
Kendall Garner 8287347f91 expose more mpris metadata 2024-01-28 14:26:02 -08:00
Kendall Garner 5cc2276781 [bugfix]: web player edge cases
Two bugs addressed in response to Safari changes:
1. Only handle `onEnded` for real streams, preventing `autoNext()` spam when the last track finishes
2. `.play()` is not necessarily a safe operation (if empty wave was started, can cause exception later). Catch this exception and discard it. This also only occurred when playing the last track
2024-01-28 01:50:51 -08:00
Kendall Garner 47ce0ed47b [bugfix]: fix repeat one with shuffle and one track 2024-01-28 00:49:17 -08:00
Kendall Garner f467a85a86 [bugfix]: use proper language key for lyrics size 2024-01-27 10:07:10 -08:00
Kendall Garner 097211954c [enhancement]: default to space for play/pause toggle 2024-01-27 08:15:13 -08:00
Kendall Garner 0cdfc64023 [bugfix]: allow Jellyfin to have empty password on edit 2024-01-26 22:54:50 -08:00
Kendall Garner bc7f4a5722 [bugfix]: Fix safari audio context (#471)
* test 3

* comments

* [bugfix]: SAFARI WHY. Use GainNode volume instead for volume control

* force vercel refresh?

* Revert "force vercel refresh?"

This reverts commit af31f38e03.

* move volume scaling calculation to setGain
2024-01-27 01:10:34 +00:00
Kendall Garner 8e7356fa7b Merge pull request #473 from kgarner7/fix-playlist-add-message
[bugfix]: Fix add to playlist success message
2024-01-27 00:51:00 +00:00
Kendall Garner bbf59a4942 [bugfix]: Fix add to playlist success message
The prior code used `form.addToPlaylist`, not `.success`. Also fixes English pluralization and
uses the correct `entity.track` as opposed to `entity.song` for other languages (I am not sure
if the en syntax could be applied to other languages, so I will just leave pluralization as-is
for now).
2024-01-25 23:58:21 -08:00
Kendall Garner 45e589fbb1 [enhancement]: use ColorInput for text rgb selection 2024-01-24 23:39:50 -08:00
Kendall Garner 527e6a76b5 [bugfix]: disable play button when queue is empty 2024-01-24 21:35:58 -08:00
Kendall Garner 362a88b6bc [bugfix]: fix race condition for opening play queue/playlist details 2024-01-24 21:15:11 -08:00
Kendall Garner 26102bd70a [bugfix]: do not change scroll position when focus changes 2024-01-24 21:05:26 -08:00
Kendall Garner 5f1d0a3b5e [bugfix]: Validate audio sample range, catch AudioContext error (#470) 2024-01-24 20:36:20 -08:00
Kendall Garner 3bca85b3a8 Merge pull request #435 from kgarner7/fix-init-timing
[bugfix]: defer restore queue until mpv exists
2024-01-23 05:18:20 +00:00
Kendall Garner 61ecd3253e Merge pull request #209 from jeffvli/fix/#202
Add frame to macOS native window bar
2024-01-23 05:07:10 +00:00
Kendall Garner 5e9ef9f23f use type, remove console 2024-01-22 18:52:14 -08:00
David Rappo c8701d1da4 Fixed incorrect label for album sort by release date (#458) 2024-01-22 10:09:15 -08:00
Kendall Garner b3a9e7ccba default theme dark 2024-01-21 22:58:10 -08:00
Kendall Garner 33972c2a83 titlebar switching 2024-01-21 22:47:59 -08:00
Kendall Garner f0f2f54e5a Merge remote-tracking branch 'upstream/development' into origin/fix/#202 2024-01-21 22:22:04 -08:00
Kendall Garner 99a188a62d Merge pull request #461 from jeffvli/dependabot/npm_and_yarn/follow-redirects-1.15.5
Bump follow-redirects from 1.15.2 to 1.15.5
2024-01-22 05:29:51 +00:00
Kendall Garner 1760e14ac5 Merge pull request #464 from jeffvli/dependabot/npm_and_yarn/release/app/electron-25.8.4
Bump electron from 25.3.0 to 25.8.4 in /release/app
2024-01-22 05:28:54 +00:00
Kendall Garner 888b5f9e90 Merge pull request #463 from jeffvli/dependabot/npm_and_yarn/postcss-8.4.33
Bump postcss from 8.4.27 to 8.4.33
2024-01-22 05:28:46 +00:00
Kendall Garner 5f099bedc2 Merge pull request #462 from jeffvli/dependabot/npm_and_yarn/babel/traverse-7.23.7
Bump @babel/traverse from 7.22.19 to 7.23.7
2024-01-22 05:28:39 +00:00
Kendall Garner b2bb2f33a9 Merge pull request #460 from jeffvli/dependabot/npm_and_yarn/zod-3.22.3
Bump zod from 3.21.4 to 3.22.3
2024-01-22 05:28:30 +00:00
dependabot[bot] f169fc7f3b Bump follow-redirects from 1.15.2 to 1.15.5
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.5.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.5)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 04:41:16 +00:00
Kendall Garner a970f967bc Merge pull request #459 from jeffvli/dependabot/npm_and_yarn/axios-1.6.0
Bump axios from 1.5.1 to 1.6.0
2024-01-22 04:40:18 +00:00
dependabot[bot] 7c0320d69a Bump electron from 25.3.0 to 25.8.4 in /release/app
Bumps [electron](https://github.com/electron/electron) from 25.3.0 to 25.8.4.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v25.3.0...v25.8.4)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 04:33:28 +00:00
dependabot[bot] 8fcfbce0d5 Bump postcss from 8.4.27 to 8.4.33
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.33.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.27...8.4.33)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 04:33:15 +00:00
dependabot[bot] 432a128b85 Bump @babel/traverse from 7.22.19 to 7.23.7
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.19 to 7.23.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.7/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 04:33:13 +00:00
dependabot[bot] 85e889a414 Bump zod from 3.21.4 to 3.22.3
Bumps [zod](https://github.com/colinhacks/zod) from 3.21.4 to 3.22.3.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Changelog](https://github.com/colinhacks/zod/blob/master/CHANGELOG.md)
- [Commits](https://github.com/colinhacks/zod/compare/v3.21.4...v3.22.3)

---
updated-dependencies:
- dependency-name: zod
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 04:33:07 +00:00
dependabot[bot] 4df5c555b0 Bump axios from 1.5.1 to 1.6.0
Bumps [axios](https://github.com/axios/axios) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.5.1...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 04:33:03 +00:00
marksdestiny 372b96a349 Improve the playback speed control (#437)
* Change playback speed selector to slider

* Improve playback speed bar

Display BPM
Improve style
Add markers

* Improve playback speed slider style
2024-01-21 20:06:46 -08:00
SlyFabi dcccccea2f Use startIndex in Jellyfin getPlaylistSongList (#449) 2024-01-21 19:46:34 -08:00
Leonardo Salgueiro 2095ff6ab9 Update scrobble-settings.tsx (#440) 2024-01-21 19:43:09 -08:00
Tarulia a0b761c9ac AppImage: Set proper categories on desktop entry (#430) 2024-01-21 19:41:23 -08:00
Kendall Garner ea67a18962 include lastfm/mbz links 2024-01-15 22:10:50 -08:00
Kendall Garner 5516daab6e enable comments, safer note 2024-01-15 20:46:06 -08:00
jeffvli 2f28cb07bc Reverse workflow changes 2024-01-14 08:32:33 -08:00
jeffvli 21cd657f0c Temporarily set releaseType to release 2024-01-14 08:18:49 -08:00
jeffvli 1ef7968834 Test electron builder action 2024-01-14 08:11:09 -08:00
jeffvli 925b1b4f68 Add release publish builds 2024-01-14 07:51:53 -08:00
jeffvli e7c665b0a0 Fix arm64 build command 2024-01-14 07:42:37 -08:00
jeffvli 245213d08a Remove unused rpm build target 2024-01-14 07:35:32 -08:00
Kendall Garner c5e08b643d [bugfix]: defer restore queue until mpv exists 2024-01-06 19:24:29 -08:00
jeffvli 960427fce8 Remove deb 2023-12-13 20:33:44 -08:00
jeffvli 118a9f3257 Separate regular and arm64 build 2023-12-13 20:32:03 -08:00
jeffvli 971bfe3823 Bump to v0.5.3 2023-12-13 19:40:51 -08:00
jeffvli 7765f14110 Add rpm and deb build targets 2023-12-13 19:39:06 -08:00
jeffvli 36b465504f Add arm64 linux build 2023-12-13 19:38:36 -08:00
jeffvli 7b639b45f7 Add new translations 2023-12-13 18:19:24 -08:00
Hosted Weblate 85d9162b12 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (519 of 519 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
2023-12-13 09:28:56 +01:00
Hosted Weblate 36670b330f Translated using Weblate (Swedish)
Currently translated at 52.6% (273 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mattias <mattiasghodsian@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/sv/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate 9c380a8241 Translated using Weblate (French)
Currently translated at 99.4% (516 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <KosmoMoustache@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate c26820ee82 Translated using Weblate (Dutch)
Currently translated at 35.8% (186 of 519 strings)

Translated using Weblate (Dutch)

Currently translated at 32.3% (168 of 519 strings)

Translated using Weblate (Dutch)

Currently translated at 15.2% (79 of 519 strings)

Added translation using Weblate (Dutch)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Idris Saklou <idrissaklou@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/nl/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate dccd6afc3d Translated using Weblate (Italian)
Currently translated at 99.0% (514 of 519 strings)

Translated using Weblate (Italian)

Currently translated at 96.5% (501 of 519 strings)

Co-authored-by: Aurora <arci@anche.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: NicKoehler <grillinicola@proton.me>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate c6a520b0d7 Translated using Weblate (Polish)
Currently translated at 99.8% (518 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mistify <fabianszafranski@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate 1f4f3a5497 Translated using Weblate (Czech)
Currently translated at 100.0% (519 of 519 strings)

Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate 58d04b3126 Translated using Weblate (German)
Currently translated at 88.8% (461 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Maik <maikguenes2003@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/de/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Hosted Weblate fcac4a5547 Translated using Weblate (Portuguese (Brazil))
Currently translated at 28.3% (147 of 519 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rafael Vieira <rafaelvieiras@pm.me>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/
Translation: feishin/Translation
2023-12-13 09:28:56 +01:00
Kendall Garner c05b474827 fix navi null date (#408) 2023-12-13 00:28:53 -08:00
mcneb10 a8814d3e8a Fix 'undefined' in window title when song has no artist name (#402) 2023-12-05 19:05:08 -08:00
Kendall Garner 3f9cdab450 convert value to number on set (#390) 2023-12-04 20:20:19 -08:00
jeffvli 6f969294b0 Set macos titlebarstyle to default 2023-08-10 01:54:15 -07:00
jeffvli 3c278d5e17 Add frame to macOS native window bar 2023-08-09 21:07:27 -07:00
328 changed files with 40055 additions and 20451 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules
Dockerfile
docker-compose.*
+1 -1
View File
@@ -2,7 +2,7 @@ root = true
[*]
indent_style = space
indent_size = 2
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
+158 -155
View File
@@ -16,7 +16,7 @@ import webpackPaths from './webpack.paths';
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
// at the dev webpack config is not accidentally run in a production environment
if (process.env.NODE_ENV === 'production') {
checkNodeEnv('development');
checkNodeEnv('development');
}
const port = process.env.PORT || 4343;
@@ -28,171 +28,174 @@ const requiredByDLLConfig = module.parent!.filename.includes('webpack.config.ren
* Warn if the DLL is not built
*/
if (!requiredByDLLConfig && !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))) {
console.log(
chalk.black.bgYellow.bold(
'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"',
),
);
execSync('npm run postinstall');
console.log(
chalk.black.bgYellow.bold(
'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"',
),
);
execSync('npm run postinstall');
}
const configuration: webpack.Configuration = {
devtool: 'inline-source-map',
devtool: 'inline-source-map',
mode: 'development',
mode: 'development',
target: ['web', 'electron-renderer'],
target: ['web', 'electron-renderer'],
entry: [
`webpack-dev-server/client?http://localhost:${port}/dist`,
'webpack/hot/only-dev-server',
path.join(webpackPaths.srcRendererPath, 'index.tsx'),
],
output: {
path: webpackPaths.distRendererPath,
publicPath: '/',
filename: 'renderer.dev.js',
library: {
type: 'umd',
},
},
module: {
rules: [
{
test: /\.s?css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
},
sourceMap: true,
importLoaders: 1,
},
},
'sass-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?css$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
entry: [
`webpack-dev-server/client?http://localhost:${port}/dist`,
'webpack/hot/only-dev-server',
path.join(webpackPaths.srcRendererPath, 'index.tsx'),
],
},
plugins: [
...(requiredByDLLConfig
? []
: [
new webpack.DllReferencePlugin({
context: webpackPaths.dllPath,
manifest: require(manifest),
sourceType: 'var',
}),
]),
new webpack.NoEmitOnErrorsPlugin(),
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*
* By default, use 'development' as NODE_ENV. This can be overriden with
* 'staging', for example, by changing the ENV variables in the npm scripts
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
}),
new webpack.LoaderOptionsPlugin({
debug: true,
}),
new ReactRefreshWebpackPlugin(),
new HtmlWebpackPlugin({
filename: path.join('index.html'),
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
}),
],
node: {
__dirname: false,
__filename: false,
},
devServer: {
port,
compress: true,
hot: true,
headers: { 'Access-Control-Allow-Origin': '*' },
static: {
publicPath: '/',
output: {
path: webpackPaths.distRendererPath,
publicPath: '/',
filename: 'renderer.dev.js',
library: {
type: 'umd',
},
},
historyApiFallback: {
verbose: true,
},
setupMiddlewares(middlewares) {
console.log('Starting preload.js builder...');
const preloadProcess = spawn('npm', ['run', 'start:preload'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting remote.js builder...');
const remoteProcess = spawn('npm', ['run', 'start:remote'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting Main Process...');
spawn('npm', ['run', 'start:main'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => {
preloadProcess.kill();
remoteProcess.kill();
process.exit(code!);
})
.on('error', (spawnError) => console.error(spawnError));
return middlewares;
module: {
rules: [
{
test: /\.s?css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
},
sourceMap: true,
importLoaders: 1,
},
},
'sass-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?css$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
plugins: [
...(requiredByDLLConfig
? []
: [
new webpack.DllReferencePlugin({
context: webpackPaths.dllPath,
manifest: require(manifest),
sourceType: 'var',
}),
]),
new webpack.NoEmitOnErrorsPlugin(),
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*
* By default, use 'development' as NODE_ENV. This can be overriden with
* 'staging', for example, by changing the ENV variables in the npm scripts
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
}),
new webpack.LoaderOptionsPlugin({
debug: true,
}),
new ReactRefreshWebpackPlugin(),
new HtmlWebpackPlugin({
filename: path.join('index.html'),
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
templateParameters: {
web: false,
},
}),
],
node: {
__dirname: false,
__filename: false,
},
devServer: {
port,
compress: true,
hot: true,
headers: { 'Access-Control-Allow-Origin': '*' },
static: {
publicPath: '/',
},
historyApiFallback: {
verbose: true,
},
setupMiddlewares(middlewares) {
console.log('Starting preload.js builder...');
const preloadProcess = spawn('npm', ['run', 'start:preload'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting remote.js builder...');
const remoteProcess = spawn('npm', ['run', 'start:remote'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting Main Process...');
spawn('npm', ['run', 'start:main'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => {
preloadProcess.kill();
remoteProcess.kill();
process.exit(code!);
})
.on('error', (spawnError) => console.error(spawnError));
return middlewares;
},
},
},
};
export default merge(baseConfig, configuration);
+100 -97
View File
@@ -21,114 +21,117 @@ checkNodeEnv('production');
deleteSourceMaps();
const devtoolsConfig =
process.env.DEBUG_PROD === 'true'
? {
devtool: 'source-map',
}
: {};
process.env.DEBUG_PROD === 'true'
? {
devtool: 'source-map',
}
: {};
const configuration: webpack.Configuration = {
...devtoolsConfig,
...devtoolsConfig,
mode: 'production',
mode: 'production',
target: ['web', 'electron-renderer'],
target: ['web', 'electron-renderer'],
entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
output: {
path: webpackPaths.distRendererPath,
publicPath: './',
filename: 'renderer.js',
library: {
type: 'umd',
output: {
path: webpackPaths.distRendererPath,
publicPath: './',
filename: 'renderer.js',
library: {
type: 'umd',
},
},
},
module: {
rules: [
{
test: /\.s?(a|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
},
sourceMap: true,
importLoaders: 1,
module: {
rules: [
{
test: /\.s?(a|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
},
sourceMap: true,
importLoaders: 1,
},
},
'sass-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?(a|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
},
'sass-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?(a|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
}),
new CssMinimizerPlugin(),
],
},
plugins: [
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
DEBUG_PROD: false,
}),
new MiniCssExtractPlugin({
filename: 'style.css',
}),
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production',
templateParameters: {
web: false,
},
}),
],
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
}),
new CssMinimizerPlugin(),
],
},
plugins: [
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
DEBUG_PROD: false,
}),
new MiniCssExtractPlugin({
filename: 'style.css',
}),
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production',
}),
],
};
export default merge(baseConfig, configuration);
@@ -116,6 +116,9 @@ const configuration: webpack.Configuration = {
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
templateParameters: {
web: false, // with hot reload, we don't have NGINX injecting variables
},
}),
],
+3
View File
@@ -128,6 +128,9 @@ const configuration: webpack.Configuration = {
},
isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production',
templateParameters: {
web: true,
},
}),
],
};
-45
View File
@@ -1,45 +0,0 @@
---
name: Bug report
about: You're having technical issues. 🐞
labels: 'bug'
---
## Expected Behavior
<!--- What should have happened? -->
## Current Behavior
<!--- What went wrong? -->
<!-- Add screenshots to help explain your problem -->
<!-- (Open the browser dev tools in the menu or using CTRL + SHIFT + I) -->
## Steps to Reproduce
<!-- Add relevant code and/or a live example -->
<!-- Add stack traces -->
1.
2.
3.
4.
## Possible Solution (Not obligatory)
<!--- Suggest a reason for the bug or how to fix it. -->
## Context
<!--- How has this issue affected you? What are you trying to accomplish? -->
## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
- Application version (e.g. v0.1.0) :
- Operating System and version (e.g. Windows 10) :
- Server and version (e.g. Navidrome v0.48.0) :
- Node version (if developing locally) :
-9
View File
@@ -1,9 +0,0 @@
---
name: Question
about: Ask a question.❓
labels: 'question'
---
<!-- Question issues will be closed. -->
<!-- Ask questions in the discussions tab: Please use discussions https://github.com/jeffvli/feishin/discussions -->
<!-- Or join the Discord/Matrix servers: https://discord.gg/FVKpcMDy5f https://matrix.to/#/#sonixd:matrix.org -->
@@ -1,11 +0,0 @@
---
name: Feature request
about: Request a feature to be added to Feishin 🎉
labels: 'enhancement'
---
## What do you want to be added?
## Additional context
<!-- Is this a server-specific feature? (e.g. Jellyfin only). -->
+63
View File
@@ -0,0 +1,63 @@
name: Bug report
description: You're having technical issues. 🐞
labels: ['bug']
body:
- type: textarea
attributes:
label: Expected Behavior
description: What should have happened?
validations:
required: true
- type: textarea
attributes:
label: Current Behavior
description: What went wrong? Add screenshots to help explain your problem. (Open the browser dev tools in the menu or using CTRL + SHIFT + I)
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
placeholder: |
<!-- Add relevant code and/or a live example -->
<!-- Add stack traces -->
1.
2.
3.
4.
validations:
required: true
- type: textarea
attributes:
label: Possible Solution
description: Suggest a reason for the bug or how to fix it.
validations:
required: false
- type: textarea
attributes:
label: Context
description: How has this issue affected you? What are you trying to accomplish?
validations:
required: false
- type: input
attributes:
label: Application version
placeholder: (e.g. v0.1.0)
validations:
required: true
- type: input
attributes:
label: Operating System and version
placeholder: (e.g. Windows 11 desktop, Webapp in Firefox)
validations:
required: true
- type: input
attributes:
label: Server and Version
placeholder: (e.g. Navidrome v0.48.0)
validations:
required: true
- type: input
attributes:
label: Node Version (if developing locally)
validations:
required: false
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Question
url: https://github.com/jeffvli/feishin/discussions
about: Please ask and answer questions here.
@@ -0,0 +1,20 @@
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
attributes:
label: What do you want to be added?
validations:
required: true
- type: textarea
attributes:
label: Additional context
validations:
required: false
- type: checkboxes
attributes:
label: Is this a server-specific feature? (e.g. Jellyfin only)
options:
- label: 'Yes'
required: false
+15 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Install Node and NPM
uses: actions/setup-node@v1
with:
node-version: 16
node-version: 18
cache: npm
- name: Install dependencies
@@ -37,3 +37,17 @@ jobs:
npm run build
npm exec electron-builder -- --publish always --linux
on_retry_command: npm cache clean --force
- name: Publish releases (arm64)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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 exec electron-builder -- --publish always --arm64
on_retry_command: npm cache clean --force
+3 -3
View File
@@ -1,4 +1,4 @@
name: Publish Windows and macOS (Manual)
name: Publish macOS (Manual)
on: workflow_dispatch
@@ -17,7 +17,7 @@ jobs:
- name: Install Node and NPM
uses: actions/setup-node@v1
with:
node-version: 16
node-version: 18
cache: npm
- name: Install dependencies
@@ -35,5 +35,5 @@ jobs:
command: |
npm run postinstall
npm run build
npm exec electron-builder -- --publish always --win --mac
npm exec electron-builder -- --publish always --mac
on_retry_command: npm cache clean --force
+53 -18
View File
@@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
os: [macos-latest]
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Checkout git repo
@@ -20,41 +20,76 @@ jobs:
- name: Install Node and NPM
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: npm
- name: Install dependencies
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
+39
View File
@@ -0,0 +1,39 @@
name: Publish Windows (Manual)
on: workflow_dispatch
jobs:
publish:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest]
steps:
- name: Checkout git repo
uses: actions/checkout@v1
- name: Install Node and NPM
uses: actions/setup-node@v1
with:
node-version: 18
cache: npm
- name: Install dependencies
run: |
npm install --legacy-peer-deps
- name: Publish releases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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 exec electron-builder -- --publish always --win
on_retry_command: npm cache clean --force
+4 -4
View File
@@ -11,10 +11,10 @@
],
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true,
"source.organizeImports": false,
"source.formatDocument": true
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never",
"source.formatDocument": "explicit"
},
"css.validate": true,
"less.validate": false,
+5 -1
View File
@@ -1,16 +1,20 @@
# --- Builder stage
FROM node:18-alpine as builder
WORKDIR /app
COPY . /app
#Copy package.json first to cache node_modules
COPY package.json package-lock.json .
# Scripts include electron-specific dependencies, which we don't need
RUN npm install --legacy-peer-deps --ignore-scripts
#Copy code and build with cached modules
COPY . .
RUN npm run build:web
# --- Production stage
FROM nginx:alpine-slim
COPY --chown=nginx:nginx --from=builder /app/release/app/dist/web /usr/share/nginx/html
COPY ./settings.js.template /etc/nginx/templates/settings.js.template
COPY ng.conf.template /etc/nginx/templates/default.conf.template
ENV PUBLIC_PATH="/"
+61 -2
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,8 +59,12 @@ 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
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.
For media keys to work, you will be prompted to allow Feishin to be a Trusted Accessibility Client. After allowing, you will need to restart Feishin for the privacy settings to take effect.
### Web and Docker
Visit [https://feishin.vercel.app](https://feishin.vercel.app) to use the hosted web version of Feishin. The web client only supports the web player backend.
@@ -66,6 +80,29 @@ docker build -t feishin .
docker run --name feishin -p 9180:9180 feishin
```
#### Docker Compose
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
1. Upon startup you will be greeted with a prompt to select the path to your MPV binary. If you do not have MPV installed, you can download it [here](https://mpv.io/installation/) or install it using any package manager supported by your OS. After inputting the path, restart the app.
@@ -73,9 +110,12 @@ docker run --name feishin -p 9180:9180 feishin
2. After restarting the app, you will be prompted to select a server. Click the `Open menu` button and select `Manage servers`. Click the `Add server` button in the popup and fill out all applicable details. You will need to enter the full URL to your server, including the protocol and port if applicable (e.g. `https://navidrome.my-server.com` or `http://192.168.0.1:4533`).
- **Navidrome** - For the best experience, select "Save password" when creating the server and configure the `SessionTimeout` setting in your Navidrome config to a larger value (e.g. 72h).
- **Linux users** - The default password store uses `libsecret`. `kwallet4/5/6` are also supported, but must be explicitly set in Settings > Window > Passwords/secret score.
3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`.
4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set.
## FAQ
### MPV is either not working or is rapidly switching between pause/play states
@@ -88,8 +128,27 @@ 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
This happens when you have user (unprivileged) namespaces disabled (`sysctl kernel.unprivileged_userns_clone` returns 0). You can fix this by either enabling unprivileged namespaces, or by making the `chrome-sandbox` Setuid.
```bash
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
+13
View File
@@ -0,0 +1,13 @@
version: '3.5'
services:
feishin:
container_name: feishin
image: ghcr.io/jeffvli/feishin:latest
restart: unless-stopped
ports:
- 9180:9180
environment:
- SERVER_NAME=jellyfin # pre defined server name
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
- SERVER_TYPE=jellyfin # navidrome also works
- SERVER_URL= # http://address:port
+8
View File
@@ -16,4 +16,12 @@ server {
alias /usr/share/nginx/html/;
try_files $uri $uri/ /index.html =404;
}
location ${PUBLIC_PATH}settings.js {
alias /etc/nginx/conf.d/settings.js;
}
location ${PUBLIC_PATH}/settings.js {
alias /etc/nginx/conf.d/settings.js;
}
}
+16663 -13154
View File
File diff suppressed because it is too large Load Diff
+48 -27
View File
@@ -2,24 +2,27 @@
"name": "feishin",
"productName": "Feishin",
"description": "Feishin music server",
"version": "0.5.2",
"version": "0.12.6",
"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",
"build:remote": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.prod.ts",
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
"build:web": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.web.prod.ts",
"build:docker": "npm run build:web && docker build -t jeffvli/feishin .",
"build:docker": "docker build -t jeffvli/feishin .",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
"lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"",
"lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lint:styles": "npx stylelint **/*.tsx --fix",
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
"package:pr": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win --mac --linux",
"package:dev": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --dir",
"postinstall": "ts-node .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": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
"start:main": "cross-env NODE_ENV=development electron -r ts-node/register/transpile-only ./src/main/main.ts",
"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",
"start:main": "cross-env NODE_ENV=development NODE_OPTIONS=\"--import tsx\" electron -r ts-node/register/transpile-only ./src/main/main.ts",
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
"start:remote": "cross-env NODE_ENV=developemnt TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.dev.ts",
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
@@ -56,7 +59,7 @@
"package.json"
],
"afterSign": ".erb/scripts/notarize.js",
"electronVersion": "27.1.0",
"electronVersion": "36.1.0",
"mac": {
"target": {
"target": "default",
@@ -132,7 +135,7 @@
"tar.xz"
],
"icon": "assets/icons/icon.png",
"category": "Development"
"category": "AudioVideo;Audio;Player"
},
"directories": {
"app": "release/app",
@@ -199,12 +202,13 @@
]
},
"devDependencies": {
"@electron/rebuild": "^3.2.10",
"@electron/rebuild": "^3.6.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
"@stylelint/postcss-css-in-js": "^0.38.0",
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.0.0",
"@types/dompurify": "^3.0.5",
"@types/electron-localshortcut": "^3.1.0",
"@types/jest": "^27.4.1",
"@types/lodash": "^4.14.188",
@@ -230,8 +234,8 @@
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"detect-port": "^1.3.0",
"electron": "^27.1.0",
"electron-builder": "^24.9.0",
"electron": "^36.1.0",
"electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0",
"electron-notarize": "^1.2.1",
"electronmon": "^2.0.2",
@@ -240,7 +244,7 @@
"eslint-config-erb": "^4.0.3",
"eslint-import-resolver-typescript": "^2.7.1",
"eslint-import-resolver-webpack": "^0.13.2",
"eslint-plugin-compat": "^4.0.2",
"eslint-plugin-compat": "^4.2.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^26.1.3",
"eslint-plugin-jsx-a11y": "^6.5.1",
@@ -252,7 +256,7 @@
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"husky": "^7.0.4",
"i18next-parser": "^6.6.0",
"i18next-parser": "^9.0.2",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.5.1",
"lint-staged": "^12.3.7",
@@ -260,7 +264,7 @@
"postcss-scss": "^4.0.4",
"postcss-styled-syntax": "^0.5.0",
"postcss-syntax": "^0.36.2",
"prettier": "^2.6.2",
"prettier": "^3.3.3",
"react-refresh": "^0.12.0",
"react-refresh-typescript": "^2.0.4",
"react-test-renderer": "^18.0.0",
@@ -277,12 +281,13 @@
"terser-webpack-plugin": "^5.3.1",
"ts-jest": "^27.1.4",
"ts-loader": "^9.2.8",
"ts-node": "^10.7.0",
"ts-node": "^10.9.2",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"tsx": "^4.16.2",
"typescript": "^5.2.2",
"typescript-plugin-styled-components": "^3.0.0",
"url-loader": "^4.1.1",
"webpack": "^5.71.0",
"webpack": "^5.94.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.0",
@@ -307,18 +312,21 @@
"@tanstack/react-query-persist-client": "^4.32.1",
"@ts-rest/core": "^3.23.0",
"@xhayper/discord-rpc": "^1.0.24",
"axios": "^1.4.0",
"audiomotion-analyzer": "^4.5.0",
"auto-text-size": "^0.2.3",
"axios": "^1.6.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"dayjs": "^1.11.6",
"dompurify": "^3.1.6",
"electron-debug": "^3.2.0",
"electron-localshortcut": "^3.2.1",
"electron-log": "^4.4.6",
"electron-log": "^5.1.1",
"electron-store": "^8.1.0",
"electron-updater": "^4.6.5",
"electron-updater": "^6.3.1",
"fast-average-color": "^9.3.0",
"format-duration": "^2.0.0",
"framer-motion": "^10.13.0",
"framer-motion": "^11.0.0",
"fuse.js": "^6.6.2",
"history": "^5.3.0",
"i18next": "^21.10.0",
@@ -330,7 +338,7 @@
"memoize-one": "^6.0.0",
"nanoid": "^3.3.3",
"net": "^1.0.2",
"node-mpv": "github:jeffvli/Node-MPV",
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
"overlayscrollbars": "^2.2.1",
"overlayscrollbars-react": "^0.5.1",
"react": "^18.2.0",
@@ -345,17 +353,30 @@
"react-virtualized-auto-sizer": "^1.0.17",
"react-window": "^1.8.9",
"react-window-infinite-loader": "^1.0.9",
"semver": "^7.5.4",
"styled-components": "^6.0.8",
"swiper": "^9.3.1",
"zod": "^3.21.4",
"zod": "^3.22.3",
"zustand": "^4.3.9"
},
"resolutions": {
"styled-components": "^6"
"styled-components": "^6",
"entities": "2.2.0"
},
"overrides": {
"entities": "2.2.0"
},
"devEngines": {
"node": ">=14.x",
"npm": ">=7.x"
"runtime": {
"name": "node",
"version": ">=18.x",
"onFail": "error"
},
"packageManager": {
"name": "npm",
"version": ">=7.x",
"onFail": "error"
}
},
"browserslist": [],
"electronmon": {
+59 -39
View File
@@ -1,21 +1,21 @@
{
"name": "feishin",
"version": "0.5.2",
"version": "0.12.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "feishin",
"version": "0.5.2",
"version": "0.12.6",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {
"cheerio": "^1.0.0-rc.12",
"mpris-service": "^2.1.2",
"ws": "^8.13.0"
"ws": "^8.18.0"
},
"devDependencies": {
"electron": "25.3.0"
"electron": "36.1.0"
}
},
"node_modules/@electron/get": {
@@ -99,10 +99,14 @@
}
},
"node_modules/@types/node": {
"version": "18.16.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz",
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
"dev": true
"version": "22.15.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.12.tgz",
"integrity": "sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/responselike": {
"version": "1.0.0",
@@ -453,14 +457,15 @@
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
},
"node_modules/electron": {
"version": "25.3.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.3.0.tgz",
"integrity": "sha512-cyqotxN+AroP5h2IxUsJsmehYwP5LrFAOO7O7k9tILME3Sa1/POAg3shrhx4XEnaAMyMqMLxzGvkzCVxzEErnA==",
"version": "36.1.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-36.1.0.tgz",
"integrity": "sha512-gnp3BnbKdGsVc7cm1qlEaZc8pJsR08mIs8H/yTo8gHEtFkGGJbDTVZOYNAfbQlL0aXh+ozv+CnyiNeDNkT1Upg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^18.11.18",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
},
"bin": {
@@ -647,9 +652,9 @@
}
},
"node_modules/global-agent/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"optional": true,
"dependencies": {
@@ -1166,9 +1171,9 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
@@ -1270,6 +1275,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -1286,9 +1298,9 @@
"dev": true
},
"node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"engines": {
"node": ">=10.0.0"
},
@@ -1408,10 +1420,13 @@
}
},
"@types/node": {
"version": "18.16.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz",
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
"dev": true
"version": "22.15.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.12.tgz",
"integrity": "sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==",
"dev": true,
"requires": {
"undici-types": "~6.21.0"
}
},
"@types/responselike": {
"version": "1.0.0",
@@ -1672,13 +1687,13 @@
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
},
"electron": {
"version": "25.3.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.3.0.tgz",
"integrity": "sha512-cyqotxN+AroP5h2IxUsJsmehYwP5LrFAOO7O7k9tILME3Sa1/POAg3shrhx4XEnaAMyMqMLxzGvkzCVxzEErnA==",
"version": "36.1.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-36.1.0.tgz",
"integrity": "sha512-gnp3BnbKdGsVc7cm1qlEaZc8pJsR08mIs8H/yTo8gHEtFkGGJbDTVZOYNAfbQlL0aXh+ozv+CnyiNeDNkT1Upg==",
"dev": true,
"requires": {
"@electron/get": "^2.0.0",
"@types/node": "^18.11.18",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
}
},
@@ -1818,9 +1833,9 @@
},
"dependencies": {
"semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"optional": true,
"requires": {
@@ -2198,9 +2213,9 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true
},
"semver-compare": {
@@ -2278,6 +2293,12 @@
"dev": true,
"optional": true
},
"undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -2291,10 +2312,9 @@
"dev": true
},
"ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"requires": {}
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
},
"xml2js": {
"version": "0.4.23",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "feishin",
"version": "0.5.2",
"version": "0.12.6",
"description": "",
"main": "./dist/main/main.js",
"author": {
@@ -15,10 +15,10 @@
"dependencies": {
"cheerio": "^1.0.0-rc.12",
"mpris-service": "^2.1.2",
"ws": "^8.13.0"
"ws": "^8.18.0"
},
"devDependencies": {
"electron": "25.3.0"
"electron": "36.1.0"
},
"license": "GPL-3.0"
}
+1
View File
@@ -0,0 +1 @@
"use strict";window.SERVER_URL="${SERVER_URL}";window.SERVER_NAME="${SERVER_NAME}";window.SERVER_TYPE="${SERVER_TYPE}";window.SERVER_LOCK=${SERVER_LOCK};
+64 -5
View File
@@ -1,4 +1,4 @@
import { PostProcessorModule } from 'i18next';
import { PostProcessorModule, TOptions, StringMap } from 'i18next';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './locales/en.json';
@@ -14,6 +14,15 @@ import ptBr from './locales/pt-BR.json';
import sr from './locales/sr.json';
import sv from './locales/sv.json';
import cs from './locales/cs.json';
import nbNO from './locales/nb-NO.json';
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 },
@@ -22,13 +31,22 @@ const resources = {
it: { translation: it },
ru: { translation: ru },
'pt-BR': { translation: ptBr },
fa: { translation: fa },
fr: { translation: fr },
ja: { translation: ja },
ko: { translation: ko },
pl: { translation: pl },
'zh-Hans': { translation: zhHans },
'zh-Hant': { translation: zhHant },
sr: { translation: sr },
sv: { translation: sv },
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 = [
@@ -52,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',
@@ -61,8 +91,20 @@ export const languages = [
value: 'ja',
},
{
label: 'Русский',
value: 'ru',
label: '한국어',
value: 'ko',
},
{
label: 'Nederlands',
value: 'nl',
},
{
label: 'Norsk (Bokmål)',
value: 'nb-NO',
},
{
label: 'فارسی',
value: 'fa',
},
{
label: 'Português (Brasil)',
@@ -72,6 +114,10 @@ export const languages = [
label: 'Polski',
value: 'pl',
},
{
label: 'Русский',
value: 'ru',
},
{
label: 'Srpski',
value: 'sr',
@@ -80,10 +126,18 @@ export const languages = [
label: 'Svenska',
value: 'sv',
},
{
label: 'Tamil',
value: 'ta',
},
{
label: '简体中文',
value: 'zh-Hans',
},
{
label: '繁體中文',
value: 'zh-Hant',
},
];
const lowerCasePostProcessor: PostProcessorModule = {
@@ -112,16 +166,21 @@ const titleCasePostProcessor: PostProcessorModule = {
},
};
const ignoreSentenceCaseLanguages = ['de'];
const sentenceCasePostProcessor: PostProcessorModule = {
type: 'postProcessor',
name: 'sentenceCase',
process: (value: string) => {
process: (value: string, _key: string, _options: TOptions<StringMap>, translator: any) => {
const sentences = value.split('. ');
return sentences
.map((sentence) => {
return (
sentence.charAt(0).toLocaleUpperCase() + sentence.slice(1).toLocaleLowerCase()
sentence.charAt(0).toLocaleUpperCase() +
(!ignoreSentenceCaseLanguages.includes(translator.language)
? sentence.slice(1).toLocaleLowerCase()
: sentence.slice(1))
);
})
.join('. ');
+3 -3
View File
@@ -5,7 +5,9 @@ module.exports = {
createOldCatalogs: true,
customValueTemplate: null,
defaultNamespace: 'translation',
defaultValue: '',
defaultValue: function (locale, namespace, key, value) {
return key;
},
failOnUpdate: false,
failOnWarnings: false,
i18nextOptions: null,
@@ -37,8 +39,6 @@ module.exports = {
output: 'src/renderer/i18n/locales/$LOCALE.json',
pluralSeparator: '_',
resetDefaultValueLocale: 'en',
skipDefaultValues: false,
sort: true,
useKeysAsDefaultValue: true,
verbose: false,
};
+185 -30
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ší",
@@ -28,7 +28,9 @@
"shuffle_off": "náhodně zakázáno",
"addLast": "přidat poslední",
"mute": "ztlumit",
"skip_forward": "přeskočit dopředu"
"skip_forward": "přeskočit dopředu",
"playSimilarSongs": "přehrát podobné skladby",
"viewQueue": "zobrazit frontu"
},
"setting": {
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
@@ -41,7 +43,6 @@
"hotkey_playbackPause": "pozastavení",
"replayGainFallback": "fallback {{ReplayGain}}",
"sidebarCollapsedNavigation_description": "zobrazit nebo skrýt navigaci ve sbaleném postranním panelu",
"mpvExecutablePath_help": "jedna na řádek",
"hotkey_volumeUp": "zvýšení hlasitosti",
"skipDuration": "doba k přeskočení",
"discordIdleStatus_description": "při povolení bude upraven stav když je přehrávač nečinný",
@@ -53,7 +54,7 @@
"skipDuration_description": "nastavení doby k přeskočení při použití tlačítek k přeskočení na liště přehrávače",
"enableRemote_description": "povolí vzdálený ovládací server pro umožnění ostatním zařízením ovládat aplikaci",
"fontType_optionSystem": "systémové písmo",
"mpvExecutablePath_description": "nastavení cesty ke spustitelnému souboru mpv",
"mpvExecutablePath_description": "nastavení cesty ke spustitelnému souboru mpv. pokud je prázdné, bude použita výchozí cesta",
"replayGainClipping_description": "Zabránění clippingu způsobenému funkcí {{ReplayGain}} automatickým snížením zesílení",
"replayGainPreamp": "před-zesílení {{ReplayGain}} (dB)",
"hotkey_favoriteCurrentSong": "oblíbit $t(common.currentSong)",
@@ -61,7 +62,7 @@
"crossfadeStyle": "způsob prolnutí",
"sidePlayQueueStyle_optionAttached": "připojené",
"sidebarConfiguration": "nastavení postranního panelu",
"sampleRate_description": "vyberte výstupní vzorkovací frekvenci k použití, když je vybraná vzorkovací frekvence jiná, než ta u aktuálního média",
"sampleRate_description": "vyberte výstupní vzorkovací frekvenci k použití, když je vybraná vzorkovací frekvence jiná, než ta u aktuálního média. hodnota nižší než 8000 použije výchozí frekvenci",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainClipping": "clipping {{ReplayGain}}",
"hotkey_zoomIn": "přiblížení",
@@ -191,7 +192,72 @@
"discordRichPresence": "{{discord}} rich presence",
"font_description": "nastavení písma použitého v aplikaci",
"savePlayQueue_description": "uložit frontu přehrávání, když je aplikace zavřena a obnovit ji při otevření aplikace",
"useSystemTheme": "použít systémový motiv"
"useSystemTheme": "použít systémový motiv",
"buttonSize": "velikost tlačítek lišty přehrávače",
"buttonSize_description": "velikost tlačítek na liště přehrávače",
"clearCache": "vymazat mezipaměť prohlížeče",
"clearCache_description": "„tvrdé pročištění“ aplikace feishin. kromě mezipaměti aplikace feishin vymaže i mezipaměť prohlížeče (uložené obrázky a další zdroje). přihlašovací údaje k serveru a nastavení nebudou ovlivněny",
"clearQueryCache": "vymazat mezipaměť aplikace feishin",
"clearQueryCache_description": "„lehké pročištění“ aplikace feishin. tímto obnovíte seznamy skladeb, metadata skladeb a resetujete uložené texty. nastavení, přihlašovací údaje k serveru a obrázky v mezipaměti nebudou ovlivněny",
"startMinimized": "spustit minimalizované",
"homeConfiguration_description": "nastavte, které položky a v jakém pořadí mají být zobrazeny na domovské stránce",
"passwordStore": "ukládání hesel / tajných klíčů",
"mpvExtraParameters_help": "jeden na řádek",
"homeConfiguration": "nastavení domovské stránky",
"playerAlbumArtResolution_description": "rozlišení náhledu obalu alba ve velkém přehrávači. větší hodnota znamená kvalitnější obrázek, ale může se déle načítat. výchozí hodnota je 0, což znamená automatické rozlišení",
"playerAlbumArtResolution": "rozlišení obalu alba v přehrávači",
"genreBehavior": "výchozí chování stránky žánrů",
"externalLinks_description": "zapne zobrazování externích odkazů (Last.fm, MusicBrainz) na stránce umělce/alba",
"genreBehavior_description": "určuje, zda kliknutí na žánr otevře seznam skladeb nebo alb",
"clearCacheSuccess": "mezipaměť úspěšně vymazána",
"externalLinks": "zobrazit externí odkazy",
"startMinimized_description": "spustit aplikaci do systémové lišty",
"passwordStore_description": "který způsob ukládání hesel / tajných klíčů použít. změňte tuto možnost, pokud máte problémy s ukládáním hesel.",
"homeFeature": "carousel doporučení na domovské stránce",
"homeFeature_description": "ovládá, zda se má zobrazovat velký carousel s doporučenými alby na domovské stránce",
"imageAspectRatio": "použít nativní poměr stran obalů alb",
"imageAspectRatio_description": "pokud je povoleno, budou obaly alb zobrazeny s jejich nativním poměrem stran. u obalů, které nemají poměr 1:1, bude zbývající místo prázdné",
"doubleClickBehavior": "dvojitým kliknutím zařadit všechny vyhledané skladby do fronty",
"doubleClickBehavior_description": "pokud je zapnuto, budou všechny odpovídající skladby ve vyhledávání zařazeny do fronty. v opačném případě bude zařazena pouze ta, na kterou kliknete",
"volumeWidth": "šířka posuvníku hlasitosti",
"volumeWidth_description": "horizontální velikost posuvníku hlasitosti",
"discordListening": "zobrazit stav jako „Poslouchá“",
"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": "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",
"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",
"transcode": "povolit překódování",
"transcode_description": "zapnout překódování do různých formátů",
"transcodeFormat_description": "vybere formát k překódování. pokud chcete nechat rozhodnout server, ponechte prázdné",
"transcodeFormat": "formát k překódování",
"transcodeBitrate": "datový tok k překódování",
"transcodeBitrate_description": "vybere datový tok k překódování. 0 znamená, že necháte server vybrat",
"albumBackground": "obrázek alba na pozadí",
"albumBackground_description": "přidá obrázek alba na pozadí pro stránky alba obsahující obrázky alba",
"albumBackgroundBlur": "velikost rozostření obrázku alba na pozadí",
"albumBackgroundBlur_description": "upraví množství rozostření použité na obrázek alba na pozadí",
"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í",
"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)",
@@ -210,7 +276,12 @@
"moveToBottom": "přesunout dolů",
"setRating": "nastavit hodnocení",
"toggleSmartPlaylistEditor": "přepnout editor $t(entity.smartPlaylist)",
"removeFromFavorites": "odebrat z $t(entity.favorite_other)"
"removeFromFavorites": "odebrat z $t(entity.favorite_other)",
"openIn": {
"lastfm": "Otevřít v Last.fm",
"musicbrainz": "Otevřít v MusicBrainz"
},
"moveToNext": "přesunout na další"
},
"common": {
"backward": "zpátky",
@@ -293,7 +364,18 @@
"random": "náhodně",
"size": "velikost",
"biography": "biografie",
"note": "poznámka"
"note": "poznámka",
"albumGain": "zisk (gain) alba",
"albumPeak": "vrchol alba",
"close": "zavřít",
"mbid": "ID MusicBrainz",
"trackGain": "zisk (gain) skladby",
"reload": "znovu načíst",
"share": "sdílet",
"codec": "kodek",
"trackPeak": "vrchol skladby",
"preview": "náhled",
"translation": "překlad"
},
"table": {
"config": {
@@ -307,7 +389,10 @@
"gap": "$t(common.gap)",
"tableColumns": "sloupce tabulky",
"autoFitColumns": "automaticky přizpůsobit sloupce",
"size": "$t(common.size)"
"size": "$t(common.size)",
"itemGap": "mezera mezi položkami (px)",
"itemSize": "velikost položek (px)",
"followCurrentSong": "následovat aktuální skladbu"
},
"label": {
"releaseDate": "datum vydání",
@@ -335,7 +420,9 @@
"discNumber": "číslo disku",
"favorite": "$t(common.favorite)",
"year": "$t(common.year)",
"albumArtist": "$t(entity.albumArtist_one)"
"albumArtist": "$t(entity.albumArtist_one)",
"codec": "$t(common.codec)",
"songCount": "$t(entity.track_other)"
}
},
"column": {
@@ -360,7 +447,9 @@
"albumArtist": "umělec alba",
"path": "cesta",
"discNumber": "disk",
"channels": "$t(common.channel_other)"
"channels": "$t(common.channel_other)",
"size": "$t(common.size)",
"codec": "$t(common.codec)"
}
},
"error": {
@@ -382,7 +471,10 @@
"mpvRequired": "vyžadován přehrávač MPV",
"audioDeviceFetchError": "při pokusu o přístup ke zvukovým zařízením se vyskytla chyba",
"invalidServer": "neplatný server",
"loginRateError": "příliš mnoho pokusů o přihlášení, zkuste to znovu za pár vteřin"
"loginRateError": "příliš mnoho pokusů o přihlášení, zkuste to znovu za pár vteřin",
"badAlbum": "tuto stránku vidíte, protože tato skladba není součástí alba. tento problém může nastat, pokud máte skladbu na nejvyšší úrovni vaší složky s hudbou. jellyfin seskupuje skladby pouze, pokud se nacházejí ve složce.",
"networkError": "vyskytla se chyba sítě",
"openError": "nepodařilo se otevřít soubor"
},
"filter": {
"mostPlayed": "nejvíce přehráváno",
@@ -440,7 +532,8 @@
"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) sdíleny"
},
"fullscreenPlayer": {
"config": {
@@ -454,11 +547,16 @@
"unsynchronized": "nesynchronizováno",
"lyricAlignment": "zarovnání textů",
"useImageAspectRatio": "použít poměr stran obrázku",
"lyricGap": "mezera textů"
"lyricGap": "mezera textů",
"dynamicImageBlur": "velikost rozostření obrázku",
"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",
@@ -488,7 +586,13 @@
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"numberSelected": "vybráno {{count}}",
"removeFromQueue": "$t(action.removeFromQueue)"
"removeFromQueue": "$t(action.removeFromQueue)",
"showDetails": "získat informace",
"shareItem": "sdílet položku",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "stáhnout",
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"home": {
"mostPlayed": "nejpřehrávanější",
@@ -499,22 +603,28 @@
},
"albumDetail": {
"moreFromArtist": "více od tohoto umělce",
"moreFromGeneric": "více od {{item}}"
"moreFromGeneric": "více od {{item}}",
"released": "vydáno"
},
"setting": {
"playbackTab": "přehrávání",
"generalTab": "obecné",
"hotkeysTab": "klávesové zkratky",
"windowTab": "okno"
"windowTab": "okno",
"advanced": "pokročilé"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
"title": "$t(entity.genre_other)",
"showTracks": "zobrazit $t(entity.track_other) s žánrem",
"showAlbums": "zobrazit $t(entity.album_other) s žánrem"
},
"trackList": {
"title": "$t(entity.track_other)"
"title": "$t(entity.track_other)",
"artistTracks": "skladby od umělce {{artist}}",
"genreTracks": "$t(entity.track_other) s žánrem „{{genre}}“"
},
"globalSearch": {
"commands": {
@@ -528,7 +638,36 @@
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
"title": "$t(entity.album_other)",
"artistAlbums": "alba od umělce {{artist}}",
"genreAlbums": "$t(entity.album_other) s žánrem „{{genre}}“"
},
"albumArtistDetail": {
"recentReleases": "nedávno vydáno",
"viewDiscography": "zobrazit diskografii",
"about": "O umělci {{artist}}",
"appearsOn": "také v",
"topSongs": "nejlepší skladby",
"topSongsFrom": "nejlepší skladby od umělce {{title}}",
"relatedArtists": "podobní $t(entity.artist_other)",
"viewAllTracks": "zobrazit všechny $t(entity.track_other)",
"viewAll": "zobrazit vše"
},
"itemDetail": {
"copiedPath": "cesta úspěšně zkopírována",
"copyPath": "kopírovat cestu do schránky",
"openFile": "zobrazit skladbu ve správci souborů"
},
"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": {
@@ -559,7 +698,7 @@
"error_savePassword": "při ukládání hesla se vyskytla chyba"
},
"addToPlaylist": {
"success": "přidáno {{message}} $t(entity.song_other) do {{numOfPlaylists}} $t(entity.playlist_other)",
"success": "přidáno $t(entity.trackWithCount, {\"count\": {{message}} }) do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "přidat do $t(entity.playlist_one)",
"input_skipDuplicates": "přeskočit duplicity",
"input_playlists": "$t(entity.playlist_other)"
@@ -578,22 +717,32 @@
"title": "Hledat texty"
},
"editPlaylist": {
"title": "upravit $t(entity.playlist_one)"
"title": "upravit $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) úspěšně aktualizován",
"publicJellyfinNote": "Jellyfin z nějakého důvodu neukazuje, zda je seznam skladeb veřejný, nebo ne. Pokud si přejete, aby zůstal veřejný, zvolte prosím následující vstup"
},
"shareItem": {
"allowDownloading": "umožnit stahování",
"success": "odkaz ke sdílení zkopírován do schránky (klikněte sem pro otevření)",
"description": "popis",
"expireInvalid": "čas vypršení musí být v budoucnosti",
"setExpiration": "nastavit vypršení",
"createFailed": "nepodařilo se vytvořit sdílení (je sdílení povoleno?)"
}
},
"entity": {
"genre_one": "žánr",
"genre_few": "žánry",
"genre_other": "žánrů",
"genre_other": "žánry",
"playlistWithCount_one": "{{count}} playlist",
"playlistWithCount_few": "{{count}} playlisty",
"playlistWithCount_other": "{{count}} playlistů",
"playlist_one": "playlist",
"playlist_few": "playlisty",
"playlist_other": "playlistů",
"playlist_other": "playlisty",
"artist_one": "umělec",
"artist_few": "umělci",
"artist_other": "umělců",
"artist_other": "umělci",
"folderWithCount_one": "{{count}} složka",
"folderWithCount_few": "{{count}} složky",
"folderWithCount_other": "{{count}} složek",
@@ -602,7 +751,7 @@
"albumArtist_other": "umělců alba",
"track_one": "skladba",
"track_few": "skladby",
"track_other": "skladeb",
"track_other": "skladby",
"albumArtistCount_one": "{{count}} umělec alba",
"albumArtistCount_few": "{{count}} umělci alba",
"albumArtistCount_other": "{{count}} umělců alba",
@@ -617,16 +766,22 @@
"artistWithCount_other": "{{count}} umělců",
"folder_one": "složka",
"folder_few": "složky",
"folder_other": "složek",
"folder_other": "složky",
"smartPlaylist": "chytrý $t(entity.playlist_one)",
"album_one": "album",
"album_few": "alba",
"album_other": "alb",
"album_other": "alba",
"genreWithCount_one": "{{count}} žánr",
"genreWithCount_few": "{{count}} žánry",
"genreWithCount_other": "{{count}} žánrů",
"trackWithCount_one": "{{count}} skladba",
"trackWithCount_few": "{{count}} skladby",
"trackWithCount_other": "{{count}} skladeb"
"trackWithCount_other": "{{count}} skladeb",
"play_one": "{{count}} přehrání",
"play_few": "{{count}} přehrání",
"play_other": "{{count}} přehrání",
"song_one": "píseň",
"song_few": "písničky",
"song_other": "písní"
}
}
+185 -46
View File
@@ -8,22 +8,26 @@
"deletePlaylist": "löschen $t(entity.playlist_one)",
"deselectAll": "Alle abwählen",
"goToPage": "Gehe zur Seite",
"moveToTop": "Nach Oben",
"moveToBottom": "Nach Unten",
"moveToTop": "Nach oben",
"moveToBottom": "Nach unten",
"removeFromPlaylist": "Entfernen von $t(entity.playlist_one)",
"viewPlaylists": "Ansicht $t(entity.playlist_other)",
"refresh": "$t(common.refresh)",
"removeFromQueue": "Von Warteschlange entfernen",
"setRating": "Bewertung festlegen",
"toggleSmartPlaylistEditor": "Editor $t(entity.smartPlaylist) umschalten",
"removeFromFavorites": "Entfernen von $t(entity.favorite_other)"
"removeFromFavorites": "Entfernen von $t(entity.favorite_other)",
"openIn": {
"lastfm": "In Last.fm öffnen",
"musicbrainz": "In MusicBrainz öffnen"
}
},
"common": {
"backward": "rückwärts",
"increase": "erhöhen",
"rating": "Wertung",
"bpm": "bpm",
"refresh": "erneuern",
"refresh": "Aktualisieren",
"unknown": "Unbekannt",
"areYouSure": "Bist Du sicher?",
"edit": "Bearbeiten",
@@ -61,7 +65,9 @@
"delete": "Löschen",
"cancel": "Abbrechen",
"forceRestartRequired": "Neustarten um die Änderungen zu übernehmen... Schließe die Benachrichtigung zum Neustarten",
"setting": "Einstellung",
"setting": "Einstellungen",
"setting_one": "Einstellung",
"setting_other": "Einstellungen",
"version": "Version",
"title": "Titel",
"filter_one": "Filter",
@@ -84,7 +90,7 @@
"sortOrder": "Reihenfolge",
"none": "keine",
"menu": "Menü",
"restartRequired": "Neustart benötigt",
"restartRequired": "(Neustart benötigt)",
"previousSong": "vorheriger $t(entity.track_one)",
"noResultsFromQuery": "Die Abfrage brachte keine Ergebnisse",
"quit": "Verlassen",
@@ -96,7 +102,18 @@
"random": "zufällig",
"size": "Größe",
"biography": "Biografie",
"note": "Hinweis"
"note": "Hinweis",
"preview": "Vorschau",
"reload": "Neu Laden",
"mbid": "MusicBrainz ID",
"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",
@@ -117,10 +134,13 @@
"mpvRequired": "MPV benötigt",
"audioDeviceFetchError": "Beim Versuch, Audiogeräte abzurufen, ist ein Fehler aufgetreten",
"invalidServer": "Ungültiger Server",
"loginRateError": "Zu viele Anmeldeversuche, bitte versuche es in einigen Sekunden erneut"
"loginRateError": "Zu viele Anmeldeversuche, bitte versuche es in einigen Sekunden erneut",
"badAlbum": "Sie sehen diese Seite, weil dieses Lied nicht Teil eines Albums ist. Wahrscheinlich sehen Sie dieses Problem, wenn Sie einen Song in Ihrem Musikordner auf oberster Ebene haben. Jellyfin gruppiert nur Songs, wenn sie sich in einem Ordner befinden.",
"networkError": "ein Netzwerkfehler ist aufgetreten",
"openError": "datei kann nicht geöffnet werden"
},
"filter": {
"mostPlayed": "Meist gespielt",
"mostPlayed": "Meistgespielt",
"comment": "Kommentar",
"playCount": "Anzahl abgespielt",
"recentlyUpdated": "kürzlich aktualisiert",
@@ -165,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)",
@@ -191,7 +211,7 @@
"error_savePassword": "Beim Versuch, das Passwort zu speichern, ist ein Fehler aufgetreten"
},
"addToPlaylist": {
"success": "{{message}} $t(entity.song_other) zu {{numOfPlaylists}} $t(entity.playlist_other) hinzugefügt",
"success": "{{message}} $t(entity.track_other) zu {{numOfPlaylists}} $t(entity.playlist_other) hinzugefügt",
"title": "Zu $t(entity.playlist_one) hinzufügen",
"input_skipDuplicates": "Duplikate überspringen",
"input_playlists": "$t(entity.playlist_other)"
@@ -205,12 +225,19 @@
"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",
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)"
},
"shareItem": {
"description": "Beschreibung",
"setExpiration": "Ablaufdatum setzen",
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
"allowDownloading": "Herunterladen zulassen"
}
},
"entity": {
@@ -224,12 +251,12 @@
"artist_other": "Interpreten",
"folderWithCount_one": "{{count}} Verzeichnis",
"folderWithCount_other": "{{count}} Verzeichnisse",
"albumArtist_one": "Album Interpret",
"albumArtist_other": "Album Interpreten",
"albumArtist_one": "Albuminterpret",
"albumArtist_other": "Albuminterpreten",
"track_one": "Track",
"track_other": "Tracks",
"albumArtistCount_one": "{{count}} Album Interpret",
"albumArtistCount_other": "{{count}} Album Interpreten",
"albumArtistCount_one": "{{count}} Albuminterpret",
"albumArtistCount_other": "{{count}} Albuminterpreten",
"albumWithCount_one": "{{count}} Album",
"albumWithCount_other": "{{count}} Alben",
"favorite_one": "Favorit",
@@ -244,13 +271,76 @@
"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": {
"view": {
"table": "Tabelle"
"table": "Tabelle",
"card": "Karte",
"poster": "Poster"
},
"general": {
"tableColumns": "Tabellenspalten",
"gap": "$t(common.gap)",
"size": "$t(common.size)",
"displayType": "Anzeigestil"
},
"label": {
"dateAdded": "Hinzugefügt am",
"lastPlayed": "zuletzt gespielt",
"rowIndex": "Reihenindex",
"trackNumber": "Tracknummer",
"biography": "$t(common.biography)",
"bitrate": "$t(common.bitrate)",
"albumArtist": "$t(entity.albumArtist_one)",
"artist": "$t(entity.artist_one)",
"favorite": "$t(common.favorite)",
"actions": "$t(common.action_other)",
"genre": "$t(entity.genre_one)",
"album": "$t(entity.album_one)",
"size": "$t(common.size)",
"bpm": "$t(common.bpm)",
"titleCombined": "$t(common.title) (kombiniert)",
"channels": "$t(common.channel_other)",
"duration": "$t(common.duration)",
"note": "$t(common.note)",
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"rating": "$t(common.rating)",
"releaseDate": "Veröffentlichungsdatum",
"title": "$t(common.title)",
"year": "$t(common.year)"
}
},
"column": {
"releaseYear": "Jahr",
"biography": "Biografie",
"releaseDate": "Veröffentlichungsdatum",
"bitrate": "Bitrate",
"title": "Titel",
"path": "Pfad",
"album": "Album",
"albumArtist": "Albenkünstler",
"bpm": "bpm",
"favorite": "Favorit",
"lastPlayed": "zuletzt gespielt",
"rating": "Bewertung",
"albumCount": "$t(entity.album_other)",
"artist": "$t(entity.artist_one)",
"channels": "$t(common.channel_other)",
"comment": "Kommentar",
"dateAdded": "hinzugefügt am",
"playCount": "Abgespielt",
"discNumber": "Disk",
"genre": "$t(entity.genre_one)",
"songCount": "$t(entity.track_other)",
"trackNumber": "Nr.",
"size": "$t(common.size)"
}
},
"page": {
@@ -261,16 +351,18 @@
"synchronized": "synchronisiert",
"followCurrentLyric": "dem Songtext folgen",
"opacity": "Deckkraft",
"lyricSize": "Songtext Größe",
"lyricSize": "Songtext-Größe",
"showLyricProvider": "Songtext-Anbieter anzeigen",
"unsynchronized": "nicht synchronisiert",
"lyricAlignment": "Songtext Ausrichtung",
"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",
@@ -278,33 +370,34 @@
"manageServers": "Server verwalten",
"expandSidebar": "Seitenleiste erweitern",
"collapseSidebar": "Seitenleiste einklappen",
"openBrowserDevtools": "Browser Entwicklungswerkzeuge öffnen",
"openBrowserDevtools": "Browser-Entwicklungswerkzeuge öffnen",
"goBack": "Gehe zurück",
"goForward": "Gehe vorwärts",
"settings": "$t(common.setting_other)",
"quit": "$t(common.quit)"
},
"home": {
"mostPlayed": "Meist gespielt",
"mostPlayed": "Meistgespielt",
"newlyAdded": "Neu hinzugefügte Veröffentlichungen",
"explore": "Entdecken Sie Ihre Bibliothek",
"explore": "Entdecke deine Bibliothek",
"recentlyPlayed": "Kürzlich gespielt",
"title": "$t(common.home)"
},
"albumDetail": {
"moreFromArtist": "Mehr von diesem $t(entity.genre_one)",
"moreFromGeneric": "Mehr von {{item}}"
"moreFromArtist": "Mehr von diesem $t(entity.artist_one)",
"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)",
@@ -319,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",
@@ -332,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": {
@@ -385,11 +512,12 @@
"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).",
"audioExclusiveMode": "Audio Exklusiver Modus",
"audioExclusiveMode": "Audio-Exklusivmodus",
"audioDevice": "Audiogerät",
"accentColor": "Akzentfarbe",
"accentColor_description": "Legt die Akzentfarbe für die Anwendung fest",
@@ -418,23 +546,22 @@
"theme_description": "Legt das für die Anwendung zu verwendende Thema fest",
"hotkey_playbackPause": "Pause",
"sidebarCollapsedNavigation_description": "Zeigt die Navigation in der minimierten Seitenleiste an oder verbirgt sie",
"mpvExecutablePath_help": "eine pro Zeile",
"hotkey_volumeUp": "Lauter",
"skipDuration": "Sprung Dauer",
"skipDuration": "Sprungdauer",
"showSkipButtons": "Schaltflächen zum Überspringen anzeigen",
"playButtonBehavior_optionPlay": "$t(player.play)",
"minimumScrobblePercentage": "minimale Scrobble-Dauer (Prozentsatz)",
"lyricFetch": "Songtexte aus dem Internet abrufen",
"scrobble": "Scrobbeln",
"skipDuration_description": "Legt die zu überspringende Dauer fest, wenn die Überspringen-Schaltflächen in der Player-Leiste verwendet werden",
"mpvExecutablePath_description": "Legt den Pfad zur ausführbaren MPV-Datei fest",
"mpvExecutablePath_description": "Legt den Pfad zur ausführbaren MPV-Datei fest. Wenn leer gelassen, wird der Standard-Pfad verwendet",
"replayGainClipping_description": "Verhindern Sie durch {{ReplayGain}} verursachtes Clipping, indem Sie die Verstärkung automatisch verringern",
"replayGainPreamp": "{{ReplayGain}} Vorverstärker (db)",
"hotkey_favoriteCurrentSong": "Favorit $t(common.currentSong)",
"sampleRate": "Abtastrate",
"sidePlayQueueStyle_optionAttached": "angefügt",
"sidebarConfiguration": "Seitenleistenkonfiguration",
"sampleRate_description": "Wählen Sie die auszugebende Abtastrate aus, wenn sich die ausgewählte Abtastfrequenz von der des aktuellen Mediums unterscheidet",
"sampleRate_description": "Wähle die auszugebende Abtastrate aus, wenn sich die ausgewählte Abtastfrequenz von der des aktuellen Mediums unterscheidet. Ein Wert unter 8000 wird die Standard-Frequenz verwenden",
"replayGainMode_optionNone": "$t(common.none)",
"hotkey_zoomIn": "Hineinzoomen",
"scrobble_description": "Scrobble wird auf Ihrem Medienserver abgespielt",
@@ -484,7 +611,7 @@
"savePlayQueue": "Wiedergabe-Warteschlange speichern",
"minimumScrobbleSeconds_description": "die Mindestdauer in Sekunden, die das Lied abspielen muss, bevor es gescrobbelt wird",
"skipPlaylistPage_description": "Gehen Sie beim Navigieren zu einer Wiedergabeliste zur Titelseite der Wiedergabeliste und nicht zur Standardseite",
"fontType_description": "Die integrierte Schriftart wählt eine der von Feishin bereitgestellten Schriftarten aus. Mit der Systemschriftart können Sie jede von Ihrem Betriebssystem bereitgestellte Schriftart auswählen. Benutzerdefiniert erlaubt es eine eigene Schriftart bereitstellen",
"fontType_description": "Die integrierte Schriftart wählt eine der von Feishin bereitgestellten Schriftarten aus. Mit der Systemschriftart können Sie jede von Ihrem Betriebssystem bereitgestellte Schriftart auswählen. Benutzerdefiniert erlaubt es eine eigene Schriftart bereitzustellen",
"playButtonBehavior": "Verhalten der Wiedergabetaste",
"volumeWheelStep": "Lautstärkeregler Stufe",
"sidebarPlaylistList_description": "Ein- oder Ausblenden der Playlisten-Liste in der Seitenleiste",
@@ -499,7 +626,7 @@
"sidebarConfiguration_description": "Wählen Sie die Elemente und die Reihenfolge aus, in der sie in der Seitenleiste angezeigt werden",
"remotePort": "Port des Fernsteuerungsserver",
"hotkey_playbackNext": "Nächster Track",
"useSystemTheme_description": "der systemdefinierten Hell oder Dunkel Präferenz folgen",
"useSystemTheme_description": "der systemdefinierten Hell- oder Dunkelpräferenz folgen",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"lyricFetch_description": "Songtexte aus verschiedenen Internetquellen abrufen",
"lyricFetchProvider_description": "Wählen Sie die Anbieter aus, von denen Sie Liedtexte abrufen möchten. Die Reihenfolge der Anbieter ist die Reihenfolge, in der sie abgefragt werden",
@@ -537,6 +664,18 @@
"fontType": "Schriftartenquelle",
"followLyric": "Songtext synchronisieren",
"floatingQueueArea_description": "Zeige ein Icon auf der rechten Seite, um beim Darüberfahren die Wartschlange anzuzeigen",
"font_description": "Wähle die Schriftart für die Anwendung"
"font_description": "Wähle die Schriftart für die Anwendung",
"themeLight": "Thema (hell)",
"sidePlayQueueStyle_optionDetached": "lösgelöst",
"windowBarStyle_description": "Wähle den Stil der Windows-Leiste",
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) zu Favoriten hinzufügen",
"clearQueryCache_description": "\"Weiches\" Zurücksetzen. Dies wird Playlisten, Musik-Metadaten und gespeicherte Liedtexte zurücksetzen, Zugangsinformationen und zwischengespeicherte Bilder werden behalten",
"discordRichPresence_description": "Zeige deinen Wiedergabe-Status in {{discord}} als rich presence an. Angezeigte Bilder sind: {{icon}}, {{playing}}, und {{paused}} ",
"clearCache": "Browser-Zwischenspeicher löschen",
"clearQueryCache": "feishins Zwischenspeicher leeren",
"clearCache_description": "Hartes Zurücksetzen. Neben feishins Zwischenspeicher wird auch der des Browsers gelöscht (Bilder und andere Daten). Zugangsinformationen und Einstellungen werden behalten",
"sidePlayQueueStyle": "Wiedergabelistenstil in der Seitenleiste",
"zoom_description": "Setzt den Zoom (in %) für das Programm",
"zoom": "Zoom"
}
}
+162 -10
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)",
@@ -16,12 +17,18 @@
"removeFromQueue": "remove from queue",
"setRating": "set rating",
"toggleSmartPlaylistEditor": "toggle $t(entity.smartPlaylist) editor",
"viewPlaylists": "view $t(entity.playlist_other)"
"viewPlaylists": "view $t(entity.playlist_other)",
"openIn": {
"lastfm": "Open in Last.fm",
"musicbrainz": "Open in MusicBrainz"
}
},
"common": {
"action_one": "action",
"action_other": "actions",
"add": "add",
"albumGain": "album gain",
"albumPeak": "album peak",
"areYouSure": "are you sure?",
"ascending": "ascending",
"backward": "backward",
@@ -33,6 +40,8 @@
"channel_one": "channel",
"channel_other": "channels",
"clear": "clear",
"close": "close",
"codec": "codec",
"collapse": "collapse",
"comingSoon": "coming soon…",
"configure": "configure",
@@ -66,6 +75,7 @@
"menu": "menu",
"minimize": "minimize",
"modified": "modified",
"mbid": "MusicBrainz ID",
"name": "name",
"no": "no",
"none": "none",
@@ -75,11 +85,13 @@
"owner": "owner",
"path": "path",
"playerMustBePaused": "player must be paused",
"preview": "preview",
"previousSong": "previous $t(entity.track_one)",
"quit": "quit",
"random": "random",
"rating": "rating",
"refresh": "refresh",
"reload": "reload",
"reset": "reset",
"resetToDefault": "reset to default",
"restartRequired": "restart required",
@@ -91,10 +103,14 @@
"setting": "setting",
"setting_one": "setting",
"setting_other": "settings",
"share": "share",
"size": "size",
"sortOrder": "order",
"title": "title",
"trackNumber": "track",
"trackGain": "track gain",
"trackPeak": "track peak",
"translation": "translation",
"unknown": "unknown",
"version": "version",
"year": "year",
@@ -125,11 +141,15 @@
"genreWithCount_other": "{{count}} genres",
"playlist_one": "playlist",
"playlist_other": "playlists",
"play_one": "{{count}} play",
"play_other": "{{count}} plays",
"playlistWithCount_one": "{{count}} playlist",
"playlistWithCount_other": "{{count}} playlists",
"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"
},
@@ -137,6 +157,7 @@
"apiRouteError": "unable to route request",
"audioDeviceFetchError": "an error occurred when trying to get audio devices",
"authenticationFailed": "authentication failed",
"badAlbum": "you are seeing this page because this song is not part of an album. you are most likely seeing this issue if you have a song at the top level of your music folder. jellyfin only groups tracks if they are in a folder.",
"credentialsRequired": "credentials required",
"endpointNotImplementedError": "endpoint {{endpoint}} is not implemented for {{serverType}}",
"genericError": "an error occurred",
@@ -145,6 +166,7 @@
"loginRateError": "too many login attempts, please try again in a few seconds",
"mpvRequired": "MPV required",
"networkError": "a network error occurred",
"openError": "could not open file",
"playbackError": "an error occurred when trying to play the media",
"remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server",
"remoteEnableError": "an error occurred when trying to $t(common.enable) the remote server",
@@ -216,7 +238,7 @@
"addToPlaylist": {
"input_playlists": "$t(entity.playlist_other)",
"input_skipDuplicates": "skip duplicates",
"success": "added {{message}} $t(entity.song_other) to {{numOfPlaylists}} $t(entity.playlist_other)",
"success": "added $t(entity.trackWithCount, {\"count\": {{message}} }) to $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "add to $t(entity.playlist_one)"
},
"createPlaylist": {
@@ -233,6 +255,8 @@
"title": "delete $t(entity.playlist_one)"
},
"editPlaylist": {
"publicJellyfinNote": "Jellyfin for some reason does not expose whether a playlist is public or not. If you wish for this to remain public, please have the following input selected",
"success": "$t(entity.playlist_one) updated successfully",
"title": "edit $t(entity.playlist_one)"
},
"lyricSearch": {
@@ -244,20 +268,42 @@
"input_optionMatchAll": "match all",
"input_optionMatchAny": "match any"
},
"shareItem": {
"allowDownloading": "allow downloading",
"description": "description",
"setExpiration": "set expiration",
"success": "share link copied to clipboard (or click here to open)",
"expireInvalid": "expiration must be in the future",
"createFailed": "failed to create share (is sharing enabled?)"
},
"updateServer": {
"success": "server updated successfully",
"title": "update server"
}
},
"page": {
"albumArtistDetail": {
"about": "About {{artist}}",
"appearsOn": "appears on",
"recentReleases": "recent releases",
"viewDiscography": "view discography",
"relatedArtists": "related $t(entity.artist_other)",
"topSongs": "top songs",
"topSongsFrom": "top songs from {{title}}",
"viewAll": "view all",
"viewAllTracks": "view all $t(entity.track_other)"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"albumDetail": {
"moreFromArtist": "more from this $t(entity.artist_one)",
"moreFromGeneric": "more from {{item}}"
"moreFromGeneric": "more from {{item}}",
"released": "released"
},
"albumList": {
"artistAlbums": "albums by {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
"title": "$t(entity.album_other)"
},
"appMenu": {
@@ -272,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)",
@@ -281,20 +335,29 @@
"createPlaylist": "$t(action.createPlaylist)",
"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",
"play": "$t(player.play)",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromQueue": "$t(action.removeFromQueue)",
"setRating": "$t(action.setRating)"
"setRating": "$t(action.setRating)",
"playShuffled": "$t(player.shuffle)",
"shareItem": "share item",
"showDetails": "get info"
},
"fullscreenPlayer": {
"config": {
"dynamicBackground": "dynamic background",
"dynamicImageBlur": "image blur size",
"dynamicIsImage": "enable background image",
"followCurrentLyric": "follow current lyric",
"lyricAlignment": "lyric alignment",
"lyricOffset": "lyrics offset (ms)",
"lyricGap": "lyric gap",
"lyricSize": "lyric size",
"opacity": "opacity",
@@ -306,9 +369,13 @@
},
"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)",
"showTracks": "show $t(entity.genre_one) $t(entity.track_other)",
"title": "$t(entity.genre_other)"
},
"globalSearch": {
@@ -326,10 +393,19 @@
"recentlyPlayed": "recently played",
"title": "$t(common.home)"
},
"itemDetail": {
"copyPath": "copy path to clipboard",
"copiedPath": "path copied successfully",
"openFile": "show track in file manager"
},
"playlist": {
"reorder": "reordering only enabled when sorting by id"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"setting": {
"advanced": "advanced",
"generalTab": "general",
"hotkeysTab": "hotkeys",
"playbackTab": "playback",
@@ -346,9 +422,12 @@
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"settings": "$t(common.setting_other)",
"shared": "shared $t(entity.playlist_other)",
"tracks": "$t(entity.track_other)"
},
"trackList": {
"artistTracks": "tracks by {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
"title": "$t(entity.track_other)"
}
},
@@ -365,6 +444,7 @@
"playbackFetchNoResults": "no songs found",
"playbackSpeed": "playback speed",
"playRandom": "play random",
"playSimilarSongs": "play similar songs",
"previous": "previous",
"queue_clear": "clear queue",
"queue_moveToBottom": "move selected to top",
@@ -375,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",
@@ -383,23 +463,44 @@
"stop": "stop",
"toggleFullscreenPlayer": "toggle fullscreen player",
"unfavorite": "unfavorite",
"pause": "pause"
"pause": "pause",
"viewQueue": "view queue"
},
"setting": {
"accentColor": "accent color",
"accentColor_description": "sets the accent color for the application",
"albumBackground": "album background image",
"albumBackground_description": "adds a background image for album pages containing the album art",
"albumBackgroundBlur": "album background image blur size",
"albumBackgroundBlur_description": "adjusts the amount of blur applied to the album background image",
"applicationHotkeys": "application hotkeys",
"applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)",
"artistConfiguration": "album artist page configuration",
"artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page",
"audioDevice": "audio device",
"audioDevice_description": "select the audio device to use for playback (web player only)",
"audioExclusiveMode": "audio exclusive mode",
"audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio",
"audioPlayer": "audio player",
"audioPlayer_description": "select the audio player to use for playback",
"buttonSize": "player bar button size",
"buttonSize_description": "the size of the player bar buttons",
"clearCache": "clear browser cache",
"clearCache_description": "a 'hard clear' of feishin. in addition to clearing feishin's cache, empty the browser cache (saved images and other assets). server credentials and settings are preserved",
"clearQueryCache": "clear feishin cache",
"clearQueryCache_description": "a 'soft clear' of feishin. this will refresh playlists, track metadata, and reset saved lyrics. settings, server credentials and cached images are preserved",
"clearCacheSuccess": "cache cleared successfully",
"contextMenu": "context menu (right click) configuration",
"contextMenu_description": "allows you to hide items that are shown in the menu when you right click on an item. items that are unchecked will be hidden",
"crossfadeDuration": "crossfade duration",
"crossfadeDuration_description": "sets the duration of the crossfade effect",
"crossfadeStyle": "crossfade style",
"crossfadeStyle_description": "select the crossfade style to use for the audio player",
"customCssEnable": "enable custom css",
"customCssEnable_description": "allow for writing custom css.",
"customCssNotice": "Warning: while there is some sanitization (disallowing url() and content:), using custom CSS can still pose risks by changing the interface.",
"customCss": "custom css",
"customCss_description": "custom css content. Note: content and remote urls are disallowed properties. A preview of your content is shown below. Additional fields you didn't set are present due to sanitization.",
"customFontPath": "custom font path",
"customFontPath_description": "sets the path to the custom font to use for the application",
"disableAutomaticUpdates": "disable automatic updates",
@@ -408,12 +509,18 @@
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
"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",
"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",
"discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)",
"doubleClickBehavior": "queue all searched tracks when double clicking",
"doubleClickBehavior_description": "if true, all matching tracks in a track search will be queued. otherwise, only the clicked one will be queued",
"enableRemote": "enable remote control server",
"enableRemote_description": "enables the remote control server to allow other devices to control the application",
"externalLinks": "show external links",
"externalLinks_description": "enables showing external links (Last.fm, MusicBrainz) on artist/album pages",
"exitToTray": "exit to tray",
"exitToTray_description": "exit the application to the system tray",
"floatingQueueArea": "show floating queue hover area",
@@ -430,8 +537,14 @@
"gaplessAudio": "gapless audio",
"gaplessAudio_description": "sets the gapless audio setting for mpv",
"gaplessAudio_optionWeak": "weak (recommended)",
"genreBehavior": "genre page default behavior",
"genreBehavior_description": "determines whether clicking on a genre opens by default in track or album list",
"globalMediaHotkeys": "global media hotkeys",
"globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback",
"homeConfiguration": "home page configuration",
"homeConfiguration_description": "configure what items are shown, and in what order, on the home page",
"homeFeature": "home featured carousel",
"homeFeature_description": "controls whether to show the large featured carousel on the home page",
"hotkey_browserBack": "browser back",
"hotkey_browserForward": "browser forward",
"hotkey_favoriteCurrentSong": "favorite $t(common.currentSong)",
@@ -465,8 +578,12 @@
"hotkey_volumeUp": "volume up",
"hotkey_zoomIn": "zoom in",
"hotkey_zoomOut": "zoom out",
"imageAspectRatio": "use native cover art aspect ratio",
"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",
@@ -480,9 +597,11 @@
"minimumScrobbleSeconds": "minimum scrobble (seconds)",
"minimumScrobbleSeconds_description": "the minimum duration in seconds of the song that must be played before it is scrobbled",
"mpvExecutablePath": "mpv executable path",
"mpvExecutablePath_description": "sets the path to the mpv executable",
"mpvExecutablePath_help": "one per line",
"mpvExecutablePath_description": "sets the path to the mpv executable. if left empty, the default path will be used",
"mpvExtraParameters": "mpv parameters",
"mpvExtraParameters_help": "one per line",
"passwordStore": "passwords/secret store",
"passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords.",
"playbackStyle": "playback style",
"playbackStyle_description": "select the playback style to use for the audio player",
"playbackStyle_optionCrossFade": "crossfade",
@@ -492,6 +611,11 @@
"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",
"playerbarOpenDrawer_description": "allows clicking of the playerbar to open the full screen player",
"remotePassword": "remote control server password",
"remotePassword_description": "sets the password for the remote control server. These credentials are by default transferred insecurely, so you should use a unique password that you do not care about",
"remotePort": "remote control server port",
@@ -510,7 +634,7 @@
"replayGainPreamp": "{{ReplayGain}} preamp (dB)",
"replayGainPreamp_description": "adjust the preamp gain applied to the {{ReplayGain}} values",
"sampleRate": "sample rate",
"sampleRate_description": "select the output sample rate to be used if the sample frequency selected is different from that of the current media",
"sampleRate_description": "select the output sample rate to be used if the sample frequency selected is different from that of the current media. a value less than 8000 will use the default frequency",
"savePlayQueue": "save play queue",
"savePlayQueue_description": "save the play queue when the application is closed and restore it when the application is opened",
"scrobble": "scrobble",
@@ -533,16 +657,37 @@
"skipDuration_description": "sets the duration to skip when using the skip buttons on the player bar",
"skipPlaylistPage": "skip playlist page",
"skipPlaylistPage_description": "when navigating to a playlist, go to the playlist song list page instead of the default page",
"startMinimized": "start minimized",
"startMinimized_description": "start the application in system tray",
"theme": "theme",
"theme_description": "sets the theme to use for the application",
"themeDark": "theme (dark)",
"themeDark_description": "sets the dark theme to use for the application",
"themeLight": "theme (light)",
"themeLight_description": "sets the light theme to use for the application",
"transcodeNote": "takes effect after 1 (web) - 2 (mpv) songs",
"transcode": "enable transcoding",
"transcode_description": "enables transcoding to different formats",
"transcodeBitrate": "bitrate to transcode",
"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",
"volumeWheelStep_description": "the amount of volume to change when scrolling the mouse wheel on the volume slider",
"volumeWidth": "volume slider width",
"volumeWidth_description": "the width of the volume slider",
"webAudio": "use web audio",
"webAudio_description": "use web audio. this enables advanced features like replaygain. disable if you experience otherwise",
"windowBarStyle": "window bar style",
"windowBarStyle_description": "select the style of the window bar",
"zoom": "zoom percentage",
@@ -558,6 +703,7 @@
"bitrate": "bitrate",
"bpm": "bpm",
"channels": "$t(common.channel_other)",
"codec": "$t(common.codec)",
"comment": "comment",
"dateAdded": "date added",
"discNumber": "disc",
@@ -569,6 +715,7 @@
"rating": "rating",
"releaseDate": "release date",
"releaseYear": "year",
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "title",
"trackNumber": "track"
@@ -576,8 +723,11 @@
"config": {
"general": {
"autoFitColumns": "auto fit columns",
"followCurrentSong": "follow current song",
"displayType": "display type",
"gap": "$t(common.gap)",
"itemGap": "item gap (px)",
"itemSize": "item size (px)",
"size": "$t(common.size)",
"tableColumns": "table columns"
},
@@ -590,6 +740,7 @@
"bitrate": "$t(common.bitrate)",
"bpm": "$t(common.bpm)",
"channels": "$t(common.channel_other)",
"codec": "$t(common.codec)",
"dateAdded": "date added",
"discNumber": "disc number",
"duration": "$t(common.duration)",
@@ -604,6 +755,7 @@
"releaseDate": "release date",
"rowIndex": "row index",
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "$t(common.title)",
"titleCombined": "$t(common.title) (combined)",
"trackNumber": "track number",
+214 -59
View File
@@ -8,10 +8,10 @@
"skip": "saltar",
"previous": "anterior",
"toggleFullscreenPlayer": "activar el reproductor a pantalla completa",
"skip_back": "saltar hacia atrás",
"skip_back": "retroceder",
"favorite": "favorito",
"next": "siguiente",
"shuffle": "mezclar",
"shuffle": "Reproducir aleatoriamente",
"playbackFetchNoResults": "ninguna canción encontrada",
"playbackFetchInProgress": "cargando canciones…",
"addNext": "añadir siguiente",
@@ -21,26 +21,27 @@
"repeat_off": "repetir desactivado",
"queue_clear": "limpiar cola",
"muted": "silenciado",
"unfavorite": "no favorito",
"queue_moveToTop": "mover seleccionado al fondo",
"unfavorite": "no favorita",
"queue_moveToTop": "mover seleccionado al final",
"queue_moveToBottom": "mover seleccionado al principio",
"shuffle_off": "mezclar desactivado",
"addLast": "añadir último",
"mute": "silencio",
"skip_forward": "saltar hacia delante",
"pause": "pausa"
"pause": "pausa",
"playSimilarSongs": "Reproducir canciones similares",
"viewQueue": "ver cola"
},
"setting": {
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
"remotePort_description": "establece el puerto para el control remoto del servidor",
"hotkey_skipBackward": "saltar hacia atrás",
"hotkey_skipBackward": "retroceder",
"replayGainMode_description": "ajusta el volumen de ganancia acorde a los valores de {{ReplayGain}} almacenados en los metadatos del archivo",
"audioDevice_description": "selecciona el dispositivo de audio para usar en la reproducción (solo reproductor web)",
"audioDevice_description": "selecciona el dispositivo de audio a usar durante la reproducción (solo reproductor web)",
"theme_description": "establece el tema a usar por la aplicación",
"hotkey_playbackPause": "pausa",
"replayGainFallback": "{{ReplayGain}} alternativa",
"sidebarCollapsedNavigation_description": "mostrar u ocultar la navegación en la barra lateral contraída",
"mpvExecutablePath_help": "uno por línea",
"hotkey_volumeUp": "subir volumen",
"skipDuration": "duración de salto",
"discordIdleStatus_description": "cuando se activa, actualiza el estado mientras el reproductor está inactivo",
@@ -52,7 +53,7 @@
"skipDuration_description": "establece la duración a saltar cuando se usa los botones de saltar en la barra del reproductor",
"enableRemote_description": "activa el control remoto del servidor para permitir a otros dispositivos controlar la aplicación",
"fontType_optionSystem": "fuente del sistema",
"mpvExecutablePath_description": "establece la ruta del ejecutable mpv",
"mpvExecutablePath_description": "establece la ruta del ejecutable mpv. si se deja vacío, se usará la ruta predeterminada",
"replayGainClipping_description": "previene el recorte causado por {{ReplayGain}} bajando automáticamente la ganancia",
"replayGainPreamp": "preamplificador (dB) de {{ReplayGain}}",
"hotkey_favoriteCurrentSong": "$t(common.currentSong) favorita",
@@ -60,14 +61,14 @@
"crossfadeStyle": "estilo de crossfade",
"sidePlayQueueStyle_optionAttached": "acoplada",
"sidebarConfiguration": "configuración de la barra lateral",
"sampleRate_description": "selecciona el ratio de muestreo de salida a ser usado si la frecuencia de muestreo seleccionada es diferente de la del medio actual",
"sampleRate_description": "selecciona el ratio de muestreo de salida a ser usado si la frecuencia de muestreo seleccionada es diferente de la del medio actual. un valor inferior a 8000 usará la frecuencia predeterminada",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainClipping": "recortar {{ReplayGain}}",
"hotkey_zoomIn": "ampliar",
"scrobble_description": "hace scrobble de las reproducciones en tu servidor de medios",
"audioExclusiveMode_description": "activa el modo de audio exclusivo. En este modo, el sistema es normalmente bloqueado, y solo se permitirá mpv en la salida de audio",
"discordUpdateInterval": "intervalo de actualización del estado de actividad de {{discord}}",
"themeLight": "tema (luminoso)",
"themeLight": "tema (claro)",
"fontType_optionBuiltIn": "fuente incorporada",
"hotkey_playbackPlayPause": "play / pausa",
"hotkey_rate1": "calificar con 1 estrella",
@@ -81,8 +82,8 @@
"hotkey_playbackPlay": "reproducir",
"hotkey_togglePreviousSongFavorite": "cambia $t(common.previousSong) a favorito",
"hotkey_volumeDown": "bajar volumen",
"hotkey_unfavoritePreviousSong": "$t(common.previousSong) no favorito",
"audioPlayer_description": "selecciona el reproductor de audio a usar en la reproducción",
"hotkey_unfavoritePreviousSong": "$t(common.previousSong) no favorita",
"audioPlayer_description": "selecciona el reproductor de audio a usar durante la reproducción",
"globalMediaHotkeys": "teclas de acceso rápido globales a medios",
"hotkey_globalSearch": "búsqueda global",
"gaplessAudio_description": "establece la configuración de audio sin pausas para mpv",
@@ -105,11 +106,11 @@
"font": "fuente",
"mpvExtraParameters": "parámetros de mpv",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"themeLight_description": "establece el tema luminoso a usar por la aplicación",
"themeLight_description": "establece el tema claro a usar por la aplicación",
"hotkey_toggleFullScreenPlayer": "cambia el reproductor a pantalla completa",
"hotkey_localSearch": "búsqueda en la página",
"hotkey_toggleQueue": "cambia la cola",
"remotePassword_description": "establece la contraseña para el control remoto del servidor. Esas credenciales son transferidas de forma insegura por defecto, por lo que deberías usar una contraseña única para que no tengas nada de qué preocuparte",
"remotePassword_description": "establece la contraseña para el control remoto del servidor. Esas credenciales son transferidas de forma insegura por defecto, por lo que deberías usar una contraseña única para que no tengas nada de lo que preocuparte",
"hotkey_rate5": "calificar con 5 estrellas",
"hotkey_playbackPrevious": "pista anterior",
"showSkipButtons_description": "muestra o esconde los botones de saltar en la barra del reproductor",
@@ -125,7 +126,7 @@
"hotkey_rate2": "calificar con 2 estrellas",
"playButtonBehavior_description": "establece el comportamiento por defecto del botón de reproducción cuando se añaden canciones a la cola",
"minimumScrobblePercentage_description": "el porcentaje mínimo de la canción que debe ser reproducido antes de hacer scrobble",
"exitToTray": "salida a bandeja",
"exitToTray": "salir a la bandeja",
"hotkey_rate4": "calificar con 4 estrellas",
"enableRemote": "activar control remoto del servidor",
"showSkipButton_description": "muestra o esconde los botones de saltar en la barra del reproductor",
@@ -141,13 +142,13 @@
"replayGainFallback_description": "ganancia en db a aplicar si el archivo no tiene etiquetas de {{ReplayGain}}",
"replayGainPreamp_description": "ajusta la ganancia del preamplificador aplicada a los valores de {{ReplayGain}}",
"hotkey_toggleRepeat": "alterna repetir",
"lyricOffset_description": "desfasa la letra por la cantidad de milisegundos especificada",
"lyricOffset_description": "desfasa la letra en la cantidad de milisegundos especificada",
"sidebarConfiguration_description": "selecciona los elementos y el orden en que aparecerán en la barra lateral",
"fontType": "tipo de fuente",
"remotePort": "puerto del control remoto del servidor",
"applicationHotkeys": "teclas de acceso rápido de la aplicación",
"hotkey_playbackNext": "pista siguiente",
"useSystemTheme_description": "sigue la preferencia luminosa u oscura definida por el sistema",
"useSystemTheme_description": "sigue la preferencia clara u oscura definida por el sistema",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"lyricFetch_description": "busca letras en varias fuentes de Internet",
"lyricFetchProvider_description": "selecciona los proveedores para buscar letras. el orden de los proveedores es el orden en el que se consultarán",
@@ -159,13 +160,13 @@
"sidePlayQueueStyle_optionDetached": "separada",
"audioPlayer": "reproductor de audio",
"hotkey_zoomOut": "reducir",
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) no favorito",
"hotkey_rate0": "limpiar calificación",
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) no favorita",
"hotkey_rate0": "Limpiar calificación",
"discordApplicationId": "id de aplicación {{discord}}",
"applicationHotkeys_description": "configura las teclas de acceso rápido de la aplicación. marca la casilla para establecerlas como teclas de acceso rápido globales (solo escritorio)",
"floatingQueueArea_description": "muestra un icono flotante en el lado derecho de la pantalla para ver la cola de reproducción",
"hotkey_volumeMute": "silenciar volumen",
"hotkey_toggleCurrentSongFavorite": "cambia $t(common.currentSong) a favorito",
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) cambia a favorita",
"remoteUsername": "nombre de usuario del control remoto del servidor",
"showSkipButton": "mostrar botones de saltar",
"sidebarPlaylistList": "listas de reproducción de la barra lateral",
@@ -191,7 +192,72 @@
"accentColor_description": "establece el color de realce de la aplicación",
"skipPlaylistPage": "saltar página de lista de reproducción",
"hotkey_browserForward": "avance",
"hotkey_browserBack": "retroceso"
"hotkey_browserBack": "retroceso",
"clearCache": "Limpiar la caché del navegador",
"clearQueryCache": "Limpiar la caché de Feishin",
"clearQueryCache_description": "Una 'limpieza suave' de Feishin. Esto refrescará las listas de reproducción, los metadatos de las pistas y restablecerá las letras guardadas. Se mantienen los ajustes, credenciales del servidor y las imágenes en caché",
"buttonSize": "tamaño del botón de la barra de reproducción",
"clearCache_description": "Una 'limpieza fuerte' de Feishin. Para limpiar la caché de Feishin, vacía la caché del navegador (imágenes guardadas y otros elementos). Se mantienen las credenciales y ajustes del servidor",
"buttonSize_description": "el tamaño de los botones de la barra de reproducción",
"passwordStore_description": "qué método de almacenamiento de contraseñas/claves secretas utilizar. cambia esta opción si tienes problemas para guardar contraseñas.",
"startMinimized_description": "inicia la aplicación en la bandeja del sistema",
"startMinimized": "iniciar minimizado",
"passwordStore": "contraseñas/almacenamiento secreto",
"playerAlbumArtResolution_description": "la resolución para la vista previa de la carátula del álbum del reproductor grande. más grande hace que parezca más nítido, pero puede ralentizar la carga. El valor predeterminado es 0, lo que significa automático",
"playerAlbumArtResolution": "resolución de la carátula del álbum del reproductor",
"homeConfiguration": "Configuración de la página de inicio",
"mpvExtraParameters_help": "Uno por línea",
"genreBehavior": "Comportamiento predeterminado de la página de géneros",
"externalLinks_description": "Permite mostrar enlaces externos (Last.fm, MusicBrainz) en las páginas del artista/álbum",
"genreBehavior_description": "Determina si al hacer clic en un género se abre por defecto la lista de pistas o de álbumes",
"homeConfiguration_description": "Configura qué elementos son mostrados y en qué orden en la página de inicio",
"clearCacheSuccess": "Caché limpiada correctamente",
"externalLinks": "Mostrar enlaces externos",
"homeFeature": "Carrusel destacado de inicio",
"homeFeature_description": "Controla si se muestra el gran carrusel destacado en la página de inicio",
"imageAspectRatio_description": "Si está habilitado, la portada será mostrada usando su relación de aspecto nativa. Para arte que no es 1:1, el espacio restante estará vacío",
"imageAspectRatio": "Usar relación de aspecto nativa de portada",
"doubleClickBehavior": "poner en cola todas las pistas buscadas al hacer doble clic",
"doubleClickBehavior_description": "si está activado, se pondrán en cola todas las pistas que coincidan en una búsqueda de pistas. De lo contrario, solo se pondrán en cola las pistas seleccionadas",
"volumeWidth": "Ancho del deslizador de volumen",
"volumeWidth_description": "La anchura del deslizador de volumen",
"discordListening_description": "muestra el estado como Escuchando en lugar de Jugando a",
"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",
"customCssEnable": "Habilitar CSS personalizado",
"customCssEnable_description": "Permite escribir CSS personalizado.",
"customCss": "CSS personalizado",
"customCssNotice": "Aviso: mientras hay alguna sanitización (rechazar url() y content:), usar CSS personalizado puede aún entrañar riesgos cambiando la interfaz.",
"customCss_description": "Content CSS personalizado. Nota: content y urls remotas son propiedades rechazadas. Una vista previa de tu content se muestra debajo. Las entradas adicionales que no estableciste están presentes debido a la sanitización.",
"webAudio": "usar audio web",
"webAudio_description": "Utilizar audio web. Esto habilita funciones avanzadas como Replaygain. Desactiva esta opción si tienes problemas",
"transcode": "activar la transcodificación",
"transcode_description": "permite la transcodificación a distintos formatos",
"transcodeBitrate": "tasa de bits a transcodificar",
"transcodeBitrate_description": "selecciona el bitrate a transcodificar. 0 significa dejar que el servidor elija",
"transcodeNote": "tendrá efecto después de 1 (web) - 2 (mpv) canciones",
"transcodeFormat": "formato a transcodificar",
"transcodeFormat_description": "selecciona el formato a transcodificar. dejar vacío para que el servidor decida",
"albumBackground": "imagen de fondo del álbum",
"albumBackground_description": "Añade una imagen de fondo a las páginas del álbum que contienen la carátula del álbum",
"albumBackgroundBlur": "Tamaño de desenfoque de la imagen de fondo del álbum",
"albumBackgroundBlur_description": "Ajusta la cantidad de desenfoque aplicado a la imagen de fondo del álbum",
"playerbarOpenDrawer": "Cambiar la barra del reproductor a pantalla completa",
"playerbarOpenDrawer_description": "Permite hacer clic en la barra del reproductor para abrir el reproductor a pantalla completa",
"artistConfiguration": "Configuración de la página del artista del álbum",
"artistConfiguration_description": "Configura 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": "muestra/oculta 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)",
@@ -207,16 +273,21 @@
"deletePlaylist": "eliminar $t(entity.playlist_one)",
"removeFromQueue": "eliminar de la cola",
"deselectAll": "desmarcar todo",
"moveToBottom": "mover al fondo",
"moveToBottom": "mover al final",
"setRating": "establecer calificación",
"toggleSmartPlaylistEditor": "cambiar editor $t(entity.smartPlaylist)",
"removeFromFavorites": "eliminar de $t(entity.favorite_other)"
"removeFromFavorites": "eliminar de $t(entity.favorite_other)",
"openIn": {
"lastfm": "Abrir en Last.fm",
"musicbrainz": "Abrir en MusicBrainz"
},
"moveToNext": "pasar al siguiente"
},
"common": {
"backward": "hacia atrás",
"increase": "aumentar",
"rating": "calificación",
"bpm": "bpm",
"bpm": "lpm",
"refresh": "actualizar",
"unknown": "desconocido",
"areYouSure": "estás seguro?",
@@ -225,7 +296,7 @@
"left": "izquierda",
"save": "guardar",
"right": "derecha",
"currentSong": "actual $t(entity.track_one)",
"currentSong": "$t(entity.track_one) actual",
"collapse": "contraer",
"trackNumber": "pista",
"descending": "descendiente",
@@ -263,7 +334,7 @@
"saveAndReplace": "guardar y reemplazar",
"playerMustBePaused": "el reproductor debe pausarse",
"confirm": "confirmar",
"resetToDefault": "restablecer a valor por defecto",
"resetToDefault": "restablecer al valor predeterminado",
"home": "inicio",
"comingSoon": "próximamente…",
"reset": "restablecer",
@@ -275,7 +346,7 @@
"previousSong": "anterior $t(entity.track_one)",
"noResultsFromQuery": "la petición no devolvió resultados",
"quit": "salir",
"expand": "ampliar",
"expand": "expandir",
"search": "buscar",
"saveAs": "guardar como",
"disc": "disco",
@@ -293,7 +364,18 @@
"action_other": "acciones",
"channel_one": "Canal",
"channel_many": "Canales",
"channel_other": "Canales"
"channel_other": "Canales",
"trackPeak": "pico de pista",
"albumPeak": "pico del álbum",
"albumGain": "Ganancia de álbum",
"mbid": "ID de MusicBrainz",
"codec": "Códec",
"close": "Cerrar",
"reload": "Recargar",
"share": "Compartir",
"trackGain": "Ganancia de pista",
"preview": "Vista previa",
"translation": "traducción"
},
"error": {
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
@@ -314,11 +396,14 @@
"mpvRequired": "MPV requerido",
"audioDeviceFetchError": "un error ocurrió cuando se intentó obtener los dispositivos de audio",
"invalidServer": "servidor inválido",
"loginRateError": "demasiados intentos de inicio de sesión, por favor inténtalo en unos segundos"
"loginRateError": "demasiados intentos de inicio de sesión, por favor inténtalo en unos segundos",
"badAlbum": "Estás viendo esta página porque esta canción no forma parte de un álbum. Este problema puede ocurrir si tienes una canción en el nivel superior de tu carpeta de música. Jellyfin solo agrupa pistas si están en una carpeta.",
"networkError": "Ocurrió un error de red",
"openError": "No se pudo abrir el archivo"
},
"filter": {
"mostPlayed": "más reproducido",
"isCompilation": "es compilación",
"isCompilation": "es una compilación",
"recentlyPlayed": "recientemente reproducido",
"isRated": "es clasificado",
"title": "título",
@@ -336,7 +421,7 @@
"albumArtist": "$t(entity.albumArtist_one)",
"isRecentlyPlayed": "reproducido recientemente",
"isFavorited": "es favorito",
"bpm": "bpm",
"bpm": "lpm",
"releaseYear": "año de lanzamiento",
"disc": "disco",
"biography": "biografía",
@@ -349,7 +434,7 @@
"criticRating": "calificación de la crítica",
"trackNumber": "pista",
"comment": "comentarios",
"playCount": "número de reproducción",
"playCount": "número de reproducciones",
"recentlyUpdated": "actualizado recientemente",
"channels": "$t(common.channel_other)",
"owner": "$t(common.owner)",
@@ -372,7 +457,8 @@
"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": "compartido $t(entity.playlist_other)"
},
"appMenu": {
"selectServer": "seleccionar servidor",
@@ -402,14 +488,20 @@
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"numberSelected": "{{count}} seleccionado",
"removeFromQueue": "$t(action.removeFromQueue)"
"removeFromQueue": "$t(action.removeFromQueue)",
"shareItem": "Compartir elemento",
"showDetails": "Obtener información",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "descargar",
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"home": {
"mostPlayed": "más reproducidos",
"newlyAdded": "nuevos lanzamientos añadidos",
"title": "$t(common.home)",
"explore": "explorar desde tu biblioteca",
"recentlyPlayed": "recientemente reproducidos"
"explore": "explora desde tu biblioteca",
"recentlyPlayed": "reproducidos recientemente"
},
"fullscreenPlayer": {
"upNext": "siguiente",
@@ -424,29 +516,40 @@
"lyricAlignment": "alineación de letra",
"useImageAspectRatio": "usar ratio de aspecto de imagen",
"showLyricMatch": "mostrar coincidencia de letras",
"lyricGap": "desfase de letra"
"lyricGap": "desfase de letra",
"dynamicImageBlur": "tamaño de desenfoque de imagen",
"dynamicIsImage": "habilitar imagen de fondo",
"lyricOffset": "desplazamiento de letras (ms)"
},
"lyrics": "letras",
"related": "relacionado"
"related": "relacionado",
"visualizer": "visualizador",
"noLyrics": "sin letras"
},
"albumDetail": {
"moreFromArtist": "más de este $t(entity.artist_one)",
"moreFromGeneric": "más de {{item}}"
"moreFromGeneric": "más de {{item}}",
"released": "publicado el"
},
"setting": {
"playbackTab": "reproducción",
"generalTab": "general",
"hotkeysTab": "teclas de acceso rápido",
"windowTab": "ventana"
"windowTab": "ventana",
"advanced": "Avanzado"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
"title": "$t(entity.genre_other)",
"showAlbums": "Mostrar $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "Mostrar $t(entity.genre_one) $t(entity.track_other)"
},
"trackList": {
"title": "$t(entity.track_other)"
"title": "$t(entity.track_other)",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
"artistTracks": "Pistas de {{artist}}"
},
"globalSearch": {
"commands": {
@@ -460,7 +563,36 @@
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
"title": "$t(entity.album_other)",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
"artistAlbums": "Álbumes de {{artist}}"
},
"albumArtistDetail": {
"viewAllTracks": "ver todas las $t(entity.track_other)",
"relatedArtists": "$t(entity.artist_other) similares",
"topSongs": "mejores canciones",
"topSongsFrom": "las mejores canciones de {{title}}",
"viewAll": "Ver todo",
"recentReleases": "Lanzamientos recientes",
"viewDiscography": "Ver discografía",
"about": "Sobre {{artist}}",
"appearsOn": "Aparece en"
},
"itemDetail": {
"copiedPath": "Ruta copiada correctamente",
"openFile": "Mostrar pista en el gestor de archivos",
"copyPath": "Copiar ruta al portapapeles"
},
"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": {
@@ -491,7 +623,7 @@
"error_savePassword": "un error ocurrió cuando se intentó guardar la contraseña"
},
"addToPlaylist": {
"success": "añadido {{message}} $t(entity.song_other) a {{numOfPlaylists}} $t(entity.playlist_other)",
"success": "añadido $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "añadir a $t(entity.playlist_one)",
"input_skipDuplicates": "saltar duplicados",
"input_playlists": "$t(entity.playlist_other)"
@@ -506,11 +638,21 @@
"title": "buscar letras"
},
"editPlaylist": {
"title": "editar $t(entity.playlist_one)"
"title": "editar $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) actualizada correctamente",
"publicJellyfinNote": "Jellyfin por alguna razón no expone si una lista de reproducción es pública o no. Si deseas que ésta siga siendo pública, por favor ten seleccionada la siguiente entrada"
},
"queryEditor": {
"input_optionMatchAll": "coincidir todos",
"input_optionMatchAny": "coincidir cualquiera"
},
"shareItem": {
"createFailed": "No se pudo crear el recurso compartido (¿está habilitado el uso compartido?)",
"allowDownloading": "Permitir la descarga",
"description": "Descripción",
"setExpiration": "Establecer expiración",
"success": "Enlace de compartición copiado al portapapeles (o pulsa aquí para abrir)",
"expireInvalid": "La expiración debe ser en el futuro"
}
},
"table": {
@@ -527,16 +669,18 @@
"releaseDate": "fecha de lanzamiento",
"bitrate": "tasa de bits",
"title": "título",
"bpm": "bpm",
"bpm": "lpm",
"dateAdded": "fecha de adición",
"artist": "$t(entity.artist_one)",
"songCount": "$t(entity.track_other)",
"trackNumber": "pista",
"genre": "$t(entity.genre_one)",
"albumArtist": "artista de álbum",
"albumArtist": "artista del álbum",
"path": "ruta",
"discNumber": "disco",
"channels": "$t(common.channel_other)"
"channels": "$t(common.channel_other)",
"size": "$t(common.size)",
"codec": "$t(common.codec)"
},
"config": {
"label": {
@@ -562,17 +706,22 @@
"note": "$t(common.note)",
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"playCount": "número de reproducción",
"playCount": "número de reproducciones",
"genre": "$t(entity.genre_one)",
"favorite": "$t(common.favorite)",
"year": "$t(common.year)"
"year": "$t(common.year)",
"codec": "$t(common.codec)",
"songCount": "$t(entity.track_other)"
},
"general": {
"gap": "$t(common.gap)",
"tableColumns": "columnas de la tabla",
"autoFitColumns": "ajuste automático de columnas",
"size": "$t(common.size)",
"displayType": "tipo de visualización"
"displayType": "tipo de visualización",
"itemGap": "espacio entre elementos (px)",
"itemSize": "tamaño del elemento (px)",
"followCurrentSong": "seguir la canción actual"
},
"view": {
"card": "tarjeta",
@@ -598,15 +747,15 @@
"folderWithCount_one": "{{count}} carpeta",
"folderWithCount_many": "{{count}} carpetas",
"folderWithCount_other": "{{count}} carpetas",
"albumArtist_one": "artista de álbum",
"albumArtist_many": "artistas de álbum",
"albumArtist_other": "artistas de álbum",
"albumArtist_one": "artista del álbum",
"albumArtist_many": "artistas del álbum",
"albumArtist_other": "artistas del álbum",
"track_one": "pista",
"track_many": "pistas",
"track_other": "pistas",
"albumArtistCount_one": "{{count}} artista de álbum",
"albumArtistCount_many": "{{count}} artistas de álbum",
"albumArtistCount_other": "{{count}} artistas de álbum",
"albumArtistCount_one": "{{count}} artista del álbum",
"albumArtistCount_many": "{{count}} artistas del álbum",
"albumArtistCount_other": "{{count}} artistas del álbum",
"albumWithCount_one": "{{count}} álbum",
"albumWithCount_many": "{{count}} álbumes",
"albumWithCount_other": "{{count}} álbumes",
@@ -627,6 +776,12 @@
"genreWithCount_other": "{{count}} géneros",
"trackWithCount_one": "{{count}} pista",
"trackWithCount_many": "{{count}} pistas",
"trackWithCount_other": "{{count}} pistas"
"trackWithCount_other": "{{count}} pistas",
"play_one": "{{count}} reproducción",
"play_many": "{{count}} reproducciones",
"play_other": "{{count}} reproducciones",
"song_one": "canción",
"song_many": "canciones",
"song_other": "canciones"
}
}
+626
View File
@@ -0,0 +1,626 @@
{
"player": {
"repeat_all": "تکرار همه",
"stop": "توقف",
"repeat": "تکرار",
"skip": "رد کن",
"toggleFullscreenPlayer": "تغییر به پخش‌کنندهٔ تمام‌صفحه",
"skip_back": "برو عقب",
"shuffle": "پخش تصادفی",
"repeat_off": "تکرار غیرفعال",
"pause": "ایست",
"unfavorite": "حذف از موردعلاقه‌ها",
"shuffle_off": "پخش تصادفی غیر فعال",
"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)",
"goToPage": "برو به صفحهٔ",
"moveToTop": "انتقال به بالا",
"clearQueue": "خالی کردن صف",
"addToFavorites": "افزودن به $t(entity.favorite_other)",
"addToPlaylist": "افزودن به $t(entity.playlist_one)",
"createPlaylist": "ساخت $t(entity.playlist_one)",
"removeFromPlaylist": "حذف از $t(entity.playlist_one)",
"viewPlaylists": "نمایش $t(entity.playlist_other)",
"refresh": "$t(common.refresh)",
"deletePlaylist": "حذف $t(entity.playlist_one)",
"removeFromQueue": "حذف از صف",
"deselectAll": "لغو انتخاب همه",
"moveToBottom": "انتقال به پایین",
"setRating": "تعیین امتیاز",
"toggleSmartPlaylistEditor": "تغییر ویرایشگر $t(entity.smartPlaylist)",
"removeFromFavorites": "حذف از $t(entity.favorite_other)",
"openIn": {
"lastfm": "باز کردن در Last.fm",
"musicbrainz": "باز کردن در MusicBranz"
},
"moveToNext": "جابجا کردن به بعدی"
},
"setting": {
"hotkey_skipBackward": "برو عقب",
"audioDevice_description": "دستگاه صوتی را برای پخش انتخاب کنید (فقط پخش‌کنندهٔ تحت وب)",
"hotkey_playbackPause": "pause",
"hotkey_volumeUp": "زیاد کردن صدا",
"playButtonBehavior_optionPlay": "$t(player.play)",
"lyricFetch": "دریافت متن ترانه از اینترنت",
"enableRemote_description": "کنترل از راه دور سرویس‌دهنده را فعال کنید تا به دستگاه‌های دیگر اجازهٔ مدیریت اپلیکیشن را بدهید",
"mpvExecutablePath_description": "تعیین مسیر فایل اجرایی MPV",
"sampleRate": "sample rate",
"replayGainMode_optionNone": "$t(common.none)",
"hotkey_rate1": "امتیاز ۱ ستاره",
"hotkey_skipForward": "برو جلو",
"disableLibraryUpdateOnStartup": "غیرفعال کردن بررسی آخرین نسخه در آغاز به کار برنامه",
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"hotkey_playbackPlay": "پخش",
"hotkey_volumeDown": "کم کردن صدا",
"audioPlayer_description": "پخش‌کنندهٔ صدا را برای پخش انتخاب کنید",
"hotkey_globalSearch": "جست و جوی سراسری",
"disableAutomaticUpdates": "غیرفعال کردن به‌‌روزرسانی خودکار",
"exitToTray_description": "خروج از اپلیکیشن به system tray",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"discordUpdateInterval_description": "فاصلهٔ بین هر به روزرسانی به ثانیه (حداقل ۱۵ ثانیه)",
"audioExclusiveMode": "حالت اختصاصی صدا",
"remotePassword": "رمز عبور کنترل از راه دور",
"language_description": "زبان اپلیکیشن را معین می‌کند $t(common.restartRequired)",
"hotkey_rate3": "امتیاز ۳ ستاره",
"font": "قلم",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"hotkey_toggleFullScreenPlayer": "تغییر به پخش‌کنندهٔ تمام‌صفحه",
"hotkey_localSearch": "جست و جو در صفحه",
"hotkey_toggleQueue": "تغییر صف",
"hotkey_rate5": "امتیاز ۵ ستاره",
"hotkey_playbackPrevious": "قطعهٔ قبل",
"language": "زبان",
"hotkey_toggleShuffle": "تغییر شافل",
"mpvExecutablePath": "مسیر اجرای MPV",
"audioDevice": "دستگاه صوتی",
"hotkey_rate2": "امتیاز ۲ ستاره",
"playButtonBehavior_description": "رفتار پیش‌فرض دکمهٔ پخش را هنگامی که آهنگی به صف افزوده می‌شود را معین می‌کند",
"exitToTray": "خروج به tray",
"hotkey_rate4": "امتیاز ۴ ستاره",
"enableRemote": "فعال کردن کنترل از راه دور سرویس‌دهنده",
"showSkipButton_description": "نمایش یا مخفی کردن دکمهٔ رد کردن روی نوار پخش‌کننده",
"playButtonBehavior": "رفتار دکمهٔ پخش",
"playbackStyle_optionNormal": "عادی",
"hotkey_toggleRepeat": "تغییر تکرار",
"fontType": "نوع قلم",
"hotkey_playbackNext": "قطعهٔ بعد",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"lyricFetch_description": "دریافت متن ترانه از منابع اینترنتی",
"customFontPath": "مسیر قلم سفارشی",
"audioPlayer": "پخش‌کنندهٔ صدا",
"hotkey_rate0": "حذف امتیاز",
"discordApplicationId": "{{discord}} application id",
"hotkey_volumeMute": "بستن صدا",
"showSkipButton": "نمایش دکمهٔ رد کردن",
"customFontPath_description": "مسیر قلم سفارشی را برای استفاده در اپلیکیشن مشخص کنید",
"gaplessAudio_optionWeak": "ضعیف (توصیه شده)",
"hotkey_playbackStop": "توقف",
"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": "به شما اجازه می‌دهد که آیتم‌های نمایش داده شده در فهرستی که وقتی روی یک آیتم کلیک راست می‌کنید پدیدار می‌شود، را پنهان کنید. آیتم‌هایی که منتخب نیستند پنهان می‌شوند",
"crossfadeStyle": "شیوه‌ی crossfade",
"customCssEnable_description": "اجازه دادن برای نوشتن css سفارشی.",
"translationApiKey": "کلید API ترجمه",
"webAudio_description": "از صدای وب بهره‌مند می‌شود. این قابلیت‌های پیشرفته‌ای مانند گین بازپخش (replygain) را فعال می‌کند. غیرفعال کنید اگر غیر از این را تجربه می‌کنید",
"windowBarStyle_description": "گزینش سبک نوار پنجره",
"translationApiKey_description": "کلید API برای ترجمه (پشتیبانی فقط برای نقطه‌ی پایانی سرویس‌دهنده‌ی جهانی)",
"theme": "تم",
"hotkey_togglePreviousSongFavorite": "تغییر وضعیت برای مورد علاقه‌ی $t(common.previousSong)",
"transcode": "فعال‌سازی رمزگردانی",
"transcode_description": "رمزگردانی به فرمت‌های گوناگون را فعال می‌کند",
"transcodeBitrate": "نرخ انتقال رمزگردانی",
"startMinimized": "پنهان‌شده آغاز کن",
"theme_description": "تم مورد استفاده در نرم‌افزار را می‌گزیند",
"themeLight": "تم (روشن)",
"transcodeBitrate_description": "نرخ انتقال برای رمزگردانی را انتخاب می‌کند. 0 بدان معناست سرور آن را انتخاب کند",
"transcodeFormat": "فرمت رمزگردانی",
"transcodeFormat_description": "فرمت رمزگردانی را انتخاب می‌کند. برای اینکه سرور آن را انتخاب کند، خالی بگذارید",
"customCssEnable": "فعال کردن css سفارشی",
"translationTargetLanguage": "زبان هدف ترجمه",
"hotkey_toggleCurrentSongFavorite": "تغییر وضعیت مورد علاقه برای $t(common.currentSong)",
"themeDark_description": "تم تاریک را برای استفاده‌ی نرم‌افزار می‌گزیند",
"volumeWheelStep_description": "اندازه‌ای از حجم صدا را در زمان اسکرول کردن روی نوار لغزنده تغییر داده شود",
"trayEnabled": "نمایش سینی",
"trayEnabled_description": "نمایش/پنهان کردن آیکون/فهرست در سینی. اگر غیرفعال باشد، کوچک کردن/خروج به سینی را نیز غیرفعال می‌کند",
"useSystemTheme_description": "از روشنی یا تاریکی که سیستم تعریف کرده است، پیروی می‌کند",
"crossfadeDuration": "زمان محو کردن گذار قطعه به قطعه‌ی بعدی",
"themeLight_description": "تم روشن را برای استفاده‌ی نرم‌افزار می‌گزیند",
"volumeWidth": "عرض نوار لغزنده‌ی حجم صدا",
"crossfadeStyle_description": "شیوه‌ی crossfade که می‌خواهید پخش‌کننده از آن استفاده کند را انتخاب کنید",
"startMinimized_description": "نرم‌افزار را در سینی اجرا کن",
"volumeWidth_description": "عرضی که نوار لغزنده‌ی حجم صدا داشته باشد",
"themeDark": "تم (تاریک)",
"useSystemTheme": "استفاده از تم سیستم",
"volumeWheelStep": "گام چرخ حجم صدا",
"webAudio": "استفاده از صدای وب",
"windowBarStyle": "سبک نوار پنجره",
"crossfadeDuration_description": "زمان افکت crossfade را مشخص می‌کند"
},
"common": {
"backward": "به عقب",
"increase": "افزایش",
"rating": "امتیاز",
"bpm": "bpm",
"refresh": "تازه‌سازی",
"unknown": "ناشناخته",
"areYouSure": "مطمئنید؟",
"edit": "ویرایش",
"favorite": "موردعلاقه",
"left": "چپ",
"save": "ذخیره",
"right": "راست",
"currentSong": "فعلی $t(entity.track_one)",
"collapse": "بستن",
"trackNumber": "قطعه",
"descending": "نزولی",
"add": "افزودن",
"gap": "فاصله",
"ascending": "صعودی",
"dismiss": "رد",
"year": "سال",
"manage": "مدیریت",
"limit": "محدود",
"minimize": "کمینه",
"modified": "ویراسته شده",
"duration": "مدت",
"name": "نام",
"maximize": "بیشینه",
"decrease": "کم کردن",
"ok": "باشه",
"description": "شرح",
"configure": "تنظیم",
"path": "مسیر",
"center": "وسط",
"no": "خیر",
"owner": "مالک",
"enable": "فعال",
"clear": "خالی",
"forward": "جلو",
"delete": "حذف",
"cancel": "لغو",
"forceRestartRequired": "برای اعمال تغییرها دوباره راه‌اندازی کنید… اعلان را برای راه‌اندازی دوباره ببندید",
"version": "نسخه",
"title": "عنوان",
"filter_one": "پالایش",
"filter_other": "پالایش",
"filters": "پالایش",
"create": "ساختن",
"bitrate": "بیت‌ریت",
"saveAndReplace": "ذخیره و جایگزین",
"action_one": "عملیات",
"action_other": "عملیات",
"playerMustBePaused": "پخش‌کننده باید متوقف شود",
"confirm": "تایید",
"resetToDefault": "بازنشانی به پیش‌فرض",
"home": "خانه",
"comingSoon": "به زودی…",
"reset": "بازنشانی",
"channel_one": "کانال",
"channel_other": "کانال",
"disable": "غیرفعال",
"sortOrder": "ترتیب",
"none": "هیچ",
"menu": "منو",
"restartRequired": "راه‌اندازی دوباره لازم است",
"previousSong": "$t(entity.track_one) پیشین",
"noResultsFromQuery": "جست‌وجو نتیجه‌ای نداشت",
"quit": "خروج",
"expand": "گسترش",
"search": "جست‌وجو",
"saveAs": "ذخیره کن با اسم",
"disc": "دیسک",
"yes": "بله",
"random": "تصادفی",
"size": "حجم",
"biography": "زندگی‌نامه",
"note": "توجه",
"albumGain": "گین آلبوم",
"close": "بستن",
"albumPeak": "اوج آلبوم",
"mbid": "شناسه‌ی MusicBrainz",
"reload": "بارگذاری مجدد",
"setting": "پیکربندی",
"trackGain": "گین قطعه",
"trackPeak": "اوج قطعه",
"translation": "ترجمه",
"preview": "پیش‌نمایش",
"share": "اشتراک‌گذاری",
"codec": "کدک"
},
"error": {
"remotePortWarning": "برای تعیین port تازه، سرویس دهنده را دوباره راه‌اندازی کنید",
"playbackError": "هنگام پخش خطایی رخ داد",
"remotePortError": "هنگام تعیین port سرویس دهنده خطایی رخ داد",
"serverRequired": "سرویس‌دهنده ضروری است",
"authenticationFailed": "احراز هویت شکست خورد",
"apiRouteError": "درخواست منتقل نشد",
"genericError": "خطایی رخ داد",
"credentialsRequired": "باید وارد شوید",
"sessionExpiredError": "جلسه شما منقضی شده است",
"remoteEnableError": "هنگام $t(common.enable) سرویس دهنده خطای رخ داد",
"serverNotSelectedError": "سرویس‌دهنده‌ای انتخاب نشده",
"remoteDisableError": "هنگام $t(common.disable) سرویس دهنده خطایی رخ داد",
"mpvRequired": "وجود MPV ضروری است",
"audioDeviceFetchError": "هنگام دسترسی به دستگاه صوتی خطایی رخ داد",
"localFontAccessDenied": "دسترسی به فونت‌های محلی پذیرفته نشد",
"loginRateError": "تلاش‌های بسیار برای ورود انجام داده‌اید،‌لطفاً بعد از چند ثانیه دوباره امتحان کنید",
"networkError": "خطای شبکه رخ داد",
"badAlbum": "شما این صفحه را می‌بینید چون‌که این آهنگ قسمتی از یک آلبوم نیست. شما احتمالا این مسأله را به این خاطر می‌بینید که آهنگی در پوشه‌ی سطح بالای آهنگ‌هایتان دارید. جلی‌فین فقط قطعه‌هایی را گروه‌بندی می‌کند که در یک پوشه قرار دارند.",
"invalidServer": "سرویس‌دهنده‌ی نامعتبر",
"openError": "نمی‌توان پرونده را باز کرد",
"endpointNotImplementedError": "نقطه‌ی پایان {{endpoint}} برای {{serverType}} قرار داده نشده است",
"systemFontError": "خطایی هنگام تلاش برای دریافت فونت‌های سیستم رخ داد"
},
"filter": {
"mostPlayed": "بیشتر پخش شده",
"comment": "نظر",
"playCount": "تعداد پخش",
"recentlyUpdated": "به تازگی به روز شده",
"channels": "$t(common.channel_other)",
"recentlyPlayed": "به تازگی پخش شده",
"isRated": "امتیاز داده شده است",
"owner": "$t(common.owner)",
"title": "عنوان",
"rating": "امتیاز",
"search": "جست‌وجو",
"bitrate": "بیت‌ریت",
"genre": "$t(entity.genre_one)",
"recentlyAdded": "به تازگی افزوده شده",
"note": "توجه",
"name": "نام",
"dateAdded": "تاریخ افزوده شدن",
"releaseDate": "تاریخ انتشار",
"albumCount": "$t(entity.album_other) عدد",
"path": "مسیر",
"favorited": "موردعلاقه",
"albumArtist": "$t(entity.albumArtist_one)",
"isRecentlyPlayed": "به تازگی پخش شده است",
"isFavorited": "موردعلاقه است",
"bpm": "bpm",
"releaseYear": "سال انتشار",
"id": "id",
"disc": "دیسک",
"biography": "زندگی‌نامه",
"songCount": "تعداد ترانه",
"artist": "$t(entity.artist_one)",
"duration": "مدت",
"isPublic": "عمومی است",
"random": "تصادفی",
"lastPlayed": "به تازگی پخش شده",
"toYear": "تا سال",
"fromYear": "از سال",
"criticRating": "امتیاز منتقدین",
"album": "$t(entity.album_one)",
"trackNumber": "قطعه",
"communityRating": "رتبه بندی جامعه",
"isCompilation": "مخلوط است"
},
"form": {
"deletePlaylist": {
"title": "حذف $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) حذف شد",
"input_confirm": "برای تایید، نام $t(entity.playlist_one) را وارد کنید"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"title": "ساخت $t(entity.playlist_one)",
"input_public": "عمومی",
"input_name": "$t(common.name)",
"success": "$t(entity.playlist_one) ساخته شد",
"input_owner": "$t(common.owner)"
},
"addServer": {
"title": "افزودن سرویس دهنده",
"input_username": "نام کاربری",
"input_url": "نشانی",
"input_password": "رمز عبور",
"input_name": "نام سرویس‌دهنده",
"success": "سرویس‌دهنده افزوده شد",
"input_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) افزوده شد",
"title": "افزودن به $t(entity.playlist_one)",
"input_playlists": "$t(entity.playlist_other)",
"input_skipDuplicates": "پرش از تکراری‌ها"
},
"lyricSearch": {
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)",
"title": "جست‌وجو در متن شعر"
},
"editPlaylist": {
"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": "ژانرها",
"playlistWithCount_one": "{{count}} فهرست پخش",
"playlistWithCount_other": "{{count}} فهرست پخش",
"playlist_one": "فهرست پخش",
"playlist_other": "فهرست‌های پخش",
"artist_one": "هنرمند",
"artist_other": "هنرمندان",
"folderWithCount_one": "{{count}} پوشه",
"folderWithCount_other": "{{count}} پوشه",
"albumArtist_one": "هنرمند آلبوم",
"albumArtist_other": "هنرمندان آلبوم",
"track_one": "قطعه",
"track_other": "قطعه‌ها",
"albumArtistCount_one": "{{count}} هنرمند آلبوم",
"albumArtistCount_other": "{{count}} هنرمند آلبوم",
"albumWithCount_one": "{{count}} آلبوم",
"albumWithCount_other": "{{count}} آلبوم",
"favorite_one": "موردعلاقه",
"favorite_other": "موردعلاقه",
"artistWithCount_one": "{{count}} هنرمند",
"artistWithCount_other": "{{count}} هنرمند",
"folder_one": "پوشه",
"folder_other": "پوشه‌ها",
"smartPlaylist": "$t(entity.playlist_one) هوشمند",
"album_one": "آلبوم",
"album_other": "آلبوم‌ها",
"genreWithCount_one": "{{count}} ژانر",
"genreWithCount_other": "{{count}} ژانر",
"trackWithCount_one": "{{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": "نمایش قطعه در مدیر پرونده"
}
},
"table": {
"column": {
"size": "$t(common.size)",
"lastPlayed": "آخرین بار پخش شده",
"discNumber": "دیسک",
"songCount": "$t(entity.track_other)",
"title": "عنوان",
"trackNumber": "قطعه",
"favorite": "مورد علاقه",
"genre": "$t(entity.genre_one)",
"comment": "دیدگاه",
"playCount": "تعداد پخش",
"rating": "امتیاز",
"path": "مسیر",
"releaseYear": "سال",
"dateAdded": "تاریخ افزوده شدن",
"releaseDate": "تاریخ عرضه"
},
"config": {
"general": {
"followCurrentSong": "آهنگ کنونی را دنبال کن",
"displayType": "نوع نمایش",
"itemSize": "اندازه‌ی آیتم (px)",
"size": "$t(common.size)",
"tableColumns": "ستون‌های جدول",
"autoFitColumns": "تطبیق دادن ستون‌ها به شیوه‌ی خودکار",
"gap": "$t(common.gap)",
"itemGap": "فاصله‌ی آیتم (px)"
},
"view": {
"card": "کارت"
},
"label": {
"playCount": "تعداد پخش",
"dateAdded": "تاریخ افزوده شدن",
"discNumber": "شماره‌ی دیسک",
"lastPlayed": "آخرین بار پخش شده",
"actions": "$t(common.action_other)"
}
}
}
}
+767
View File
@@ -0,0 +1,767 @@
{
"common": {
"size": "koko",
"search": "etsi",
"sortOrder": "järjestys",
"setting": "asetus",
"title": "otsikko",
"trackNumber": "raita",
"action_one": "toiminto",
"action_other": "toiminnot",
"add": "lisää",
"areYouSure": "oletko varma?",
"ascending": "nouseva",
"backward": "takaperin",
"bitrate": "bittinopeus",
"channel_one": "kanava",
"channel_other": "kanavat",
"collapse": "luhista",
"comingSoon": "tulossa pian…",
"configure": "konfiguroi",
"confirm": "hyväksy",
"disable": "poista käytöstä",
"disc": "levy",
"dismiss": "hylkää",
"favorite": "suosikki",
"filter_one": "suodatin",
"filter_other": "suodattimet",
"filters": "suodattimet",
"forceRestartRequired": "käynnistä uudelleen ottaaksesi muutokset käyttöön… sulje ilmoitus käynnistääksesi uudelleen",
"gap": "väli",
"home": "koti",
"left": "vasen",
"limit": "raja",
"manage": "hallitse",
"menu": "valikko",
"minimize": "minimoi",
"modified": "muokattu",
"name": "nimi",
"no": "ei",
"none": "ei mitään",
"noResultsFromQuery": "kysely ei tuottanut tuloksia",
"note": "huomautus",
"ok": "ok",
"owner": "omistaja",
"path": "polku",
"preview": "esikatsele",
"previousSong": "edellinen $t(entity.track_one)",
"resetToDefault": "palauta oletusarvoihin",
"restartRequired": "vaatii uudelleenkäynnistyksen",
"right": "oikea",
"save": "tallenna",
"saveAndReplace": "tallenna ja korvaa",
"saveAs": "tallenna nimellä",
"unknown": "tuntematon",
"version": "versio",
"year": "vuosi",
"yes": "kyllä",
"close": "sulje",
"descending": "laskeva",
"biography": "biografia",
"cancel": "peruuta",
"bpm": "bpm",
"decrease": "pienennä",
"center": "keskitä",
"clear": "tyhjennä",
"codec": "koodekki",
"create": "luo",
"description": "kuvaus",
"currentSong": "nykyinen $t(entity.track_one)",
"delete": "poista",
"duration": "kesto",
"edit": "muokkaa",
"enable": "ota käyttöön",
"expand": "laajenna",
"increase": "lisää",
"forward": "eteenpäin",
"maximize": "maksimoi",
"mbid": "MusicBrainz ID",
"share": "jaa",
"random": "satunnainen",
"reload": "lataa uudelleen",
"quit": "poistu",
"rating": "arvostelu",
"refresh": "virkistä",
"reset": "nollaa",
"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": "albumit",
"albumArtist_one": "albumin artisti",
"albumArtist_other": "albumin artistit",
"artistWithCount_one": "{{count}} artisti",
"artistWithCount_other": "{{count}} artistia",
"playlist_one": "soittolista",
"playlist_other": "soittolistat",
"playlistWithCount_one": "{{count}} soittolista",
"playlistWithCount_other": "{{count}} soittolistaa",
"albumArtistCount_one": "{{count}} albumin artisti",
"albumArtistCount_other": "{{count}} albumin artistia",
"albumWithCount_one": "{{count}} albumi",
"albumWithCount_other": "{{count}} albumia",
"artist_one": "artisti",
"artist_other": "artistit",
"favorite_one": "suosikki",
"favorite_other": "suosikit",
"folder_one": "kansio",
"folder_other": "kansiot",
"folderWithCount_one": "{{count}} kansio",
"folderWithCount_other": "{{count}} kansiota",
"genre_one": "genre",
"genre_other": "genret",
"genreWithCount_one": "{{count}} genre",
"genreWithCount_other": "{{count}} genreä",
"smartPlaylist": "älykäs $t(entity.playlist_one)",
"track_one": "raita",
"track_other": "raidat",
"trackWithCount_one": "{{count}} raita",
"trackWithCount_other": "{{count}} raitaa",
"play_one": "{{count}} toisto",
"play_other": "{{count}} toistoa",
"song_one": "kappale",
"song_other": "kappaleet"
},
"action": {
"clearQueue": "tyhjennä jono",
"createPlaylist": "luo $t(entity.playlist_one)",
"deselectAll": "poista kaikkien valinta",
"editPlaylist": "muokkaa $t(entity.playlist_one)",
"removeFromQueue": "poista jonosta",
"viewPlaylists": "katsele $t(entity.playlist_other)",
"openIn": {
"lastfm": "Avaa Last.fm:ssä",
"musicbrainz": "Avaa MusicBrainz:ssä"
},
"goToPage": "mene sivulle",
"moveToBottom": "siirry pohjalle",
"moveToTop": "siirry ylös",
"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",
"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": "verkkoyhteysvirhe",
"openError": "tiedostoa ei voitu avata",
"localFontAccessDenied": "paikallisiin fontteihin pääsy on kielletty",
"playbackError": "mediaa toistaessa tapahtui virhe",
"remotePortWarning": "käynnistä palvelin uudestaan ottaaksesi uuden portin käyttöön",
"endpointNotImplementedError": "päätepiste {{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 kirjautumistapaa",
"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": "$t(entity.trackWithCount, {\"count\": {{message}} }) lisätty $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",
"customCss": "oma css",
"customFontPath_description": "asettaa polun mukautetulle fontille jota sovellus käyttää",
"homeConfiguration": "koti sivun muokkaus",
"homeConfiguration_description": "määritä mitä osioita näkyy, ja missä järjestyksessä, koti sivulla",
"gaplessAudio_optionWeak": "heikko (suositus)",
"genreBehavior_description": "määrittää avautuuko generä painettaessa oletuksena ääniraita vaiko albumi listassa",
"hotkey_browserBack": "selain takaisin",
"hotkey_playbackPlay": "toista",
"hotkey_playbackPlayPause": "toista / tauko",
"hotkey_playbackPrevious": "edellinen ääniraita",
"hotkey_rate3": "arvostelu 3 tähteä",
"hotkey_playbackStop": "lopeta",
"hotkey_rate4": "arvostelu 4 tähteä",
"hotkey_rate1": "arvostelu 1 tähti",
"hotkey_rate2": "arvostelu 2 tähteä",
"hotkey_unfavoriteCurrentSong": "poista suosikeista $t(common.currentSong)",
"fontType_description": "sisäänrakennettu fontti valitsee yhden Feishinin tuomista fonteista. järjestelmän fontti antaa sinun valita minkä tahansa käyttöjärjestelmään asennetun fontin. mukautettu antaa sinun tuoda oman fontin",
"fontType_optionBuiltIn": "sisäänrakennettu fontti",
"fontType_optionSystem": "järjestelmän fontti",
"fontType_optionCustom": "mukautettu fontti",
"hotkey_favoriteCurrentSong": "lisää suosikiksi $t(common.currentSong)",
"hotkey_favoritePreviousSong": "lisää suosikiksi $t(common.previousSong)",
"hotkey_rate5": "arvostelu 5 tähteä",
"hotkey_skipBackward": "ohita taaksepäin",
"hotkey_skipForward": "ohita eteenpäin",
"font": "kirjaisin",
"font_description": "asettaa fontin jota sovellus käyttää",
"discordApplicationId": "{{discord}} sovelluksen tunnus",
"hotkey_globalSearch": "globaali haku",
"hotkey_playbackNext": "seuraava ääniraita",
"hotkey_browserForward": "selain eteenpäin",
"hotkey_playbackPause": "tauko",
"hotkey_localSearch": "hae sivulta",
"customFontPath": "mukautetun fontin polku",
"fontType": "fonttityyppi",
"hotkey_unfavoritePreviousSong": "poista suosikeista $t(common.previousSong)",
"customCss_description": "mukautettu CSS-sisältö. Huomautus: content- ja etä-URL-osoitteet ovat estettyjä ominaisuuksia. Esikatselu sisällöstäsi on alla. Lisäkenttiä, joita et ole määrittänyt, on näkyvissä puhdistuksen vuoksi.",
"customCssNotice": "Varoitus: vaikka jonkinlainen puhdistus onkin tehty (url()- ja content:-komentojen estäminen), mukautetun CSS:n käyttäminen voi silti aiheuttaa riskejä muuttamalla käyttöliittymää.",
"disableLibraryUpdateOnStartup": "poista uusimman version tarkistus käynnistyksen yhteydessä käytöstä",
"disableAutomaticUpdates": "poista automaattiset päivitykset käytöstä",
"discordIdleStatus": "näytä rich presencen käyttämätön tila",
"discordIdleStatus_description": "kun käytössä, päivitä tila kun soitin on käyttämättömänä",
"doubleClickBehavior": "lisää kaikki haetut kappaleet soittojonoon tuplaklikkauksella",
"discordUpdateInterval_description": "päivitysväli sekunnteina (vähintään 15 sekunttia)",
"discordRichPresence": "{{discord}} rich presence",
"discordRichPresence_description": "ota toiston tila käyttöön {{discord}}n rich presence-toiminnossa. Kuvakkeiden avaimet ovat {{icon}}, {{playing}} ja {{paused}}. ",
"discordUpdateInterval": "{{discord}} rich presencen päivitysväli",
"enableRemote": "aktivoi etäohjauspalvelin",
"externalLinks_description": "ottaa ulkoiset linkit (Last.fm, MusicBrainz) artistien/albumien sivuilla",
"exitToTray": "sulje tehtäväpalkkiin",
"doubleClickBehavior_description": "jos päällä, kaikki hakutuloksissa olevat kappaleet lisätään soittojonoon. muuten vain napsautettu kappale lisätään jonoon",
"discordApplicationId_description": "{{discord}}n ohjelma-ID rich presenceä varten (oletuksena {{defaultId}})",
"enableRemote_description": "aktivoi etäohjauspalvelimen, jolla muut laitteet voivat ohjata sovellusta",
"externalLinks": "näytä ulkoiset linkit",
"exitToTray_description": "sovellus suljetaan tehtäväpalkkiin",
"discordListening_description": "näytä status kuuntelee pelaa sijaan",
"discordListening": "näytä status kuuntelee",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"lastfmApiKey_description": "API-avain {{lastfm}}:lle. tarvitaan kansikuvia varten",
"passwordStore_description": "mitä salasanojen/avaimien tallennusta käytetään. muuta tätä, jos sinulla on ongelmia salasanojen tallennuksessa.",
"floatingQueueArea_description": "näyttää ikonin ikkunan oikealla reunalla jonon katselua varten",
"homeFeature_description": "ohjaa näytetäänkö suuri esittelykaruselli kotisivulla",
"hotkey_rate0": "arvostelun tyhjennys",
"hotkey_togglePreviousSongFavorite": "vaihda $t(common.previousSong) suosikkiasetus",
"imageAspectRatio_description": "jos käytössä, kansikuvat näytetään niiden alkuperäisellä kuvasuhteella. jos kuvasuhde ei ole 1:1, jäljelle jäävä tila jää tyhjäksi",
"language_description": "asettaa sovelluksen kielen $t(common.restartRequired)",
"lyricFetch": "hae sanoitukset internetistä",
"lyricFetchProvider_description": "valitse lähteet sanoituksien hakua varten. lähteiden järjestys on se järjestys, jossa ne tiedustellaan",
"minimumScrobblePercentage": "pienin skrobblauksen kesto (prosenttia)",
"mpvExecutablePath": "mpv:n suoritettavan tiedoston polku",
"mpvExecutablePath_description": "asettaa mpv:n suoritettavan tiedoston polun. ollessa tyhjä, käytetään oletuspolkua",
"mpvExtraParameters_help": "yksi per rivi",
"playButtonBehavior_optionPlay": "$t(player.play)",
"genreBehavior": "genre-sivun oletustoiminta",
"globalMediaHotkeys": "globaalit median pikanäppäimet",
"globalMediaHotkeys_description": "ota käyttöön tai poista käytöstä järjestelmän median pikanäppäinten käyttö toiston hallintaa",
"hotkey_toggleCurrentSongFavorite": "vaihda $t(common.currentSong) suosikkiasetus",
"imageAspectRatio": "käytä alkuperäistä kansikuvan kuvasuhdetta",
"language": "kieli",
"lyricOffset_description": "siirrä sanoituksia valitun ajan millisekuntteina",
"minimizeToTray": "pienennä ilmaisinalueelle",
"gaplessAudio_description": "asettaa tauottoman toiston asetukset mpv:hen",
"hotkey_volumeDown": "äänenvoimakkuuden vähentäminen",
"hotkey_zoomIn": "lähennä",
"lyricFetch_description": "hae sanoitukset eri lähteistä internetissä",
"lyricFetchProvider": "lähteet sanoituksia varten",
"lyricOffset": "sanotuksien siirto (ms)",
"mpvExtraParameters": "mpv:n parametrit",
"followLyric": "seuraa lyriikoita",
"followLyric_description": "vieritä lyriikat tämänhetkiseen paikkaan",
"hotkey_toggleQueue": "vaihda jono",
"minimumScrobblePercentage_description": "vähimmäisprosentti kappaleesta, joka on soitettava ennen kuin se skrobblataan",
"minimumScrobbleSeconds": "pienin skrobblaus (sekunttia)",
"minimumScrobbleSeconds_description": "vähimmäisaika kappaleesta, joka on soitettava ennen kuin se skrobblataan",
"passwordStore": "salasanojen/avaimien tallennus",
"hotkey_volumeUp": "äänenvoimakkuuden lisääminen",
"hotkey_toggleShuffle": "vaihda sekoitus",
"hotkey_volumeMute": "mykistäminen",
"lastfmApiKey": "{{lastfm}} API-avain",
"minimizeToTray_description": "pienennä sovellus ilmaisinalueelle",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"hotkey_zoomOut": "loitonna",
"floatingQueueArea": "näytä kelluvan jonon avausalue",
"homeFeature": "kodin esittelykaruselli",
"hotkey_toggleFullScreenPlayer": "vaihda kokonäytön toistin",
"hotkey_toggleRepeat": "vaihda kertaus",
"gaplessAudio": "tauoton toisto",
"transcodeFormat_description": "valitsee transkoodattavan formaatin. jätä tyhjäksi palvelimen valintaa varten",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"themeDark": "teema (tumma)",
"transcodeNote": "tulee voimaan 1 (web) - 2 (mpv) kappaleen jälkeen",
"translationApiKey_description": "API-avain käännöstä varten (tukee vain globaalia palvelun palvelupistettä)",
"playbackStyle_description": "valitse toiston tyyli, jota käytetään soittimessa",
"transcode_description": "ottaa transkoodaksen käyttöön eri formaateille",
"transcodeBitrate": "transkoodattava bittinopeus",
"translationApiProvider": "käännös-API:n palveluntarjoaja",
"trayEnabled_description": "näytä/piilota järjestelmäpalkin kuvake/valikko. jos poistettu käytöstä, myös pienennä/sulje järjestelmäpalkkiin -toiminto poistetaan käytöstä",
"windowBarStyle_description": "valitse ikkunapalkin tyyli",
"webAudio": "käytä web-ääntä",
"windowBarStyle": "ikkunapalkin tyyli",
"zoom": "zoomausprosentti",
"playbackStyle": "toiston tyyli",
"remotePassword": "kauko-ohjauspalvelimen salasana",
"remoteUsername_description": "asettaa käyttäjänimen kauko-ohjauspalvelimelle. jos sekä käyttäjätunnus, että salasana ovat tyhjänä, todennus poistetaan käytöstä",
"skipPlaylistPage": "ohita soittolistojen sivu",
"themeDark_description": "asettaa tumman teeman käytettäväksi sovelluksessa",
"playbackStyle_optionCrossFade": "ristivaihto",
"playbackStyle_optionNormal": "normaali",
"playButtonBehavior": "toistopainikkeen toiminta",
"playButtonBehavior_description": "asettaa toistopainikkeen oletustoiminnan lisättäessä kappaleita jonoon",
"remotePort": "kauko-ohjauspalvelimen portti",
"replayGainMode": "{{ReplayGain}} tila",
"sampleRate_description": "valitse käytettävä näytteenottotaajuus, jos valittu näytetaajuus poikkeaa nykyisen median taajuudesta. arvo, joka on alle 8 000, käyttää oletustaajuutta",
"skipDuration": "ohituksen kesto",
"sidePlayQueueStyle_description": "asettaa tyylin sivupalkin toistojonolle",
"sidePlayQueueStyle_optionAttached": "liitetty",
"sidePlayQueueStyle_optionDetached": "irrotettu",
"startMinimized_description": "käynnistä sovellus järjestelmäpalkissa",
"theme": "teema",
"useSystemTheme_description": "seuraa järjestelmän määrittämää asetusta vaalealle tai tummalle asetukselle",
"remoteUsername": "kauko-ohjauspalvelimen käyttäjänimi",
"remotePort_description": "asettaa kauko-ohjauspalvelimen portin",
"remotePassword_description": "asettaa kauko-ohjauspalvelimen salasanan. Nämä tunnukset siirretään oletuksena turvattomasti, joten sinun kuuluisi käyttää uniikkia salasanaa, josta et välitä",
"replayGainClipping": "{{ReplayGain}} leikkaus",
"replayGainClipping_description": "Estää {{ReplayGain}}n aiheuttaman leikkauksen laskemalla vahvistusta automaatisesti",
"replayGainFallback": "{{ReplayGain}} palautus",
"playerAlbumArtResolution_description": "suurien kansikuvien resoluutio soittimen esikatselussa. suurempi tekee niistä terävempiä, mutta voi hidastaa latausta. oletuksena on 0, joka tarkoittaa automaattista",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"replayGainPreamp": "{{ReplayGain}} esivahvistus (dB)",
"scrobble_description": "skrobblaa toistot mediapalvelimellesi",
"replayGainPreamp_description": "säätää esivahvistuksen määrää {{ReplayGain}} arvoon",
"showSkipButtons": "näytä ohituspainikkeet",
"showSkipButtons_description": "näytä tai piilota soitinpalkin ohituspainikkeet",
"showSkipButton": "näytä ohituspainikkeet",
"showSkipButton_description": "näytä tai piilota soitinpalkin ohituspainikkeet",
"sidebarPlaylistList": "sivupakin soittolistojen lista",
"skipDuration_description": "asettaa ohitettavan ajan käytettäessä soitinpalkin ohituspainikkeita",
"volumeWidth": "äänenvoimakkuuden säätimen leveys",
"sidebarCollapsedNavigation_description": "näytä tai piilota navigointi romautetussa sivupalkissa",
"sidebarConfiguration": "sivupalkin asetukset",
"sidebarConfiguration_description": "valitse kohteet ja niiden järjestys sivupalkissa",
"volumeWidth_description": "äänenvoimakkuuden säätimen leveys",
"playerAlbumArtResolution": "soittimen kansikuvien resoluutio",
"playerbarOpenDrawer": "toistipalkin kokoruudun kytkin",
"playerbarOpenDrawer_description": "sallii toistopalkin klikkaamisen avaamaan kokonäytön soittimen",
"replayGainFallback_description": "asetettava vahvistus desibelinä (dB), jos tiedostolla ei ole {{ReplayGain}} tageja",
"replayGainMode_description": "säätää äänenvoimmakkuutta {{ReplayGain}} arvojen mukaisesti tiedoston metadatasta",
"sampleRate": "näytteenottotaajuus",
"savePlayQueue": "tallenna toistojono",
"savePlayQueue_description": "tallenna toistojono, kun sovellus suljetaan ja avaa se uudestaan, kun sovellus avataan",
"scrobble": "skrobblaus",
"sidebarCollapsedNavigation": "sivupalkin (romautettu) navigointi",
"sidebarPlaylistList_description": "näytä tai piilota soittolistojen lista sivupalkissa",
"sidePlayQueueStyle": "sivupalkin jonon tyyli",
"skipPlaylistPage_description": "navigoidessa soittolistaan, mene soittolistan kappaleiden listaan oletussivun sijaan",
"theme_description": "asettaa ohjelmassa käytettävän teeman",
"themeLight": "teema (vaalea)",
"themeLight_description": "asettaa vaalean teeman käytettäväksi sovelluksessa",
"transcode": "ota transkoodaus käyttöön",
"transcodeBitrate_description": "valitsee transkoodattavan bittinopeuden. 0 tarkoittaa palvelimen valintaa",
"transcodeFormat": "transkoodattava formaatti",
"translationApiProvider_description": "palveluntarjoajan API käännöstä varten",
"translationApiKey": "käännöksen API-avain",
"translationTargetLanguage": "käännöksen kohdekieli",
"translationTargetLanguage_description": "kohdekieli käännöstä varten",
"trayEnabled": "näytä järjestelmäpalkki",
"volumeWheelStep_description": "äänenvoimakkuuden muutoksen suuruus rullattaessa hiiren rullalla äänenvoimakkuuden säätimen päällä",
"zoom_description": "asettaa sovelluksen zoomausprosentin",
"webAudio_description": "käytä web-ääntä. tämä mahdollistaa edistyneet ominaisuudet, kuten replaygainin. poista käytöstä, jos koet ongelmia",
"startMinimized": "käynnistä pienennettynä",
"useSystemTheme": "käytä järjestelmän teemaa",
"volumeWheelStep": "äänenvoimakkuusrullan askel"
},
"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"
},
"table": {
"config": {
"general": {
"gap": "$t(common.gap)",
"size": "$t(common.size)",
"autoFitColumns": "sovita sarakkeet",
"followCurrentSong": "seuraa nykyistä kappaletta",
"displayType": "näytön tyyppi",
"itemGap": "kohteiden väli (px)",
"itemSize": "kohteiden koko (px)",
"tableColumns": "taulukon sarakkeet"
},
"label": {
"channels": "$t(common.channel_other)",
"trackNumber": "raidan numero",
"album": "$t(entity.album_one)",
"actions": "$t(common.action_other)",
"codec": "$t(common.codec)",
"dateAdded": "lisäyspäivämäärä",
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"albumArtist": "$t(entity.albumArtist_one)",
"artist": "$t(entity.artist_one)",
"discNumber": "levyn numero",
"duration": "$t(common.duration)",
"favorite": "$t(common.favorite)",
"lastPlayed": "viimeksi soitettu",
"note": "$t(common.note)",
"titleCombined": "$t(common.title) (yhdistetty)",
"rowIndex": "rivin indeksi",
"biography": "$t(common.biography)",
"bitrate": "$t(common.bitrate)",
"bpm": "$t(common.bpm)",
"genre": "$t(entity.genre_one)",
"playCount": "toistojen lukumäärä",
"rating": "$t(common.rating)",
"releaseDate": "julkaisupäivämäärä",
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "$t(common.title)",
"year": "$t(common.year)"
},
"view": {
"table": "taulukko",
"card": "kortti",
"poster": "juliste"
}
},
"column": {
"releaseYear": "vuosi",
"bpm": "bpm",
"artist": "$t(entity.artist_one)",
"biography": "biografia",
"dateAdded": "lisäyspäivämäärä",
"album": "albumi",
"albumArtist": "albumin artisti",
"lastPlayed": "viimeksi toistettu",
"path": "polku",
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "nimi",
"trackNumber": "raita",
"codec": "$t(common.codec)",
"comment": "kommentti",
"albumCount": "$t(entity.album_other)",
"bitrate": "bittinopeus",
"channels": "$t(common.channel_other)",
"discNumber": "levy",
"favorite": "suosikki",
"genre": "$t(entity.genre_one)",
"playCount": "toistoja",
"rating": "arvostelu",
"releaseDate": "julkaisupäivämäärä"
}
}
}
+225 -58
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",
@@ -28,13 +28,15 @@
"mute": "muet",
"skip_forward": "avancer",
"pause": "pause",
"unfavorite": "dé-favori"
"unfavorite": "retirer des favoris",
"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)",
@@ -47,22 +49,27 @@
"moveToBottom": "déplacer en bas",
"setRating": "noter",
"toggleSmartPlaylistEditor": "basculer l'éditeur de $t(entity.smartPlaylist)",
"removeFromFavorites": "retirer des $t(entity.favorite_other)"
"removeFromFavorites": "retirer des $t(entity.favorite_other)",
"openIn": {
"lastfm": "Ouvrir dans Last.fm",
"musicbrainz": "Ouvrir dans MusicBrainz"
},
"moveToNext": "passer au suivant"
},
"common": {
"backward": "reculer",
"backward": "en arrière",
"increase": "augmenter",
"rating": "note",
"bpm": "bpm",
"refresh": "rafraichir",
"unknown": "inconnu",
"areYouSure": "êtes vous sûr ?",
"areYouSure": "êtes-vous sûr?",
"edit": "éditer",
"favorite": "favoris",
"left": "gauche",
"save": "sauvegarder",
"save": "enregistrer",
"right": "droite",
"currentSong": "actuelle $t(entity.track_one)",
"currentSong": "$t(entity.track_one) actuelle",
"collapse": "réduire",
"trackNumber": "piste",
"descending": "décroisant",
@@ -87,7 +94,7 @@
"no": "non",
"owner": "propriétaire",
"enable": "activer",
"clear": "effacer",
"clear": "vider",
"forward": "avancer",
"delete": "supprimer",
"cancel": "annuler",
@@ -101,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",
@@ -119,43 +126,57 @@
"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",
"size": "taille",
"biography": "biographie",
"note": "note"
"note": "note",
"albumGain": "gain de l'album",
"albumPeak": "crête de l'album",
"close": "fermer",
"mbid": "Identifiants MusicBrainz",
"preview": "aperçu",
"share": "partager",
"reload": "recharger",
"trackGain": "gain de la piste",
"trackPeak": "crête de la piste",
"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é",
@@ -172,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",
@@ -180,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",
@@ -209,7 +230,8 @@
"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": "partagé $t(entity.playlist_other)"
},
"fullscreenPlayer": {
"config": {
@@ -221,13 +243,18 @@
"unsynchronized": "désynchronisé",
"lyricAlignment": "alignement des paroles",
"useImageAspectRatio": "utiliser le ratio de l'image",
"opacity": "opacitée",
"opacity": "opacité",
"lyricSize": "Taille des paroles",
"lyricGap": "espacement des lettres"
"lyricGap": "espacement des lettres",
"dynamicIsImage": "activer l'image d'arrière-plan",
"dynamicImageBlur": "intensité de flou sur image d'arrière-plan",
"lyricOffset": "paroles décalées (ms)"
},
"upNext": "suivant",
"upNext": "à suivre",
"lyrics": "paroles",
"related": "similaire"
"related": "similaire",
"visualizer": "visualisateur",
"noLyrics": "aucune parole trouvée"
},
"appMenu": {
"selectServer": "sélectionner le serveur",
@@ -250,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érale",
"hotkeysTab": "raccourci",
"generalTab": "général",
"hotkeysTab": "raccourcis",
"windowTab": "fenêtre",
"playbackTab": "lecteur"
"playbackTab": "lecteur",
"advanced": "avancé"
},
"globalSearch": {
"commands": {
@@ -282,22 +311,61 @@
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"removeFromQueue": "$t(action.removeFromQueue)"
"removeFromQueue": "$t(action.removeFromQueue)",
"shareItem": "partager un élément",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"showDetails": "obtenir des informations",
"download": "télécharger",
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
"title": "$t(entity.genre_other)",
"showAlbums": "afficher $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "afficher $t(entity.genre_one) $t(entity.track_other)"
},
"trackList": {
"title": "$t(entity.track_other)"
"title": "$t(entity.track_other)",
"artistTracks": "pistes par {{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": "albums par {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
},
"albumArtistDetail": {
"about": "À propos de {{artist}}",
"appearsOn": "apparaît sur",
"topSongsFrom": "meilleures chansons de {{title}}",
"viewAll": "voir tout",
"viewAllTracks": "voir tout $t(entity.track_other)",
"recentReleases": "sorties récentes",
"viewDiscography": "voir la discographie",
"relatedArtists": "en rapport avec $t(entity.artist_other)",
"topSongs": "meilleures chansons"
},
"itemDetail": {
"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": {
@@ -320,7 +388,6 @@
"remotePort_description": "définit le port du serveur de contrôle à distance",
"hotkey_skipBackward": "reculer",
"hotkey_playbackPause": "pause",
"mpvExecutablePath_help": "line par line",
"hotkey_volumeUp": "monter le volume",
"discordIdleStatus_description": "quand activé, mettre à jour le status pendant que le lecteur est inactif",
"showSkipButtons": "affiche les boutons suivants et précédents",
@@ -329,10 +396,10 @@
"scrobble": "scrobble",
"enableRemote_description": "activer le serveur de contrôle à distance, qui permet à d'autres appareils de contrôler l'application",
"fontType_optionSystem": "police système",
"mpvExecutablePath_description": "définit le chemin vers l'exécutable mpv",
"mpvExecutablePath_description": "définit le chemin vers l'exécutable mpv, si vide, le chemin par défaut sera utilisé",
"hotkey_favoriteCurrentSong": "favori $t(common.currentSong)",
"sampleRate": "taux d'échantillonnage",
"sampleRate_description": "sélectionnez le taux d'échantillonnage de sortie utilisé si la fréquence d'échantillonnage sélectionnée est différente du média actuel",
"sampleRate_description": "sélectionne le taux d'échantillonnage de sortie utilisé si la fréquence d'échantillonnage sélectionnée est différente de celle du média actuel. une valeur inférieure à 8000 utilisera la fréquence par défaut",
"hotkey_zoomIn": "zoom avant",
"scrobble_description": "scrobble les lectures à votre serveur multimédia",
"hotkey_browserForward": "avancer",
@@ -341,7 +408,7 @@
"hotkey_playbackPlayPause": "lecture / pause",
"hotkey_rate1": "noter 1 étoile",
"hotkey_skipForward": "avancer",
"disableLibraryUpdateOnStartup": "désactive la vérification de mise à jour au démarrage",
"disableLibraryUpdateOnStartup": "désactive la recherche de mise à jour au démarrage",
"gaplessAudio": "audio sans interruption",
"minimizeToTray_description": "réduit l'application vers la barre des tâches",
"hotkey_playbackPlay": "lecture",
@@ -360,7 +427,7 @@
"fontType_optionCustom": "police personnalisée",
"remotePassword": "mot de passe du serveur de contrôle à distance",
"lyricFetchProvider": "fournisseur depuis lequel récupérer les paroles",
"language_description": "définit la langue de l'application $t(common.restartRequired)",
"language_description": "définit la langue de l'application ($t(common.restartRequired))",
"playbackStyle_optionCrossFade": "fondu enchaîné",
"hotkey_rate3": "noter 3 étoiles",
"font": "police",
@@ -372,7 +439,7 @@
"hotkey_rate5": "noter 5 étoiles",
"hotkey_playbackPrevious": "piste précédente",
"showSkipButtons_description": "affiche ou cache les boutons suivants et précédents de la barre de lecture",
"language": "language",
"language": "langage",
"playbackStyle": "style de lecture",
"hotkey_toggleShuffle": "basculer la lecture aléatoire",
"playbackStyle_description": "sélectionnez le style de lecture à utiliser pour le lecteur audio",
@@ -400,12 +467,12 @@
"lyricFetchProvider_description": "sélectionnez le fournisseur auprès desquels récupérer les paroles. l'ordre des fournisseurs et l'ordre dans lequel ils seront interrogés",
"globalMediaHotkeys_description": "active ou désactive l'utilisation des raccourcis clavier multimédia système pour contrôler la lecture",
"followLyric": "suivre les paroles actuelles",
"discordIdleStatus": "afficher l'état d'inactivité dans le status de l'activité",
"discordIdleStatus": "afficher l'état d'inactivité dans le statut de l'activité",
"hotkey_zoomOut": "zoom arrière",
"hotkey_unfavoriteCurrentSong": "favorisé $t(common.currentSong)",
"hotkey_unfavoriteCurrentSong": "retirer des favoris la $t(common.currentSong)",
"hotkey_rate0": "supprimer la note",
"hotkey_volumeMute": "couper le son",
"hotkey_toggleCurrentSongFavorite": "basculer $t(common.currentSong) favori",
"hotkey_toggleCurrentSongFavorite": "basculer favori de la $t(common.currentSong)",
"remoteUsername": "nom d'utilisateur du serveur de contrôle à distance",
"hotkey_browserBack": "retour arrière",
"showSkipButton": "affiche les boutons suivants et précédents",
@@ -414,34 +481,34 @@
"minimumScrobbleSeconds": "scrobble minimum (secondes)",
"hotkey_playbackStop": "stop",
"font_description": "définit la police à utiliser pour l'application",
"savePlayQueue_description": "sauvegarde la liste de lecture quand l'application est fermée et restaure quand l'application est ouverte",
"savePlayQueue_description": "sauvegarde la liste de lecture quand l'application est fermée et la restaure quand l'application est ouverte",
"sidebarCollapsedNavigation_description": "affiche ou cache la navigation dans la barre latérale réduite",
"sidebarConfiguration": "configuration de la barre latérale",
"sidebarConfiguration_description": "sélectionnez les items et l'ordre dans lesquels ils seront affichaient dans la barre latérale",
"sidebarConfiguration_description": "sélectionnez les éléments et l'ordre dans lequel ils seront affichés dans la barre latérale",
"sidebarPlaylistList": "liste de playlist de la barre latérale",
"sidebarCollapsedNavigation": "navigation de la barre latéral (réduite)",
"skipDuration": "temps de l'avance rapide",
"skipDuration": "durée de l'avance rapide",
"sidePlayQueueStyle_optionAttached": "attaché",
"sidePlayQueueStyle": "style de la liste de lecture latérale",
"sidebarPlaylistList_description": "affiche ou cache la liste de playlist de la barre latérale",
"sidePlayQueueStyle_description": "définit le style de la liste de lecture latérale",
"sidePlayQueueStyle_optionDetached": "détaché",
"volumeWheelStep_description": "la quantité de volume à modifier lors du défilement de la molette de la souris sur le curseur de volume",
"volumeWheelStep_description": "la valeur de volume à modifier lors du défilement de la molette de la souris sur le curseur de volume",
"theme_description": "définit le thème à utiliser pour l'application",
"skipDuration_description": "définit le durée de l'avance rapide, lors de l'utilisation des boutons skip dans la barre de lecture",
"skipDuration_description": "définit le durée du saut rapide, lors de l'utilisation des boutons avancer/reculer de la barre de lecture",
"themeLight": "thème (clair)",
"zoom": "pourcentage de zoom",
"themeDark_description": "définit le thème sombre à utiliser pour l'application",
"themeLight_description": "définit le thème clair à utiliser pour l'application",
"zoom_description": "définit le pourcentage de zoom de l'application",
"theme": "thème",
"skipPlaylistPage_description": "lors de la navigation dans une playlist, aller directement vers le liste des morceaux, au lieu de la page par défaut",
"volumeWheelStep": "marche du curseur de volume",
"skipPlaylistPage_description": "lors de la navigation dans une playlist, aller directement vers la liste des morceaux, au lieu de la page par défaut",
"volumeWheelStep": "valeur du pas de volume",
"windowBarStyle": "style de la barre de la fenêtre",
"useSystemTheme_description": "suivre les préférence du système (sombre ou clair)",
"useSystemTheme_description": "suivre les préférences du système (mode clair ou sombre)",
"skipPlaylistPage": "sauter la page de playlist",
"themeDark": "thème (sombre)",
"windowBarStyle_description": "sélectionner le style de la barre de la fenêtre",
"windowBarStyle_description": "ajuster le style de la barre de la fenêtre",
"useSystemTheme": "utiliser le thème du système",
"discordApplicationId_description": "l'identifiant de l'application pour le statut d'activité {{discord}} (par défaut à {{defaultId}})",
"audioExclusiveMode": "mode de sortie audio exclusif",
@@ -453,7 +520,80 @@
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)"
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"replayGainMode_description": "ajuste le gain de volume accordement à la valeur de {{ReplayGain}} sauvegardé dans les métadonnées du fichier",
"replayGainFallback": "{{ReplayGain}} fallback",
"replayGainClipping_description": "Prévient le clipping causé par {{ReplayGain}} en baissant automatiquement le gain",
"replayGainPreamp": "préamplificateur (dB) de {{ReplayGain}}",
"replayGainClipping": "{{ReplayGain}} clipping",
"replayGainMode": "mode de {{ReplayGain}}",
"replayGainFallback_description": "gain en dB à appliquer si le fichier n'a pas de tag {{ReplayGain}}",
"replayGainPreamp_description": "ajuste le gain de préampli appliqué a la valeur de {{ReplayGain}}",
"clearQueryCache": "vide le cache de feishin",
"clearCache": "vider le cache navigateur",
"buttonSize_description": "la taille des boutons de la barre de lecture",
"clearQueryCache_description": "un 'soft clear' de feishin. cela actualisera les playlists, les métadonnées des pistes, et réinitialisera les paroles enregistrées. les paramètres, identifiants serveurs et les images mises en cache sont conservés",
"clearCache_description": "un 'hard clear' de feishin. en plus de vider le cache de feishin, vide le cache du navigateur (images sauvegardées et autres ressources). les identifiants serveurs et paramètres sont conservés",
"buttonSize": "taille des boutons du lecteur",
"clearCacheSuccess": "le cache a été vidé",
"externalLinks_description": "activer l'affichage de liens externes (Last.fm, MusicBrainz) sur la page de l'artiste/album",
"genreBehavior": "comportement par défaut de la page des genres",
"startMinimized_description": "démarrer l'application dans la barre des tâches",
"externalLinks": "afficher les liens externes",
"homeConfiguration": "configuration de la page d'accueil",
"homeFeature": "carrousel de la page d'accueil",
"homeFeature_description": "active ou désactive le carrousel sur la page d'accueil",
"imageAspectRatio": "utiliser le rapport hauteur/largeur natif de la pochette",
"imageAspectRatio_description": "si cette option est activée, les pochettes seront affichées en utilisant leur rapport hauteur/largeur natif. pour les pochettes qui n'ont pas un rapport 1:1 (carré), l'espace restant sera vide",
"mpvExtraParameters_help": "un par ligne",
"passwordStore_description": "quel mot de passe utiliser. changez cela si vous rencontrez des problèmes pour stocker les mots de passe.",
"playerAlbumArtResolution": "résolution de la pochette de l'album du lecteur",
"passwordStore": "mots de passe",
"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",
"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": {
@@ -475,7 +615,7 @@
"error_savePassword": "une erreur sest produite lors de la tentative de sauvegarde du mot de passe"
},
"addToPlaylist": {
"success": "{{message}} $t(entity.song_other) ajouté à {{numOfPlaylists}} $t(entity.playlist_other)",
"success": "$t(entity.trackWithCount, {\"count\": {{message}} }) ajouté à $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "ajouter à $t(entity.playlist_one)",
"input_skipDuplicates": "sauter les doublons",
"input_playlists": "$t(entity.playlist_other)"
@@ -497,12 +637,22 @@
"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",
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)"
},
"shareItem": {
"allowDownloading": "autoriser le téléchargement",
"description": "description",
"setExpiration": "définir une expiration",
"success": "lien de partage copié dans le presse-papier (ou cliquez ici pour ouvrir)",
"expireInvalid": "l'expiration doit être définie à une date ultérieure",
"createFailed": "échec de la création du lien de partage (le partage est-il activé ?)"
}
},
"entity": {
@@ -551,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": {
@@ -560,7 +716,10 @@
"tableColumns": "colonnes de la liste",
"autoFitColumns": "colonnes à ajustement automatique",
"gap": "$t(common.gap)",
"size": "$t(common.size)"
"size": "$t(common.size)",
"itemGap": "écart entre les éléments (en pixel)",
"itemSize": "taille des élements (en pixel)",
"followCurrentSong": "suivre la chanson actuelle"
},
"view": {
"table": "liste",
@@ -589,7 +748,13 @@
"rating": "$t(common.rating)",
"note": "$t(common.note)",
"owner": "$t(common.owner)",
"path": "$t(common.path)"
"path": "$t(common.path)",
"title": "$t(common.title)",
"size": "$t(common.size)",
"genre": "$t(entity.genre_one)",
"year": "$t(common.year)",
"songCount": "$t(entity.track_other)",
"codec": "$t(common.codec)"
}
},
"column": {
@@ -614,7 +779,9 @@
"artist": "$t(entity.artist_one)",
"genre": "$t(entity.genre_one)",
"songCount": "$t(entity.track_other)",
"channels": "$t(common.channel_other)"
"channels": "$t(common.channel_other)",
"size": "$t(common.size)",
"codec": "$t(common.codec)"
}
}
}
+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"
}
}
}
}
+37 -17
View File
@@ -46,7 +46,7 @@
"left": "sinistra",
"save": "salva",
"right": "destra",
"currentSong": "$t(entity.track_one) corrent",
"currentSong": "$t(entity.track_one) corrente",
"trackNumber": "traccia",
"descending": "decrescente",
"gap": "gap",
@@ -102,9 +102,9 @@
"note": "nota"
},
"player": {
"repeat_all": "ripeti tutto",
"repeat_all": "ripeti coda",
"stop": "ferma",
"repeat": "ripeti",
"repeat": "ripeti traccia",
"queue_remove": "rimuovi selezionati",
"playRandom": "riproduci casuale",
"skip": "salta",
@@ -113,21 +113,21 @@
"skip_back": "salta indietro",
"favorite": "preferito",
"next": "successivo",
"shuffle": "mischia",
"shuffle": "mescola",
"playbackFetchNoResults": "nessuna canzone trovata",
"playbackFetchInProgress": "caricamento canzoni…",
"addNext": "aggiungi successivo",
"playbackSpeed": "velocità riproduzione",
"playbackSpeed": "velocità di riproduzione",
"playbackFetchCancel": "ci sta mettendo un po'... chiudi la notifica per annullare",
"play": "riproduci",
"repeat_off": "ripeti disabilitato",
"repeat_off": "non ripetere",
"pause": "pausa",
"queue_clear": "cancella coda",
"muted": "silenziato",
"unfavorite": "togli dai preferiti",
"queue_moveToTop": "sposta selezionati in fondo",
"queue_moveToBottom": "sposta selezionati in cima",
"shuffle_off": "mischia disabilitato",
"shuffle_off": "non mescolare",
"addLast": "aggiungi in coda",
"mute": "silenzia",
"skip_forward": "salta avanti"
@@ -140,7 +140,6 @@
"audioDevice_description": "seleziona il device audioda usare per la riproduzione (solo web player)",
"theme_description": "imposta il tema da usare per l'applicazione",
"hotkey_playbackPause": "pausa",
"mpvExecutablePath_help": "uno per linea",
"hotkey_volumeUp": "alza volume",
"skipDuration": "salta durata",
"discordIdleStatus_description": "quando è attivo, aggiorna lo stato mentre il player è inattivo",
@@ -156,8 +155,8 @@
"crossfadeStyle": "stile dissolvenza",
"sidebarConfiguration": "configurazione barra laterale",
"replayGainMode_optionNone": "$t(common.none)",
"hotkey_zoomIn": "ingrandisci",
"scrobble_description": "esegui scrobble delle riproduzioni al tuo media server",
"hotkey_zoomIn": "ingrandisci layout",
"scrobble_description": "invia lo scrobble delle riproduzioni al tuo media server",
"audioExclusiveMode_description": "abilità modalità output esclusiva. In questa modalità il sistema è di solito chiuso fuori, e solo mpv potrà riprodurre audio",
"discordUpdateInterval": "intervallo aggiornamento stato attività {{discord}}",
"themeLight": "tema (chiaro)",
@@ -207,7 +206,7 @@
"crossfadeDuration_description": "imposta la durata dell'effetto di dissolvenza",
"language": "lingua",
"playbackStyle": "stile riproduzione",
"hotkey_toggleShuffle": "attiva/disattiva mischia",
"hotkey_toggleShuffle": "attiva/disattiva mescolamento",
"theme": "tema",
"playbackStyle_description": "selezione lo stile di riproduzione da usare per il player audio",
"discordRichPresence_description": "abilita lo status del playback nello stato attività di {{discord}}. Le chiavi immagine sono: {{icon}}, {{playing}} e {{paused}} ",
@@ -247,7 +246,7 @@
"crossfadeDuration": "durata dissolvenza",
"discordIdleStatus": "visualizza lo stato attività in stato inattivo",
"audioPlayer": "player audio",
"hotkey_zoomOut": "rimpicciolisci",
"hotkey_zoomOut": "rimpicciolisci layout",
"hotkey_rate0": "rimuovi voto",
"discordApplicationId": "application id {{discord}}",
"applicationHotkeys_description": "configura tasti a scelta rapida dell'applicazione. attiva/disattiva la casella per impostare un tasto a scelta rapida globale (solo desktop)",
@@ -275,7 +274,26 @@
"showSkipButton_description": "mostra o nascondi i pulsanti per saltare nella barra del player",
"hotkey_unfavoriteCurrentSong": "rimuovi $t(common.currentSong) dai preferiti",
"hotkey_toggleCurrentSongFavorite": "imposta/rimuovi $t(common.currentSong) favorito",
"showSkipButton": "mostra pulsanti per saltare"
"showSkipButton": "mostra pulsanti per saltare",
"hotkey_browserForward": "Vai avanti di una pagina",
"hotkey_browserBack": "Torna indietro di una pagina",
"sidebarCollapsedNavigation_description": "mostra o nascondi la navigazione nella barra laterale collassata",
"replayGainClipping_description": "Previeni il clipping causato da {{ReplayGain}} abbassando automaticamente il gain",
"replayGainPreamp": "preamplificazione {{ReplayGain}} (dB)",
"sidePlayQueueStyle": "stile della coda di riproduzione laterale",
"showSkipButtons_description": "mostra o nascondi i pulsanti per saltare dalla barra di riproduzione",
"skipPlaylistPage_description": "quando si naviga in una playlist, si va alla pagina dell'elenco dei brani della playlist invece che alla pagina predefinita",
"sidePlayQueueStyle_description": "imposta lo stile della coda di riproduzione laterale",
"replayGainMode": "modalità {{ReplayGain}}",
"replayGainFallback_description": "gain in db da applicare se il file non possiede tag {{ReplayGain}}",
"replayGainPreamp_description": "aggiusta la preamplificazione del gain applicato sui valori {{ReplayGain}}",
"skipPlaylistPage": "Salta la pagina playlist",
"sidebarCollapsedNavigation": "navigazione con barra laterale (collassata)",
"clearCache_description": "pulitura \"forzata\" di feishin. Oltre a pulire la cache di feishin, elimina la cache del browser(immagini salvate e altri elementi). credenziali e impostazioni del server saranno mantenute",
"clearQueryCache": "pulisci cache di feishin",
"buttonSize_description": "Dimensione bottoni nella barra di riproduzione",
"clearCache": "pulisci la cache del browser",
"clearQueryCache_description": "\"leggera\" pulizia di feishin. verranno aggiornate le playlist, metadata delle tracce e i testi salvati. impostazioni, credenziali del server e le immagini salvate saranno mantenute"
},
"error": {
"remotePortWarning": "riavvia il server per applicare la nuova porta",
@@ -378,7 +396,7 @@
"selectServer": "seleziona server",
"version": "versione {{version}}",
"settings": "$t(common.setting_other)",
"manageServers": "gestisci sever",
"manageServers": "gestisci server",
"expandSidebar": "espandi barra laterale",
"collapseSidebar": "collassa barra laterale",
"openBrowserDevtools": "apri devtools browser",
@@ -473,7 +491,7 @@
"error_savePassword": "si è verificato un errore quando si è provato a salvare la password"
},
"addToPlaylist": {
"success": "aggiunto {{message}} $t(entity.song_other) a {{numOfPlaylists}} $t(entity.playlist_other)",
"success": "aggiunto {{message}} $t(entity.track_other) a {{numOfPlaylists}} $t(entity.playlist_other)",
"title": "aggiungi a $t(entity.playlist_one)",
"input_skipDuplicates": "salta duplicati",
"input_playlists": "$t(entity.playlist_other)"
@@ -505,7 +523,8 @@
"size": "$t(common.size)"
},
"view": {
"table": "tabella"
"table": "tabella",
"card": "Scheda"
},
"label": {
"releaseDate": "data rilascio",
@@ -558,7 +577,8 @@
"albumArtist": "artista album",
"path": "percorso",
"discNumber": "disco",
"channels": "$t(common.channel_other)"
"channels": "$t(common.channel_other)",
"size": "$t(common.size)"
}
},
"entity": {
+3 -3
View File
@@ -41,7 +41,6 @@
"hotkey_playbackPause": "一時停止",
"replayGainFallback": "{{ReplayGain}} フォールバック",
"sidebarCollapsedNavigation_description": "折りたたみサイドバーのナビゲーションを表示/非表示にします",
"mpvExecutablePath_help": "1行ごとに1アイテム",
"hotkey_volumeUp": "音量を上げる",
"skipDuration": "スキップの長さ",
"discordIdleStatus_description": "プレイヤーがアイドル状態でもステータスを更新します",
@@ -354,7 +353,8 @@
"albumArtist": "アルバムアーティスト",
"path": "パス",
"discNumber": "ディスク",
"channels": "$t(common.channel_other)"
"channels": "$t(common.channel_other)",
"size": "$t(common.size)"
}
},
"error": {
@@ -553,7 +553,7 @@
"error_savePassword": "パスワードを保存する際にエラーが発生しました"
},
"addToPlaylist": {
"success": "{{message}} $t(entity.song_other) を {{numOfPlaylists}} $t(entity.playlist_other) に追加しました",
"success": "{{message}} $t(entity.track_other) を {{numOfPlaylists}} $t(entity.playlist_other) に追加しました",
"title": "$t(entity.playlist_one) に追加",
"input_skipDuplicates": "重複をスキップ",
"input_playlists": "$t(entity.playlist_other)"
+286
View File
@@ -0,0 +1,286 @@
{
"action": {
"createPlaylist": "$t(entity.playlist_one) 생성",
"addToFavorites": "$t(entity.favorite_other)에 추가",
"addToPlaylist": "$t(entity.playlist_one)에 추가",
"clearQueue": "대기열 지우기",
"deletePlaylist": "$t(entity.playlist_one) 삭제",
"deselectAll": "모두 선택 해제",
"editPlaylist": "$t(entity.playlist_one) 편집",
"goToPage": "페이지 이동",
"moveToBottom": "맨 아래로 이동",
"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"
}
}
+322
View File
@@ -0,0 +1,322 @@
{
"action": {
"editPlaylist": "pas $t(entity.playlist_one) aan",
"goToPage": "ga naar pagina",
"moveToTop": "verplaats naar boven",
"addToFavorites": "toevoegen aan $t(entity.favorite_other)",
"addToPlaylist": "toevoegen aan $t(entity.playlist_one)",
"createPlaylist": "maak $t(entity.playlist_one)",
"removeFromPlaylist": "verwijder van $t(entity.playlist_one)",
"viewPlaylists": "bekijk $t(entity.playlist_other)",
"refresh": "$t(common.refresh)",
"deletePlaylist": "verwijder $t(entity.playlist_one)",
"removeFromQueue": "verwijder van lijst",
"deselectAll": "deselecteer alles",
"moveToBottom": "verplaats naar bodem",
"setRating": "selecteer rating",
"toggleSmartPlaylistEditor": "editor $t(entity.smartPlaylist) schakelen",
"removeFromFavorites": "verwijder van $t(entity.favorite_other)",
"clearQueue": "verwijder lijst",
"openIn": {
"lastfm": "Open in Last.fm",
"musicbrainz": "Open in MusicBrainz"
}
},
"common": {
"backward": "achteruit",
"increase": "verhogen",
"rating": "rating",
"bpm": "bpm",
"areYouSure": "weet je het zeker?",
"edit": "aanpassen",
"favorite": "favoriet",
"left": "links",
"currentSong": "huidig $t(entity.track_one)",
"collapse": "samenvouwen",
"descending": "aflopend",
"add": "toevoegen",
"gap": "gat",
"ascending": "oplopend",
"dismiss": "negeren",
"manage": "beheren",
"limit": "limiet",
"minimize": "minimaliseren",
"modified": "aangepast",
"duration": "duur",
"name": "naam",
"maximize": "maximaliseren",
"decrease": "verminder",
"ok": "ok",
"description": "beschrijving",
"configure": "configureren",
"path": "pad",
"center": "centreren",
"no": "nee",
"owner": "eigenaar",
"enable": "activeren",
"clear": "opschonen",
"forward": "vooruit",
"delete": "verwijder",
"cancel": "annuleer",
"forceRestartRequired": "herstart om aanpassingen toe te passen... wanneer de notificatie gesloten wordt zal de applicatie herstarten",
"filter_one": "filter",
"filter_other": "filters",
"filters": "filters",
"create": "aanmaken",
"bitrate": "bitrate",
"action_one": "actie",
"action_other": "acties",
"playerMustBePaused": "player moet gepauzeerd zijn",
"confirm": "bevestig",
"home": "home",
"comingSoon": "komt binnenkort…",
"channel_one": "kanaal",
"channel_other": "kanalen",
"disable": "deactiveren",
"none": "geen",
"menu": "menu",
"previousSong": "vorige $t(entity.track_one)",
"noResultsFromQuery": "de zoekopdracht leverde geen resultaten op",
"quit": "sluiten",
"expand": "vergroten",
"disc": "disk",
"random": "willekeurig",
"biography": "biografie",
"note": "Opmerking",
"refresh": "verversen",
"unknown": "onbekend",
"save": "opslaan",
"right": "rechts",
"trackNumber": "track",
"year": "jaar",
"version": "versie",
"title": "titel",
"saveAndReplace": "opslaan en vervangen",
"resetToDefault": "herstellen naar standaard",
"reset": "terugzetten",
"sortOrder": "volgorde",
"restartRequired": "herstart is nodig",
"search": "zoeken",
"saveAs": "opslaan als",
"yes": "ja",
"size": "grootte",
"reload": "herlaad",
"setting": "instelling",
"close": "sluiten"
},
"filter": {
"rating": "rating",
"communityRating": "community rating",
"criticRating": "criticus rating",
"mostPlayed": "meest gespeeld",
"comment": "commentaar",
"playCount": "aantal keer afgespeeld",
"recentlyUpdated": "recentelijk geüpdate",
"channels": "$t(common.channel_other)",
"isCompilation": "is compilatie",
"recentlyPlayed": "recentelijk afgespeeld",
"isRated": "is rated",
"owner": "$t(common.owner)",
"bitrate": "bitrate",
"genre": "$t(entity.genre_one)",
"recentlyAdded": "recentelijk toegevoegd",
"note": "notitie",
"name": "naam",
"dateAdded": "datum toegevoegd",
"albumCount": "$t(entity.album_other) totaal",
"path": "pad",
"favorited": "favoriet",
"albumArtist": "$t(entity.albumArtist_one)",
"isRecentlyPlayed": "is recentelijk afgespeeld",
"isFavorited": "is favoriet",
"bpm": "bpm",
"id": "id",
"disc": "disk",
"biography": "biografie",
"artist": "$t(entity.artist_one)",
"duration": "duratie",
"isPublic": "is publiek",
"random": "willekeurig",
"lastPlayed": "laatst gespeeld",
"fromYear": "van jaar",
"album": "$t(entity.album_one)",
"title": "titel",
"search": "zoeken",
"releaseDate": "releasedatum",
"releaseYear": "release jaar",
"songCount": "aantal nummers",
"toYear": "tot jaar",
"trackNumber": "track"
},
"page": {
"contextMenu": {
"setRating": "$t(action.setRating)",
"addToPlaylist": "$t(action.addToPlaylist)",
"addToFavorites": "$t(action.addToFavorites)",
"moveToTop": "$t(action.moveToTop)",
"deletePlaylist": "$t(action.deletePlaylist)",
"moveToBottom": "$t(action.moveToBottom)",
"createPlaylist": "$t(action.createPlaylist)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"addNext": "$t(player.addNext)",
"deselectAll": "$t(action.deselectAll)",
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"numberSelected": "{{count}} geselecteerd",
"removeFromQueue": "$t(action.removeFromQueue)"
},
"appMenu": {
"selectServer": "selecteer server",
"version": "versie {{version}}",
"settings": "$t(common.setting_other)",
"manageServers": "beheer servers",
"expandSidebar": "sidebar uitklappen",
"collapseSidebar": "sidebar inklappen",
"openBrowserDevtools": "open browser devtools",
"quit": "$t(common.quit)",
"goBack": "terug",
"goForward": "vooruit"
},
"albumDetail": {
"moreFromArtist": "meer van deze $t(entity.artist_one)",
"moreFromGeneric": "meer van {{item}}"
},
"fullscreenPlayer": {
"config": {
"dynamicBackground": "dynamische achtergrond",
"followCurrentLyric": "volg de actuele songtekst",
"opacity": "opaciteit",
"lyricSize": "tekstgrootte",
"lyricAlignment": "songtekst uitlijning",
"lyricGap": "tekstkloof"
}
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
}
},
"error": {
"remotePortWarning": "herstart de server om de nieuwe poort in te stellen",
"systemFontError": "er is iets fout gegaan tijdens het verkrijgen van systeem fonts",
"playbackError": "er is iets fout gegaan bij het afspelen van de media",
"endpointNotImplementedError": "endpoint {{endpoint}} is niet geïmplementeerd voor {{serverType}}",
"remotePortError": "er is iets fout gegaan tijdens het selecteren van de remote server",
"serverRequired": "server vereist",
"authenticationFailed": "authenticatie mislukt",
"apiRouteError": "verzoek kan niet doorgestuurd worden",
"genericError": "er is iets fout gegaan",
"credentialsRequired": "inloggegevens vereist",
"sessionExpiredError": "jouw sessie is verlopen",
"remoteEnableError": "er is iets fout gegaan tijdens het $t(common.enable) van de remote server",
"localFontAccessDenied": "toegang geweigerd tot lokale fonts",
"serverNotSelectedError": "geen server geselecteerd",
"remoteDisableError": "er is iets fout gegaan tijdens het $t(common.disable) van de remote server",
"mpvRequired": "MPV vereist",
"audioDeviceFetchError": "er is iets mis gegaan met het ophalen van de audioapparaten",
"invalidServer": "ongeldige server",
"loginRateError": "te veel login pogingen, probeer het opnieuw in een paar seconde"
},
"entity": {
"genre_one": "genre",
"genre_other": "genres",
"playlistWithCount_one": "{{count}} afspeellijst",
"playlistWithCount_other": "{{count}} afspeellijsten",
"playlist_one": "afspeellijst",
"playlist_other": "afspeellijsten",
"artist_one": "artiest",
"artist_other": "artiesten",
"folderWithCount_one": "{{count}} folder",
"folderWithCount_other": "{{count}} folders",
"albumArtist_one": "album artiest",
"albumArtist_other": "album artiesten",
"track_one": "track",
"track_other": "tracks",
"albumArtistCount_one": "{{count}} album artiest",
"albumArtistCount_other": "{{count}} album artiesten",
"albumWithCount_one": "{{count}} album",
"albumWithCount_other": "{{count}} albums",
"favorite_one": "favoriet",
"favorite_other": "favorieten",
"artistWithCount_one": "{{count}} artiest",
"artistWithCount_other": "{{count}} artiesten",
"folder_one": "folder",
"folder_other": "folders",
"smartPlaylist": "smart $t(entity.playlist_one)",
"album_one": "album",
"album_other": "albums",
"genreWithCount_one": "{{count}} genre",
"genreWithCount_other": "{{count}} genres",
"trackWithCount_one": "{{count}} track",
"trackWithCount_other": "{{count}} tracks"
},
"table": {
"column": {
"rating": "rating",
"size": "$t(common.size)"
},
"config": {
"label": {
"rating": "$t(common.rating)"
}
}
},
"setting": {
"hotkey_rate5": "rating 5 sterren",
"hotkey_rate4": "rating 4 sterren"
},
"form": {
"addServer": {
"title": "server toevoegen",
"input_username": "gebruikersnaam",
"input_url": "url",
"input_password": "wachtwoord",
"input_legacyAuthentication": "activeer legacy authenticatie",
"input_name": "server naam",
"success": "server met succes toegevoegd",
"input_savePassword": "wachtwoord opslaan",
"ignoreSsl": "negeer ssl $t(common.restartRequired)",
"ignoreCors": "negeer cors $t(common.restartRequired)",
"error_savePassword": "er is iets mis gegaan met het opslaan van het wachtwoord"
},
"deletePlaylist": {
"title": "verwijder $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) succesvol verwijdert",
"input_confirm": "Typ de naam van $t(entity.playlist_one) om te bevestigen"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"title": "$t(entity.playlist_one) aanmaken",
"input_public": "publiek",
"input_name": "$t(common.name)",
"success": "$t(entity.playlist_one) aangemaakt",
"input_owner": "$t(common.owner)"
},
"addToPlaylist": {
"success": "{{message}}$t(entity.song_other) aan {{numOfPlaylists}} $t(entity.playlist_other) toegevoegd",
"title": "aan $t(entity.playlist_one) toevoegen",
"input_skipDuplicates": "duplicaten overslaan",
"input_playlists": "$t(entity.playlist_other)"
},
"queryEditor": {
"input_optionMatchAll": "alles matchen",
"input_optionMatchAny": "elke match"
},
"lyricSearch": {
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)",
"title": "tekst zoeken"
},
"editPlaylist": {
"title": "$t(entity.playlist_one) aanpassen"
},
"updateServer": {
"title": "update server",
"success": "server succesvol geüpdatet"
}
}
}
+190 -35
View File
@@ -16,7 +16,12 @@
"createPlaylist": "utwórz $t(entity.playlist_one)",
"deletePlaylist": "usuń $t(entity.playlist_one)",
"moveToBottom": "przesuń na dół",
"setRating": "oceń"
"setRating": "oceń",
"openIn": {
"lastfm": "Otwórz w Last.fm",
"musicbrainz": "Otwórz w MusicBrainz"
},
"moveToNext": "przesuń na następne"
},
"common": {
"increase": "zwiększ",
@@ -99,7 +104,18 @@
"decrease": "obniż",
"path": "ścieżka",
"center": "środkowy",
"note": "notatka"
"note": "notatka",
"albumPeak": "spadek albumu",
"albumGain": "wzrost albumu",
"mbid": "ID MusicBrainz",
"reload": "przeładuj",
"share": "udostępnij",
"trackGain": "gain utworu",
"trackPeak": "peak utworu",
"codec": "kodek",
"preview": "podgląd",
"close": "zamknij",
"translation": "tłumaczenie"
},
"entity": {
"genre_one": "gatunek",
@@ -147,7 +163,13 @@
"genreWithCount_many": "{{count}} gatunków",
"trackWithCount_one": "{{count}} utwór",
"trackWithCount_few": "{{count}} utwory",
"trackWithCount_many": "{{count}} utworów"
"trackWithCount_many": "{{count}} utworów",
"play_one": "{{count}} odtworzenie",
"play_few": "{{count}} odtworzenia",
"play_many": "{{count}} odtworzeń",
"song_one": "piosenka",
"song_few": "piosenki",
"song_many": "piosenek"
},
"error": {
"remotePortWarning": "uruchom ponownie serwer aby używać nowego portu",
@@ -168,7 +190,10 @@
"mpvRequired": "wymagane MPV",
"audioDeviceFetchError": "wystąpił błąd podczas próby znalezienia urządzeń dźwiękowych",
"invalidServer": "nieprawidłowy serwer",
"loginRateError": "zbyt dużo prób logowania, poczekaj chwilę i spróbuj ponownie"
"loginRateError": "zbyt dużo prób logowania, poczekaj chwilę i spróbuj ponownie",
"badAlbum": "ta strona jest wyświetlana, ponieważ ten utwór nie jest częścią albumu. najprawdopodobniej ten problem występuje, jeśli utwór znajduje się w nadrzędnym folderze plików z muzyką. jellyfin grupuje utwory tylko wtedy, gdy znajdują się one w folderze.",
"networkError": "wystąpił błąd sieciowy",
"openError": "nie można otworzyć pliku"
},
"filter": {
"mostPlayed": "najczęściej odtwarzane",
@@ -242,7 +267,7 @@
"error_savePassword": "wystąpił błąd podczas próby zapisania hasła"
},
"addToPlaylist": {
"success": "dodano {{message}} $t(entity.song_other) do {{numOfPlaylists}} $t(entity.playlist_other)",
"success": "dodano $t(entity.trackWithCount, {\"count\": {{message}} }) do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "dodano do $t(entity.playlist_one)",
"input_skipDuplicates": "pomiń duplikaty",
"input_playlists": "$t(entity.playlist_other)"
@@ -261,7 +286,17 @@
"title": "wyszukiwanie tekstów"
},
"editPlaylist": {
"title": "edytuj $t(entity.playlist_one)"
"title": "edytuj $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) zaktualizowana pomyślnie",
"publicJellyfinNote": "Z jakiegoś powodu Jellyfin nie udostępnia informacji na temat publiczności playlisty. Jeżeli chcesz, aby ta pozostała publiczna, mniej wybraną poniższą opcję"
},
"shareItem": {
"allowDownloading": "zezwól na pobieranie",
"description": "opis",
"setExpiration": "ustaw czas wygaśnięcia",
"success": "link do udostępniania skopiowany do schowka (lub kliknij tutaj, aby otworzyć)",
"createFailed": "nie udało się utworzyć linku do udostępniania (czy udostępnianie jest włączone?)",
"expireInvalid": "ustawiony czas wygaśnięcia musi być w przyszłości"
}
},
"page": {
@@ -277,11 +312,16 @@
"unsynchronized": "niezsynchronizowane",
"lyricAlignment": "wyrównaj tekst",
"useImageAspectRatio": "użyj współczynnika proporcji obrazu",
"lyricGap": "odstępy tekstu"
"lyricGap": "odstępy tekstu",
"dynamicImageBlur": "rozmiar rozmycia obrazu",
"dynamicIsImage": "włącz obraz w tle",
"lyricOffset": "opóźnienie tekstów (ms)"
},
"upNext": "następny",
"lyrics": "tekst",
"related": "powiązane"
"related": "powiązane",
"visualizer": "wizualizer",
"noLyrics": "nie znaleziono tekstu"
},
"appMenu": {
"selectServer": "wybierz serwer",
@@ -311,20 +351,31 @@
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"numberSelected": "zaznaczono {{count}}",
"removeFromQueue": "$t(action.removeFromQueue)"
"removeFromQueue": "$t(action.removeFromQueue)",
"shareItem": "udostępnij pozycję",
"showDetails": "zobacz informacje",
"download": "pobierz",
"playShuffled": "$t(player.shuffle)",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"moveToNext": "$t(action.moveToNext)"
},
"albumDetail": {
"moreFromArtist": "więcej od $t(entity.artist_one)",
"moreFromGeneric": "więcej od {{item}}"
"moreFromGeneric": "więcej od {{item}}",
"released": "wydany"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
"title": "$t(entity.genre_other)",
"showAlbums": "pokaż $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "pokaż $t(entity.genre_one) $t(entity.track_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
"title": "$t(entity.album_other)",
"artistAlbums": "albumy artysty {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
},
"sidebar": {
"nowPlaying": "teraz odtwarzane",
@@ -337,7 +388,8 @@
"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": "udostępnione $t(entity.playlist_other)"
},
"home": {
"mostPlayed": "najczęściej odtwarzane",
@@ -350,10 +402,13 @@
"playbackTab": "odtworzenia",
"generalTab": "ogólne",
"hotkeysTab": "skróty klawiszowe",
"windowTab": "okno"
"windowTab": "okno",
"advanced": "zaawansowane"
},
"trackList": {
"title": "$t(entity.track_other)"
"title": "$t(entity.track_other)",
"artistTracks": "utwory przez {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)"
},
"globalSearch": {
"commands": {
@@ -365,6 +420,33 @@
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumArtistDetail": {
"topSongs": "popularne utwory",
"topSongsFrom": "popularne utwory z {{title}}",
"about": "O {{artist}}",
"recentReleases": "ostatnie wydania",
"viewAll": "zobacz wszystko",
"viewDiscography": "przeglądaj dyskografię",
"relatedArtists": "powiązane z $t(entity.artist_other)",
"appearsOn": "pojawia się na",
"viewAllTracks": "zobacz wszystko $t(entity.track_other)"
},
"itemDetail": {
"copyPath": "kopiuj ścieżkę do schowka",
"copiedPath": "ścieżka została skopiowana pomyślnie",
"openFile": "pokaż utwór w menedżerze plików"
},
"manageServers": {
"title": "zarządzaj serwerami",
"url": "URL",
"username": "nazwa użytkownika",
"removeServer": "usuń serwer",
"serverDetails": "szczegóły serwera",
"editServerDetailsTooltip": "edytuj szczegóły serwera"
},
"playlist": {
"reorder": "zmiana kolejności jest możliwa tylko podczas sortowania według id"
}
},
"player": {
@@ -372,14 +454,14 @@
"stop": "stop",
"repeat": "powtarzaj jeden",
"queue_remove": "usuń zaznaczone",
"playRandom": "odtwarzaj losowe",
"playRandom": "odtwarzaj losowo",
"skip": "pomiń",
"previous": "poprzedni",
"toggleFullscreenPlayer": "przełącz odtwarzacz pełnoekranowy",
"skip_back": "przeskocz do tyłu",
"favorite": "ulubione",
"next": "następny",
"shuffle": "losowa kolejność",
"shuffle": "odtwarzaj losowo",
"playbackFetchNoResults": "nie znaleziono utworów",
"playbackFetchInProgress": "wczytywanie utworów…",
"addNext": "dodaj następny",
@@ -396,7 +478,9 @@
"shuffle_off": "losowa kolejność wyłączona",
"addLast": "dodaj na końcu",
"mute": "wycisz",
"skip_forward": "przeskocz do przodu"
"skip_forward": "przeskocz do przodu",
"viewQueue": "zobacz kolejkę",
"playSimilarSongs": "odtwarzaj podobne"
},
"setting": {
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
@@ -413,13 +497,13 @@
"hotkey_zoomIn": "przybliż",
"hotkey_browserForward": "przeglądarka w przód",
"audioExclusiveMode_description": "włącz wyłączny tryb wyjścia. W tym trybie, system zwykle jest zablokowany i może odtwarzać tylko poprzez mpv",
"discordUpdateInterval": "{{discord}} interwał aktualizacji obszernej obecności",
"discordUpdateInterval": "{{discord}} interwał aktualizacji rich presence",
"fontType_optionBuiltIn": "wbudowana czcionka",
"hotkey_playbackPlayPause": "odtwarzaj / wstrzymaj",
"hotkey_rate1": "oceń na 1 gwiazdkę",
"hotkey_skipForward": "przeskocz do przodu",
"disableLibraryUpdateOnStartup": "wyłącz wyszukiwanie aktualizacji podczas uruchamiania aplikacji",
"discordApplicationId_description": "id dla aplikacji {{discord}} obszernie obecne (domyślnie {{defaultId}})",
"discordApplicationId_description": "id dla aplikacji {{discord}} rich presence (domyślnie {{defaultId}})",
"gaplessAudio": "dźwięk bez przerw",
"hotkey_playbackPlay": "odtwarzaj",
"hotkey_togglePreviousSongFavorite": "dodaj $t(common.previousSong) do ulubionych",
@@ -449,7 +533,7 @@
"crossfadeDuration_description": "ustaw czas trwania efektu przenikania",
"language": "język",
"hotkey_toggleShuffle": "przełącz kolejność losową",
"discordRichPresence_description": "włącz status odtwarzania w {{discord}} obszernie obecny. Klucze obrazów to {{icon}}, {{playing}} i {{paused}}. ",
"discordRichPresence_description": "włącz status odtwarzania w {{discord}} (rich presence). Dzięki temu będą wyświetlane informacje takie jak: {{icon}}, {{playing}} i {{paused}}. ",
"audioDevice": "urządzenia dźwiękowe",
"hotkey_rate2": "oceń na 2 gwiazdki",
"exitToTray": "zamknij do zasobnika",
@@ -470,12 +554,12 @@
"customFontPath": "niestandardowa ścieżka czcionki",
"followLyric": "podążaj za tekstem",
"crossfadeDuration": "czas trwania przenikania",
"discordIdleStatus": "pokaż obszerne informacje w stanie bezczynności",
"discordIdleStatus": "pokaż status w stanie bezczynności",
"audioPlayer": "odtwarzacz dźwięku",
"hotkey_zoomOut": "oddal",
"hotkey_unfavoriteCurrentSong": "usuń $t(common.currentSong) z ulubionych",
"hotkey_rate0": "wyczyść oceny",
"discordApplicationId": "id aplikacji {{discord}}",
"discordApplicationId": "ID aplikacji {{discord}}",
"applicationHotkeys_description": "ustaw skróty klawiszowe aplikacji. przełącz pole wyboru aby ustawić skrót globalny (tylko komputery)",
"floatingQueueArea_description": "wyświetl ikonę najechania kursorem po prawej stronie ekranu, aby wyświetlić kolejkę odtwarzania",
"hotkey_volumeMute": "wycisz",
@@ -485,12 +569,11 @@
"customFontPath_description": "ustaw ścieżkę dla niestandardowych czcionek dla aplikacji",
"gaplessAudio_optionWeak": "słabe (rekomendowane)",
"hotkey_playbackStop": "zatrzymaj",
"discordRichPresence": "{{discord}} obszernie obecny",
"discordRichPresence": "Status {{discord}} (rich presence)",
"font_description": "ustaw czcionkę dla aplikacji",
"mpvExecutablePath_help": "jedna na linnię",
"playButtonBehavior_optionPlay": "$t(player.play)",
"minimumScrobblePercentage": "minimalny czas trwania scrobble (procentowy)",
"mpvExecutablePath_description": "ustaw ścieżkę dla plików wykonywalnych mpv",
"mpvExecutablePath_description": "ustaw ścieżkę dla plików wykonywalnych mpv. gdy puste, zostanie użyta domyślna ścieżka",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"minimizeToTray_description": "zminimalizuj aplikację do zasobnika systemowego",
"remotePassword": "hasło dla serwera zdalnej kontroli",
@@ -501,28 +584,28 @@
"mpvExecutablePath": "ścieżka pliku wykonywalnego mpv",
"playButtonBehavior_description": "ustaw domyślne zachowanie dla przycisku odtwarzania kiedy piosenka zostanie dodana do kolejki",
"minimumScrobblePercentage_description": "minimalny czas odtwarzania piosenki który musi upłynąć aby uznać ją za scrobble",
"minimumScrobbleSeconds_description": "minimalny czas odtwarzania piosenki w sekundach jaki musi upłynąć aby uznać ją za scrobble",
"minimumScrobbleSeconds_description": "minimalny czas odtwarzania piosenki w sekundach jaki musi upłynąć aby uznać ją za scrobbling",
"playButtonBehavior": "zachowanie przycisku odtwarzania",
"playbackStyle_optionNormal": "normalny",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"minimumScrobbleSeconds": "minimalne scrobble (sekund)",
"minimumScrobbleSeconds": "minimalne scrobble (w sekundach)",
"remotePort_description": "ustaw port dla serwera zdalnej kontroli",
"replayGainMode_description": "dostosuj wzmocnienie dźwięku zgodnie z wartościami {{ReplayGain}} przechowywanymi w metadanych do pliku",
"replayGainFallback": "rezerwowy {{ReplayGain}}",
"sidebarCollapsedNavigation_description": "pokaż lub ukryj nawigację na zwiniętym pasku bocznym",
"skipDuration": "czas trwania pominięcia",
"showSkipButtons": "pokaż przyciski pomijania",
"scrobble": "scrobble",
"scrobble": "scrobbling",
"skipDuration_description": "ustaw czas pominięcia kiedy zostanie użyty przycisk pominięcia na pasku odtwarzania",
"replayGainClipping_description": "Zapobiegaj wzmocnieniu spowodowanemu przez {{ReplayGain}} na automatyczne obniżanie wzmocnienia",
"replayGainPreamp": "przedwzmacniacz {{ReplayGain}} (db)",
"sampleRate": "częstotliwość próbkowania",
"sidePlayQueueStyle_optionAttached": "przyłączony",
"sidebarConfiguration": "konfiguracja paska bocznego",
"sampleRate_description": "wybierz wyjściową częstotliwość próbkowania, która ma być używana, jeśli wybrana częstotliwość próbkowania różni się od częstotliwości bieżącego utworu",
"sampleRate_description": "wybierz wyjściową częstotliwość próbkowania, która ma być używana, jeśli wybrana częstotliwość próbkowania różni się od częstotliwości bieżącego utworu. wartość mniejsza niż 8000 spowoduje użycie częstotliwości domyślnej",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainClipping": "wzmocnienie {{ReplayGain}}",
"scrobble_description": "odtwarzanie scrobble na serwerze multimediów",
"scrobble_description": "przekazywanie informacji o odtwarzaniu (scrobbling) do twojego serwera multimediów",
"sidePlayQueueStyle": "boczny styl kolejki odtwarzania",
"remoteUsername_description": "ustaw nazwę użytkownika dla serwera zdalnej kontroli. Jeśli nazwa użytkownika i hasło są puste, autoryzacja będzie wyłączona",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
@@ -559,7 +642,72 @@
"skipPlaylistPage": "pomiń stronę list odtwarzania",
"themeDark": "motyw (ciemny)",
"windowBarStyle_description": "wybierz styl paska okna",
"useSystemTheme": "użyj motywu systemowego"
"useSystemTheme": "użyj motywu systemowego",
"buttonSize": "Rozmiar przycisku paska odtwarzacza",
"clearQueryCache": "wyczyść pamięć podręczną feishin",
"clearCache_description": "\"twarde wyczyszczenie\" feishin. oprócz wyczyszczenia pamięci podręcznej feishin, opróżnij pamięć podręczną przeglądarki (zapisane obrazy i inne zasoby). dane i ustawienia serwera zostaną zachowane",
"clearQueryCache_description": "\"miękkie wyczyszczenie\" feishin. spowoduje to odświeżenie list odtwarzania, metadanych utworów i zresetowanie zapisanych tekstów. ustawienia, dane uwierzytelniające serwera i obrazy w pamięci podręcznej zostaną zachowane",
"buttonSize_description": "rozmiar przycisków paska odtwarzacza",
"clearCache": "wyczyść pamięć podręczną przeglądarki",
"playerAlbumArtResolution": "rozdzielczość okładki albumu odtwarzacza",
"externalLinks": "pokaż zewnętrzne linki",
"genreBehavior_description": "określa, czy kliknięcie gatunku domyślnie otwiera listę utworów czy albumów",
"mpvExtraParameters_help": "po jednym na linię",
"passwordStore": "hasła",
"passwordStore_description": "jakie hasło ma być używane. zmień to, jeśli masz problemy z przechowywaniem haseł.",
"playerAlbumArtResolution_description": "rozdzielczość podglądu okładki albumu w dużym odtwarzaczu. większa sprawia, że wygląda bardziej wyraziście, ale może spowolnić ładowanie. domyślnie 0, czyli auto",
"startMinimized": "uruchom zminimalizowany",
"startMinimized_description": "uruchom aplikację w zasobniku systemowym",
"clearCacheSuccess": "pamięć podręczna została wyczyszczona pomyślnie",
"genreBehavior": "domyślne zachowanie strony gatunek",
"externalLinks_description": "umożliwia wyświetlanie linków zewnętrznych (Last.fm, MusicBrainz) na stronach artystów/albumów",
"homeConfiguration": "konfiguracja strony głównej",
"homeConfiguration_description": "konfiguracja elementów wyświetlanych na stronie głównej i ich kolejności",
"albumBackground_description": "dodaje obraz tła dla stron albumu zawierających grafikę albumu",
"albumBackgroundBlur": "rozmiar rozmycia obrazu tła albumu",
"albumBackgroundBlur_description": "dostosowywuje ilość rozmycia nakladanego na obraz tła albumu",
"albumBackground": "obraz tła albumu",
"artistConfiguration_description": "skonfiguruj jakie elementy są pokazywane, i w jakiej kolejności, na stronie albumu wykonawcy",
"discordListening_description": "pokazuje status jako słucha zamiast w grze",
"transcodeNote": "przynosi efekt po 1 (web) - 2 (mpv) piosenkach",
"transcode_description": "włącza transkodowanie na inne formaty",
"transcodeBitrate": "bitrate do transkodowania",
"transcode": "włącz transkodowanie",
"translationApiProvider": "usługodawca do api tłumaczeń",
"translationApiProvider_description": "wybór usługodawcy do api tłumaczeń",
"translationApiKey": "klucz api do tłumaczeń",
"transcodeFormat_description": "wybiera format do transkodowania. zostaw pusty aby serwer wybrał format",
"translationApiKey_description": "klucz api do tłumaczenia (Obsługuje tylko globalny endpoint)",
"homeFeature": "karuzela polecanych na stronie głównej",
"customCssEnable": "włącz niestandardowy css",
"customCssEnable_description": "pozwalaj na pisanie niestandardowego css.",
"customCssNotice": "Ostrzeżenie: chociaż istnieje pewne filtrowanie (uniemożliwia używanie url() i content:), używanie niestandardowego CSS-a może stwarzać ryzyko przez zmiany w interfejsie.",
"customCss_description": "zawartość niestandardowego css. Uwaga: content i zdalne url są niedozwolonymi właściwościami. Podgląd twojej zawartości jest pokazana poniżej. Dodatkowe pola których nie ustawiłeś, są obecne z powodu sanityzacji.",
"customCss": "niestandardowy css",
"doubleClickBehavior": "zakolejkuj wszystkie wyszukane utwory gdy podwójnie kliknięto",
"trayEnabled_description": "pokaż/ukryj ikonę/menu w zasobniku. jeżeli wyłączone, wyłącza też minimalizowanie.wyjście do zasobnika",
"webAudio_description": "używaj web audio. włącza to zaawansowane funkcje takie jak replaygain. wyłącz jeżeli nie działa poprawnie",
"artistConfiguration": "konfiguracja strony albumu wykonawcy",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerbarOpenDrawer_description": "pozwala przełączyć na odtwarzacz pełnoekranowy po kliknięciu paska odtwarzania",
"playerbarOpenDrawer": "przełącznik pełnego ekranu na pasku odtwarzania",
"imageAspectRatio": "używaj natywnych proporcji okładki",
"volumeWidth": "szerokość paska głośności",
"discordListening": "pokazuj status jako słucha",
"imageAspectRatio_description": "jeżeli włączone, okładka będzie pokazywana z użyciem jej natywnych proporcji. dla okładek które nie mają proporcji 1:1, pozostałe miejsce będzie puste",
"volumeWidth_description": "szerokość paska głośności",
"contextMenu_description": "pozwala ci na ukrycie elementów które są pokazywane w menu po kliknięciu prawym przyciskiem myszy na element. elementy które zostały odznaczone będą ukryte",
"contextMenu": "konfiguracja menu kontekstowego (pod prawym przyciskiem myszy)",
"transcodeBitrate_description": "wybiera bitrate do transkodowania. 0 pozwala wybrać to serwerowi",
"transcodeFormat": "format do transkodowania",
"translationTargetLanguage_description": "język do którego będzie tłumaczona treść",
"trayEnabled": "pokazuj w zasobniku",
"webAudio": "używaj web audio",
"homeFeature_description": "ustawienie powoduje to czy wyświetlana jest karuzela z polecanymi utworami na stronie głównej",
"doubleClickBehavior_description": "jeżeli włączone, wszystkie pasujące utwory w wyszukiwaniu zostaną zakolejkowane. w przeciwnym wypadku, tylko kliknięty będzie zakolejkowany",
"lastfmApiKey": "klucz API {{lastfm}}",
"lastfmApiKey_description": "klucz API dla {{lastfm}}. wymagany dla okładek",
"translationTargetLanguage": "docelowy język tłumaczenia"
},
"table": {
"config": {
@@ -573,7 +721,10 @@
"gap": "$t(common.gap)",
"tableColumns": "kolumny tabeli",
"autoFitColumns": "automatyczne dopasowanie kolumn",
"size": "$t(common.size)"
"size": "$t(common.size)",
"itemSize": "rozmiar elementu (px)",
"itemGap": "odstęp między elementami (px)",
"followCurrentSong": "śledź aktualną piosenkę"
},
"label": {
"releaseDate": "data premiery",
@@ -601,7 +752,9 @@
"discNumber": "numer płyty",
"favorite": "$t(common.favorite)",
"year": "$t(common.year)",
"albumArtist": "$t(entity.albumArtist_one)"
"albumArtist": "$t(entity.albumArtist_one)",
"codec": "$t(common.codec)",
"songCount": "$t(entity.track_other)"
}
},
"column": {
@@ -626,7 +779,9 @@
"albumArtist": "artysta albumu",
"path": "ścieżka",
"discNumber": "płyta",
"channels": "$t(common.channel_other)"
"channels": "$t(common.channel_other)",
"size": "$t(common.size)",
"codec": "$t(common.codec)"
}
}
}
+353 -34
View File
@@ -1,6 +1,6 @@
{
"common": {
"backward": "voltar",
"backward": "para trás",
"areYouSure": "tem certeza?",
"add": "adicionar",
"ascending": "ascendente",
@@ -9,7 +9,7 @@
"bitrate": "taxa de bits",
"action_one": "ação",
"action_many": "ações",
"action_other": "(n == 0 || n == 1) ? ação : ações",
"action_other": "ações",
"biography": "biografia",
"bpm": "bpm",
"edit": "editar",
@@ -27,11 +27,11 @@
"title": "titulo",
"create": "criar",
"confirm": "confirmar",
"home": "inicio",
"home": "início",
"comingSoon": "em breve…",
"channel_one": "canal",
"channel_many": "canais",
"channel_other": "(n == 0 || n == 1) ? canal : canais",
"channel_other": "canais",
"disable": "desabilitar",
"expand": "expandir",
"disc": "disco",
@@ -56,13 +56,13 @@
"path": "caminho",
"no": "não",
"owner": "dono",
"forward": "avançar",
"forward": "para frente",
"forceRestartRequired": "reinicie para aplicar as alterações… feche a notificação para reiniciar",
"setting": "contexto",
"setting": "configuração",
"version": "versão",
"filter_one": "filtro",
"filter_many": "filtros",
"filter_other": "(n == 0 || n == 1) ? filtro : filtros",
"filter_other": "filtros",
"filters": "filtros",
"saveAndReplace": "salvar e substituir",
"playerMustBePaused": "o player deve estar pausado",
@@ -74,13 +74,24 @@
"restartRequired": "é necessário reiniciar",
"previousSong": "anterior $t(entity.track_one)",
"noResultsFromQuery": "a consulta não retornou resultados",
"quit": "abandonar",
"quit": "sair",
"search": "procurar",
"saveAs": "salvar como",
"yes": "sim",
"random": "aleatório",
"size": "tamanho",
"note": "observação"
"note": "observação",
"mbid": "ID no MusicBrainz",
"reload": "recarregar",
"codec": "codec",
"preview": "pré-visualizar",
"share": "compartilhar",
"close": "fechar",
"translation": "tradução",
"albumGain": "ganho do álbum",
"trackPeak": "pico da faixa",
"albumPeak": "pico do álbum",
"trackGain": "ganho da faixa"
},
"action": {
"goToPage": "vá para página",
@@ -98,36 +109,103 @@
"removeFromPlaylist": "remover da $t(entity.playlist_one)",
"deletePlaylist": "deletar $t(entity.playlist_one)",
"deselectAll": "desmarcar todos",
"removeFromFavorites": "remover de $t(entity.favorite_other)"
"removeFromFavorites": "remover de $t(entity.favorite_other)",
"openIn": {
"lastfm": "Abrir em Last.fm",
"musicbrainz": "Abrir em MusicBrainz"
},
"toggleSmartPlaylistEditor": "alternar editor $t(entity.smartPlaylist)",
"moveToNext": "mover para o próximo"
},
"form": {
"deletePlaylist": {
"title": "deletar $t(entity.playlist_one)"
"title": "deletar $t(entity.playlist_one)",
"input_confirm": "escreva o nome da $t(entity.playlist_one) para confirmar",
"success": "$t(entity.playlist_one) deletada com sucesso"
},
"addServer": {
"title": "adicionar servidor"
"title": "adicionar servidor",
"input_password": "senha",
"input_legacyAuthentication": "habilitar autenticação legada",
"error_savePassword": "um erro ocorreu ao tentar salvar a senha",
"ignoreSsl": "ignorar ssl ($t(common.restartRequired))",
"input_savePassword": "salvar senha",
"input_url": "url",
"success": "servidor adicionado com sucesso",
"input_name": "nome do servidor",
"input_username": "nome de usuário",
"ignoreCors": "ignorar CORS ($t(common.restartRequired))"
},
"createPlaylist": {
"title": "criar $t(entity.playlist_one)"
"title": "criar $t(entity.playlist_one)",
"input_public": "público",
"input_description": "$t(common.description)",
"success": "$t(entity.playlist_one) criada com sucesso",
"input_owner": "$t(common.owner)",
"input_name": "$t(common.name)"
},
"updateServer": {
"title": "atualizar servidor"
"title": "atualizar servidor",
"success": "servidor atualizado com sucesso"
},
"editPlaylist": {
"title": "editar $t(entity.playlist_one)"
"title": "editar $t(entity.playlist_one)",
"publicJellyfinNote": "O Jellyfin por algum motivo não expõe se uma playlist é pública ou não. Se você deseja que ela permaneça pública, por favor selecione a seguinte entrada",
"success": "$t(entity.playlist_one) atualizada com sucesso"
},
"addToPlaylist": {
"title": "adicionar à $t(entity.playlist_one)"
"title": "adicionar à $t(entity.playlist_one)",
"input_playlists": "$t(entity.playlist_other)",
"input_skipDuplicates": "pular duplicadas",
"success": "adicionado $t(entity.trackWithCount, {\"count\": {{message}} }) para $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })"
},
"lyricSearch": {
"title": "pesquisa de letras"
"title": "pesquisa de letras",
"input_artist": "$t(entity.artist_one)",
"input_name": "$t(common.name)"
},
"shareItem": {
"createFailed": "falha ao criar compartilhamento (o compartilhamento está ativado?)",
"setExpiration": "definir expiração",
"success": "link de compartilhamento copiado para a área de transferência (ou clique aqui para abrir)",
"allowDownloading": "permitir downloads",
"description": "descrição",
"expireInvalid": "a expiração deve ser uma data futura"
},
"queryEditor": {
"input_optionMatchAny": "corresponder qualquer um",
"input_optionMatchAll": "corresponder todos"
}
},
"setting": {
"discordIdleStatus_description": "quando ativado, atualiza o status enquanto o player está ocioso",
"discordUpdateInterval_description": "o tempo em segundos entre cada atualização (mínimo 15 segundos)",
"playButtonBehavior_description": "define o comportamento padrão do botão play ao adicionar músicas à fila",
"discordApplicationId": "{{discord}} ID do aplicativo"
"discordApplicationId": "{{discord}} ID do aplicativo",
"audioPlayer": "player de áudio",
"applicationHotkeys": "teclas de atalho da aplicação",
"applicationHotkeys_description": "configure as teclas de atalho da aplicação. clique na caixa de seleção para definir como tecla de atalho global (somente desktop)",
"contextMenu": "configuração do menu de contexto (clique do botão direito do mouse)",
"clearQueryCache": "limpar cache do feishin",
"clearCache": "limpar cache do navegador",
"clearQueryCache_description": "uma 'limpeza leve' do feishin. isso irá renovar playlists, metadados de faixas, e resetar letras salvas. as configurações, as credenciais de servidor e o cache de imagens serão mantidos",
"audioPlayer_description": "selecione o player de áudio usado para reprodução",
"audioExclusiveMode": "modo de áudio exclusivo",
"buttonSize": "tamanho do botão da barra de reprodução",
"albumBackground_description": "adiciona uma imagem de fundo contendo a arte do álbum para a página de álbum",
"clearCache_description": "uma 'limpeza geral' do feishin. em adição a limpar o cache do feishin, limpa o cache do navegador (imagens salvas e outros recursos). as credenciais de servidor e as configurações serão mantidas",
"clearCacheSuccess": "cache limpo com sucesso",
"audioDevice": "dispositivo de áudio",
"audioDevice_description": "selecione o dispositivo de áudio usado para reprodução (somente player web)",
"audioExclusiveMode_description": "ativar modo de saída exclusiva. Neste modo, o sistema é geralmente bloqueado, e apenas mpv terá saída de áudio",
"accentColor": "cor de realce",
"accentColor_description": "define a cor de realce para a aplicação",
"artistConfiguration": "configuração da página de artista de álbum",
"artistConfiguration_description": "configure quais itens serão mostrados, e em qual ordem, na página de artista de álbum",
"buttonSize_description": "o tamanho dos botões da barra de reprodução",
"albumBackgroundBlur": "tamanho de desfoque da imagem de fundo do álbum",
"albumBackgroundBlur_description": "ajusta a quantidade de desfoque aplicada para a imagem de fundo do álbum",
"albumBackground": "imagem de fundo do álbum"
},
"table": {
"config": {
@@ -139,7 +217,8 @@
},
"column": {
"title": "titulo",
"discNumber": "disco"
"discNumber": "disco",
"size": "$t(common.size)"
}
},
"page": {
@@ -154,57 +233,297 @@
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
"title": "$t(entity.genre_other)",
"showTracks": "mostrar $t(entity.genre_one) $t(entity.track_other)",
"showAlbums": "mostrar $t(entity.genre_one) $t(entity.album_other)"
},
"trackList": {
"title": "$t(entity.track_other)"
"title": "$t(entity.track_other)",
"artistTracks": "faixas de {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)"
},
"globalSearch": {
"title": "comandos"
"title": "comandos",
"commands": {
"serverCommands": "comandos do servidor",
"goToPage": "ir para a página",
"searchFor": "buscar por {{query}}"
}
},
"sidebar": {
"home": "$t(common.home)"
"home": "$t(common.home)",
"tracks": "$t(entity.track_other)",
"shared": "$t(entity.playlist_other) compartilhada",
"albums": "$t(entity.album_other)",
"genres": "$t(entity.genre_other)",
"folders": "$t(entity.folder_other)",
"albumArtists": "$t(entity.albumArtist_other)",
"artists": "$t(entity.artist_other)",
"nowPlaying": "tocando agora",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"settings": "$t(common.setting_other)"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
"title": "$t(entity.album_other)",
"artistAlbums": "álbuns de {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
},
"appMenu": {
"openBrowserDevtools": "abrir ferramentas do desenvolvedor",
"quit": "$t(common.quit)",
"selectServer": "selecionar servidor",
"collapseSidebar": "recolher barra lateral",
"expandSidebar": "expandir barra lateral",
"goBack": "voltar",
"goForward": "avançar",
"version": "versão {{version}}",
"manageServers": "gerenciar servidores",
"settings": "$t(common.setting_other)"
},
"contextMenu": {
"moveToTop": "$t(action.moveToTop)",
"moveToBottom": "$t(action.moveToBottom)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"numberSelected": "{{count}} selecionado",
"addFavorite": "$t(action.addToFavorites)",
"addLast": "$t(player.addLast)",
"addNext": "$t(player.addNext)",
"addToFavorites": "$t(action.addToFavorites)",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"removeFromQueue": "$t(action.removeFromQueue)",
"play": "$t(player.play)",
"playShuffled": "$t(player.shuffle)",
"createPlaylist": "$t(action.createPlaylist)",
"download": "baixar",
"shareItem": "compartilhar item",
"showDetails": "obter informações",
"addToPlaylist": "$t(action.addToPlaylist)",
"deletePlaylist": "$t(action.deletePlaylist)",
"deselectAll": "$t(action.deselectAll)",
"moveToNext": "$t(action.moveToNext)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"setRating": "$t(action.setRating)"
},
"albumArtistDetail": {
"viewAllTracks": "ver todas as $t(entity.track_other)",
"appearsOn": "aparece em",
"recentReleases": "lançamentos recentes",
"viewDiscography": "ver discografia",
"relatedArtists": "$t(entity.artist_other) relacionados",
"viewAll": "ver tudo",
"topSongsFrom": "músicas mais tocadas de {{title}}",
"topSongs": "músicas mais tocadas",
"about": "Sobre {{artist}}"
},
"fullscreenPlayer": {
"config": {
"unsynchronized": "não sincronizado",
"dynamicIsImage": "habilitar imagem de fundo",
"dynamicImageBlur": "tamanho do desfoque da imagem",
"lyricAlignment": "alinhamento da letra",
"showLyricMatch": "exibir correspondência da letra",
"showLyricProvider": "exibir origem da letra",
"synchronized": "sincronizado",
"lyricOffset": "deslocamento da letra (ms)",
"followCurrentLyric": "acompanhar letra",
"useImageAspectRatio": "usar proporção da imagem",
"lyricGap": "espaçamento da letra",
"lyricSize": "tamanho da letra",
"dynamicBackground": "fundo dinâmico",
"opacity": "opacidade"
},
"related": "relacionado",
"visualizer": "visualizador",
"upNext": "a seguir",
"lyrics": "letra",
"noLyrics": "nenhuma letra encontrada"
},
"albumDetail": {
"moreFromArtist": "mais deste $t(entity.artist_one)",
"moreFromGeneric": "mais de {{item}}",
"released": "lançado"
},
"itemDetail": {
"copyPath": "copiar caminho para a área de transferência",
"copiedPath": "caminho copiado com sucesso",
"openFile": "mostrar faixa no gerenciador de arquivos"
},
"manageServers": {
"serverDetails": "detalhes do servidor",
"url": "URL",
"username": "nome de usuário",
"editServerDetailsTooltip": "editar detalhes do servidor",
"removeServer": "remover servidor",
"title": "gerenciar servidores"
},
"setting": {
"generalTab": "geral",
"hotkeysTab": "teclas de atalho",
"windowTab": "janela",
"advanced": "avançado",
"playbackTab": "reprodução"
},
"playlist": {
"reorder": "reordenar apenas disponível quando ordenado pelo id"
}
},
"filter": {
"title": "titulo",
"disc": "disco",
"mostPlayed": "mais tocado"
"mostPlayed": "mais tocado",
"album": "$t(entity.album_one)",
"name": "nome",
"biography": "bibliografia",
"duration": "duração",
"favorited": "favoritado",
"fromYear": "a partir do ano",
"songCount": "contador de músicas",
"toYear": "até o ano",
"random": "aleatório",
"search": "buscar",
"lastPlayed": "última tocada",
"isCompilation": "é compilação",
"trackNumber": "faixa",
"communityRating": "Nota da comunidade",
"isPublic": "é público",
"playCount": "contador de execuções",
"recentlyUpdated": "atualizado recentemente",
"dateAdded": "data de adição",
"isRecentlyPlayed": "foi tocado recentemente",
"albumArtist": "$t(entity.albumArtist_one)",
"recentlyAdded": "adicionado recentemente",
"releaseDate": "data de lançamento",
"recentlyPlayed": "tocado recentemente",
"criticRating": "avaliação da crítica",
"isFavorited": "é favoritado",
"releaseYear": "ano de lançamento",
"rating": "avaliação",
"artist": "$t(entity.artist_one)",
"bpm": "bpm",
"channels": "$t(common.channel_other)",
"comment": "comentário",
"owner": "$t(common.owner)",
"path": "caminho",
"id": "id",
"bitrate": "bitrate",
"isRated": "possui avaliação",
"note": "nota",
"albumCount": "número de $t(entity.album_other)",
"genre": "$t(entity.genre_one)"
},
"player": {
"playbackFetchNoResults": "nenhuma música encontrada",
"playbackFetchInProgress": "carregando músicas…"
"playbackFetchInProgress": "carregando músicas…",
"skip_forward": "avançar",
"mute": "mudo",
"playSimilarSongs": "tocar músicas similares",
"skip": "pular",
"stop": "parar",
"addNext": "adicionar a seguir",
"muted": "mudo",
"queue_clear": "limpar fila",
"toggleFullscreenPlayer": "alternar player de tela cheia",
"addLast": "adicionar no final",
"next": "próximo",
"play": "tocar",
"playRandom": "tocar aleatório",
"shuffle_off": "aleatório desativado",
"queue_moveToBottom": "mover selecionados para o topo",
"queue_moveToTop": "mover selecionados para o fim",
"skip_back": "retroceder",
"unfavorite": "remover favorito",
"playbackSpeed": "velocidade de reprodução",
"previous": "anterior",
"favorite": "favorito",
"playbackFetchCancel": "isso está demorando um pouco... feche a notificação para cancelar",
"queue_remove": "remover selecionados",
"repeat": "repetir",
"repeat_all": "repetir tudo",
"repeat_off": "repetição desativada",
"shuffle": "tocar aleatório",
"pause": "pausar",
"viewQueue": "ver fila"
},
"entity": {
"albumArtist_one": "artista do álbum",
"albumArtist_many": "artistas do álbum",
"albumArtist_other": "(n == 0 || n == 1) ? artista do álbum : artistas do álbum",
"albumArtist_other": "artistas do álbum",
"albumArtistCount_one": "{{count}} artista do álbum",
"albumArtistCount_many": "{{count}} artistas do álbum",
"albumArtistCount_other": "(n == 0 || n == 1) ? {{count}} artista do álbum : {{count}} artistas do álbum",
"albumArtistCount_other": "{{count}} artistas do álbum",
"album_one": "álbum",
"album_many": "álbuns",
"album_other": "(n == 0 || n == 1) ? álbum : álbuns",
"album_other": "álbuns",
"artist_one": "artista",
"artist_many": "artistas",
"artist_other": "(n == 0 || n == 1) ? artista : artistas",
"artist_other": "artistas",
"albumWithCount_one": "{{count}} álbum",
"albumWithCount_many": "{{count}} álbuns",
"albumWithCount_other": "(n == 0 || n == 1) ? {{count}} álbum : {{count}} álbuns",
"albumWithCount_other": "{{count}} álbuns",
"favorite_one": "favorito",
"favorite_many": "favoritos",
"favorite_other": "(n == 0 || n == 1) ? favorito : favoritos",
"favorite_other": "favoritos",
"artistWithCount_one": "{{count}} artista",
"artistWithCount_many": "{{count}} artistas",
"artistWithCount_other": "(n == 0 || n == 1) ? artista : artistas",
"artistWithCount_other": "{{count}} artistas",
"folder_one": "pasta",
"folder_many": "pastas",
"folder_other": "(n == 0 || n == 1) ? pasta : pastas"
"folder_other": "pastas",
"genre_one": "gênero",
"genre_many": "gêneros",
"genre_other": "gêneros",
"playlistWithCount_one": "{{count}} playlist",
"playlistWithCount_many": "{{count}} playlists",
"playlistWithCount_other": "{{count}} playlists",
"playlist_one": "playlist",
"playlist_many": "playlists",
"playlist_other": "playlists",
"folderWithCount_one": "{{count}} pasta",
"folderWithCount_many": "{{count}} pastas",
"folderWithCount_other": "{{count}} pastas",
"genreWithCount_one": "{{count}} gênero",
"genreWithCount_many": "{{count}} gêneros",
"genreWithCount_other": "{{count}} gêneros",
"trackWithCount_one": "{{count}} faixa",
"trackWithCount_many": "{{count}} faixas",
"trackWithCount_other": "{{count}} faixas",
"track_one": "faixa",
"track_many": "faixas",
"track_other": "faixas",
"smartPlaylist": "$t(entity.playlist_one) inteligente",
"song_one": "música",
"song_many": "músicas",
"song_other": "músicas",
"play_one": "{{count}} reprodução",
"play_many": "{{count}} reproduções",
"play_other": "{{count}} reproduções"
},
"error": {
"remotePortWarning": "reinicie o servidor para aplicar a nova porta",
"systemFontError": "ocorreu um erro ao tentar obter fontes do sistema",
"playbackError": "ocorreu um erro ao tentar reproduzir a mídia",
"endpointNotImplementedError": "endpoint {{endpoint}} não está implementado para {{serverType}}",
"remotePortError": "ocorreu um erro ao tentar definir a porta do servidor remoto",
"serverRequired": "servidor necessário",
"authenticationFailed": "falha na autenticação",
"apiRouteError": "não é possível encaminhar a solicitação",
"genericError": "um erro ocorreu",
"credentialsRequired": "credenciais necessárias",
"sessionExpiredError": "sua sessão expirou",
"remoteEnableError": "ocorreu um erro ao tentar $t(common.enable) o servidor remoto",
"localFontAccessDenied": "acesso negado a fontes locais",
"serverNotSelectedError": "nenhum servidor selecionado",
"remoteDisableError": "ocorreu um erro ao tentar $t(common.disable) o servidor remoto",
"mpvRequired": "MPV necessário",
"audioDeviceFetchError": "ocorreu um erro ao tentar obter dispositivos de áudio",
"invalidServer": "servidor inválido",
"loginRateError": "muitas tentativas de login, tente novamente em alguns segundos",
"badAlbum": "você está vendo este erro por que está música não é parte de algum album. um motivo comum para você estar vendo este erro é se a sua música estiver na raiz da sua pasta de músicas. o jellyfin apenas agrupa as músicas se elas estiveram na mesma pasta.",
"networkError": "ocorreu um erro na internet",
"openError": "não foi possível abrir o arquivo"
}
}
+398 -121
View File
@@ -7,22 +7,27 @@
"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": "удалить из очереди",
"deselectAll": "снять выделение",
"moveToBottom": "вниз",
"setRating": "оценить",
"toggleSmartPlaylistEditor": "вкл/выкл $t(entity.smartPlaylist) редактор",
"removeFromFavorites": "удалить из $t(entity.favorite_other)"
"toggleSmartPlaylistEditor": "вкл./откл. редактор $t(entity.smartPlaylist)",
"removeFromFavorites": "удалить из $t(entity.favorite_few)",
"openIn": {
"lastfm": "открыть на Last.fm",
"musicbrainz": "открыть на MusicBrainz"
},
"moveToNext": "следующий"
},
"common": {
"backward": "назад",
"increase": "увеличить",
"rating": "рейтинг",
"bpm": "ударов в мин.",
"bpm": "уд./мин.",
"refresh": "обновить",
"unknown": "неизвестно",
"areYouSure": "вы уверены?",
@@ -34,19 +39,19 @@
"currentSong": "текущий $t(entity.track_one)",
"collapse": "закрыть",
"trackNumber": "трек",
"descending": "убывающий",
"descending": "по убыванию",
"add": "добавить",
"gap": "промежуток",
"ascending": "возрастающий",
"ascending": "по возрастанию",
"dismiss": "отклонить",
"year": "год",
"manage": "управлять",
"limit": "лимит",
"minimize": "минимизировать",
"manage": "управление",
"limit": "ограничение",
"minimize": "свернуть",
"modified": "изменено",
"duration": "продолжительность",
"duration": "длительность",
"name": "имя",
"maximize": "максимизировать",
"maximize": "развернуть",
"decrease": "уменьшить",
"ok": "ок",
"description": "описание",
@@ -60,8 +65,11 @@
"forward": "вперёд",
"delete": "удалить",
"cancel": "отменить",
"forceRestartRequired": "перезапустите приложение, чтобы применить изменения... закройте уведомление, чтобы перезапустить приложение",
"forceRestartRequired": "перезапустите приложение, чтобы применить изменения... закройте уведомление для перезапуска",
"setting": "настройка",
"setting_one": "настройка",
"setting_few": "",
"setting_many": "",
"version": "версия",
"title": "название",
"filter_one": "фильтр",
@@ -74,32 +82,43 @@
"action_one": "действие",
"action_few": "действия",
"action_many": "действий",
"playerMustBePaused": "воспроизведение должно быть остановлено",
"playerMustBePaused": "необходимо остановить воспроизведение",
"confirm": "подтвердить",
"resetToDefault": "сбросить к настройкам по умолчанию",
"home": "Главная страница",
"comingSoon": "скоро будет…",
"resetToDefault": "сбросить настройки",
"home": "главная",
"comingSoon": "скоро...",
"reset": "сбросить",
"channel_one": "канал",
"channel_few": "канала",
"channel_many": "каналов",
"disable": "выключить",
"disable": "отключить",
"sortOrder": "порядок",
"menu": "меню",
"restartRequired": "необходим перезапуск приложения",
"previousSong": "предыдущий $t(entity.track_one)",
"noResultsFromQuery": ет результатов",
"noResultsFromQuery": ичего не найдено",
"quit": "выйти",
"expand": "расширить",
"search": "Поиск",
"expand": "раскрыть",
"search": "поиск",
"saveAs": "сохранить как",
"disc": "диск",
"yes": "да",
"random": "случайный",
"random": "случайно",
"size": "размер",
"biography": "биография",
"note": "заметка",
"none": "нет"
"none": "нет",
"mbid": "MusicBrainz ID",
"reload": "перезагрузить",
"preview": "просмотр",
"codec": "кодек",
"share": "поделиться",
"close": "закрыть",
"albumGain": "альбом усиление",
"trackGain": "усиление трека",
"translation": "перевод",
"albumPeak": "пик альбома",
"trackPeak": "пик трека"
},
"entity": {
"album_one": "альбом",
@@ -114,18 +133,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}} авторов альбома",
@@ -152,7 +178,7 @@
"table": {
"config": {
"view": {
"card": "карта",
"card": "карточки",
"table": "таблица",
"poster": "постер"
},
@@ -161,7 +187,10 @@
"gap": "$t(common.gap)",
"tableColumns": "столбцы таблицы",
"autoFitColumns": "автоматически расставить столбцы",
"size": "$t(common.size)"
"followCurrentSong": "следовать за исполняемым треком",
"size": "$t(common.size)",
"itemSize": "размер элементов (px)",
"itemGap": "отступ между элементами (px)"
},
"label": {
"releaseDate": "дата выхода",
@@ -173,7 +202,7 @@
"bpm": "$t(common.bpm)",
"lastPlayed": "последний",
"trackNumber": "номер трека",
"rowIndex": "индекс ряда",
"rowIndex": "номер строки",
"rating": "$t(common.rating)",
"artist": "$t(entity.artist_one)",
"album": "$t(entity.album_one)",
@@ -189,7 +218,9 @@
"discNumber": "номер диска",
"favorite": "$t(common.favorite)",
"year": "$t(common.year)",
"albumArtist": "$t(entity.albumArtist_one)"
"albumArtist": "$t(entity.albumArtist_one)",
"codec": "$t(common.codec)",
"songCount": "$t(entity.track_other)"
}
},
"column": {
@@ -204,29 +235,44 @@
"trackNumber": "трек",
"genre": "$t(entity.genre_one)",
"path": "путь",
"discNumber": "диск"
"discNumber": "диск",
"size": "$t(common.size)",
"dateAdded": "дата добавления",
"album": "альбом",
"albumArtist": "исполнитель альбома",
"biography": "биография",
"codec": "$t(common.codec)",
"comment": "комментарий",
"albumCount": "$t(entity.album_other)",
"artist": "$t(entity.artist_one)",
"bitrate": "битрейт",
"channels": "$t(common.channel_other)",
"bpm": "bpm"
}
},
"error": {
"remotePortWarning": "перезапустить сервер для применения нового порта",
"remotePortWarning": "необходимо перезапустить сервер для применения нового порта",
"systemFontError": "произошла ошибка при попытке получить системные шрифты",
"playbackError": "произошла ошибка при попытке проиграть медиа",
"endpointNotImplementedError": "запрос {{endpoint}} is not implemented for {{serverType}}",
"playbackError": "произошла ошибка при попытке проигрывания медиа",
"endpointNotImplementedError": "запрос {{endpoint}} не реализован для {{serverType}}",
"remotePortError": "произошла ошибка при попытке установить порт удаленного сервера",
"serverRequired": "необходим сервер",
"authenticationFailed": "аутентификация завершилась с ошибкой",
"serverRequired": "сервер не выбран",
"authenticationFailed": "не удалось авторизироваться",
"apiRouteError": "невозможно выполнить запрос",
"genericError": "произошла ошибка",
"credentialsRequired": "необходимы учётные данные",
"sessionExpiredError": "ваш сеанс истек",
"remoteEnableError": "ошибка произошла при попытке $t(common.enable) удаленного сервера",
"credentialsRequired": "введите данные для входа",
"sessionExpiredError": "ваш сеанс истёк",
"remoteEnableError": "произошла ошибка при попытке $t(common.enable) удалённый сервер",
"localFontAccessDenied": "не получилось получить доступ к шрифтам",
"serverNotSelectedError": "не выбран сервер",
"remoteDisableError": "ошибка произошла при попытке $t(common.disable) удаленного сервера",
"mpvRequired": "Необходим MPV",
"remoteDisableError": "произошла ошибка при попытке $t(common.disable) удалённый сервер",
"mpvRequired": "необходим MPV",
"audioDeviceFetchError": "произошла ошибка с аудиоустройством",
"invalidServer": "недействительный сервер",
"loginRateError": "слишком много попыток входа, пожалуйста, попробуйте еще раз через несколько секунд"
"loginRateError": "превышено максимальное количество попыток входа, пожалуйста, попробуйте ещё раз через несколько секунд",
"openError": "не удалось открыть файл",
"badAlbum": "вы видите эту страницу из-за того, что эта песня не входит в альбом. скорее всего, вы видите эту ошибку, так как песня находится в корневой директории папки с музыкой. jellyfin группирует треки только по папкам.",
"networkError": "возникла ошибка сети"
},
"filter": {
"isCompilation": "сборник",
@@ -236,76 +282,78 @@
"communityRating": "рейтинг сообщества",
"favorited": "любимый",
"albumArtist": "$t(entity.albumArtist_one)",
"isFavorited": "любимый",
"bpm": "ударов в мин.",
"isFavorited": "любимые",
"bpm": "уд./мин.",
"disc": "диск",
"biography": "биография",
"artist": "$t(entity.artist_one)",
"duration": "продолжительность",
"fromYear": "из года",
"duration": "длительность",
"fromYear": "год",
"criticRating": "рейтинг критиков",
"mostPlayed": "наибольшое кол-во воспроизведений",
"mostPlayed": "слушают чаще всего",
"comment": "комментировать",
"playCount": "кол-во воспроизведений",
"recentlyUpdated": "недавно обновлено",
"playCount": "количество воспроизведений",
"recentlyUpdated": "обновленные недавно",
"channels": "$t(common.channel_other)",
"recentlyPlayed": "недавно проиграно",
"recentlyPlayed": "проигрывались недавно",
"owner": "$t(common.owner)",
"title": "название",
"rating": "рейтинг",
"search": "Поиск",
"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": "кол-во песен",
"id": "",
"songCount": "количество песен",
"isPublic": "публичный",
"random": "случайный",
"random": "случайно",
"lastPlayed": "последний раз проигрывалась",
"toYear": "до года",
"album": "$t(entity.album_one)",
"trackNumber": "трек"
},
"player": {
"repeat_all": "повтор всех",
"repeat_all": "повторять все",
"stop": "остановить",
"repeat": "повтор",
"queue_remove": "удалить выделенные",
"playRandom": "случайные песни",
"repeat": "повторять текущий",
"queue_remove": "удалить выбранное",
"playRandom": "играть случайные песни",
"playSimilarSongs": "играть похожие песни",
"skip": "пропустить",
"previous": "предыдущий",
"toggleFullscreenPlayer": "включить полноэкранный режим",
"skip_back": "назад",
"favorite": "любимый",
"next": "следующее",
"next": "следующий",
"shuffle": "перемешать",
"playbackFetchNoResults": "нет песен",
"playbackFetchNoResults": "песни не найдены",
"playbackFetchInProgress": "загрузка песен..",
"addNext": "добавить следующий",
"addNext": "воспроизвести следующим",
"playbackSpeed": "скорость воспроизведения",
"playbackFetchCancel": "это занимает некоторое время... закрыть уведомление для отмены",
"play": "прослушать",
"playbackFetchCancel": "пожалуйста, подождите немного... закройте уведомление для отмены",
"play": "играть",
"repeat_off": "повтор выключен",
"pause": "пауза",
"queue_clear": "очистить очередь",
"muted": "звук отключён",
"unfavorite": "убрать из любимых",
"queue_moveToTop": "переместить выделение вниз",
"queue_moveToBottom": "переместить выделение вверх",
"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)",
@@ -325,28 +373,41 @@
"followCurrentLyric": "следовать за текущими словами песни",
"opacity": "непрозрачность",
"lyricSize": "размер слов",
"showLyricProvider": "показать провайдера слов",
"unsynchronized": "несинхронизировано",
"showLyricProvider": "показать источник слов",
"unsynchronized": "не синхронизировано",
"lyricAlignment": "выравнивание слов песни",
"lyricOffset": "задержка слов (мсек)",
"useImageAspectRatio": "использовать соотношение сторон изображения",
"lyricGap": "пробел между словами"
"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)",
@@ -359,37 +420,45 @@
"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)",
"numberSelected": "{{count}} выбрано",
"removeFromQueue": "$t(action.removeFromQueue)"
"removeFromQueue": "$t(action.removeFromQueue)",
"showDetails": "получить информацию",
"shareItem": "поделиться"
},
"home": {
"mostPlayed": "наибольшее кол-во воспроизведений",
"mostPlayed": "слушают чаще всего",
"newlyAdded": "недавно добавленные релизы",
"title": "$t(common.home)",
"explore": "изучите вашу медиатеку",
"recentlyPlayed": "недавно прослушано"
"explore": "откройте новое",
"recentlyPlayed": "игралось недавно"
},
"albumDetail": {
"moreFromArtist": "больше из жанра $t(entity.genre_one)",
"moreFromGeneric": "больше из {{item}}"
"moreFromArtist": "больше от $t(entity.artist_one)",
"moreFromGeneric": "больше из {{item}}",
"released": "выпущен"
},
"setting": {
"playbackTab": "воспроизведение",
"generalTab": "общее",
"hotkeysTab": "горячие клавиши",
"windowTab": "окно"
"windowTab": "окно",
"advanced": "расширенные"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
"title": "$t(entity.genre_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)"
"title": "$t(entity.track_other)",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)"
},
"globalSearch": {
"commands": {
@@ -399,18 +468,39 @@
},
"title": "комманды"
},
"playlist": {
"reorder": "сортировка доступна только по ID"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
"title": "$t(entity.album_other)",
"artistAlbums": "альбомы {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
},
"albumArtistDetail": {
"topSongs": "популярные треки",
"viewAll": "посмотреть всё",
"appearsOn": "появляется в",
"viewDiscography": "посмотреть дискографию",
"relatedArtists": "похож на $t(entity.artist_many)",
"viewAllTracks": "посмотреть все $t(entity.track_other)",
"recentReleases": "недавние релизы",
"about": "О {{artist}}",
"topSongsFrom": "популярные треки из {{title}}"
},
"itemDetail": {
"copyPath": "скопировать путь в буфер обмена",
"openFile": "открыть трек в менеджере файлов",
"copiedPath": "путь успешно скопирован"
}
},
"form": {
"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)",
@@ -423,24 +513,24 @@
"addServer": {
"title": "добавить сервер",
"input_username": "пользователь",
"input_url": "url",
"input_url": "адрес",
"input_password": "пароль",
"input_legacyAuthentication": "включить старую аутентификацию",
"input_legacyAuthentication": "включить старую авторизацию",
"input_name": "название сервера",
"success": "сервер добавлен успешно",
"success": "сервер успешно добавлен",
"input_savePassword": "сохранить пароль",
"ignoreSsl": "ignore ssl $t(common.restartRequired)",
"ignoreCors": "$t(common.restartRequired)",
"error_savePassword": "произошла ошибка во время попытки сохранения пароля"
"ignoreSsl": "игнорировать ssl ($t(common.restartRequired))",
"ignoreCors": "игнорировать CORS ($t(common.restartRequired))",
"error_savePassword": "произошла ошибка при сохранении пароля"
},
"addToPlaylist": {
"success": "добавлено(а) {{message}} $t(entity.song_other) в {{numOfPlaylists}} $t(entity.playlist_other)",
"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": {
@@ -453,36 +543,223 @@
"title": "поиск слов песни"
},
"editPlaylist": {
"title": "редактировать $t(entity.playlist_one)"
"title": "редактировать $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) обновлён успешно"
},
"shareItem": {
"success": "ссылка скопирована в буфер обмена (нажмите здесь, чтобы открыть)",
"expireInvalid": "время истечения срока действия должно быть в будущем",
"createFailed": "не удалось создать ссылку для общего доступа (проверьте, включен ли общий доступ?)",
"allowDownloading": "разрешить скачивание",
"setExpiration": "установить срок действия",
"description": "описание"
}
},
"setting": {
"accentColor": "цвет акцента",
"accentColor_description": "устанавливает цвет акцента для приложения",
"albumBackground": "фоновое изображение альбомов",
"albumBackground_description": "добавляет фоновое изображение для страниц альбомов, содержащих обложку",
"albumBackgroundBlur": "размытие фонового изображения альбома",
"albumBackgroundBlur_description": "определяет степень размытия фонового изображения на странице альбомов",
"applicationHotkeys": "горячие клавиши приложения",
"crossfadeStyle_description": "Выберите вид эффекта crossfade для аудиоплеера",
"enableRemote_description": "Включает сервер удалённого управления для управления воспроизведением с помощью других устройств",
"fontType_optionSystem": "Системный шрифт",
"mpvExecutablePath_description": "Укажите папку, в которой находится исполняющий файл аудиоплеера MPV",
"crossfadeStyle": "Вид эффекта crossfade",
"fontType_optionBuiltIn": "Встроенный в приложение",
"disableLibraryUpdateOnStartup": "Отключить проверку новых версий при запуске приложения",
"minimizeToTray_description": "Сворачивать приложение в панель уведомлений",
"audioPlayer_description": "Укажите - какой аудиоплеер использовать для воспроизведения",
"disableAutomaticUpdates": "Отключить проверку обновлений",
"crossfadeStyle_description": "выберите вид эффекта crossfade для аудиоплеера",
"customCssEnable": "использовать кастомные css",
"customCssEnable_description": "разрешить использование кастомных css.",
"enableRemote_description": "включает сервер удалённого управления для управления воспроизведением с помощью других устройств",
"fontType_optionSystem": "системный",
"mpvExecutablePath_description": "укажите папку, в которой находится исполняющий файл аудиоплеера MPV. если оставить пустым, будет использоваться путь по умолчанию",
"crossfadeStyle": "вид эффекта crossfade",
"fontType_optionBuiltIn": "встроенный",
"disableLibraryUpdateOnStartup": "отключить проверку новых версий при запуске приложения",
"minimizeToTray_description": "сворачивать приложение в панель уведомлений",
"audioPlayer_description": "укажите, какой аудиоплеер использовать для воспроизведения",
"disableAutomaticUpdates": "отключить проверку обновлений",
"exitToTray_description": "При закрытии приложения - оно останется в панели уведомлений",
"fontType_optionCustom": "Пользовательский шрифт",
"remotePassword": "Пароль к серверу удалённого управления",
"fontType_optionCustom": "пользовательский",
"remotePassword": "пароль к серверу удалённого управления",
"font": "Шрифт",
"crossfadeDuration_description": "Укажите длительность эффекта crossfade",
"mpvExecutablePath": "Папка с аудиоплеером MPV",
"exitToTray": "Сворачивать в панель уведомлений при закрытии",
"enableRemote": "Включить сервер удалённого управления",
"fontType": "Источник шрифта",
"mpvExecutablePath": "папка с аудиоплеером MPV",
"exitToTray": "сворачивать в панель уведомлений при закрытии",
"enableRemote": "включить сервер удалённого управления",
"fontType": "тип шрифта",
"crossfadeDuration": "Длительность эффекта crossfade",
"audioPlayer": "Аудиоплеер",
"minimizeToTray": "Сворачивать в панель уведомлений",
"font_description": "Выберите - какой шрифт использовать в приложении",
"remoteUsername": "Имя пользователя для доступа к серверу удалённого управления"
"minimizeToTray": "сворачивать в панель уведомлений",
"font_description": "Выберите, какой шрифт использовать в приложении",
"remoteUsername": "имя пользователя для доступа к серверу удалённого управления",
"buttonSize_description": "размер кнопок в панели управления воспроизведением",
"clearCache": "очистить кэш браузера",
"clearQueryCache": "очистить кэш feishin",
"audioDevice": "устройство воспроизведения",
"audioDevice_description": "выберите устройство воспроизведения (только в режиме аудиоплеера web)",
"buttonSize": "размер кнопок панели управления воспроизведением",
"hotkey_volumeDown": "уменьшить громкость",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"theme_description": "устанавливает тему, которая будет использоваться в приложении",
"passwordStore": "хранилище паролей/секретов",
"sidebarPlaylistList": "список плейлистов в боковой панели",
"windowBarStyle_description": "выберите стиль заголовка окна",
"followLyric": "следовать за текстом трека",
"volumeWheelStep": "шаг регулировки громкости колёсиком мыши",
"windowBarStyle": "стиль заголовка окна",
"hotkey_zoomOut": "уменьшить масштаб",
"playbackStyle_optionCrossFade": "затухание",
"replayGainMode": "режим {{ReplayGain}}",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"clearQueryCache_description": "так называемая \"мягкая очистка\" feishin: обновляются плейлисты, метаданные треков, но сохранённые тексты треков сбрасываются. настройки, учётные данные и кэшированные изображения сохраняются",
"hotkey_favoriteCurrentSong": "добавить $t(common.currentSong) в избранное",
"genreBehavior": "поведения страницы жанров",
"globalMediaHotkeys": "глобальные мультимедийные горячие клавиши",
"hotkey_browserForward": "кнопка браузера \"вперёд\"",
"hotkey_favoritePreviousSong": "добавить $t(common.previousSong) в избранное",
"hotkey_globalSearch": "глобальный поиск",
"hotkey_playbackNext": "следующий трек",
"hotkey_playbackPause": "пауза",
"hotkey_playbackPlay": "играть",
"hotkey_playbackPlayPause": "играть / пауза",
"hotkey_playbackPrevious": "предыдущий трек",
"hotkey_playbackStop": "остановить",
"hotkey_rate0": "убрать оценку",
"hotkey_rate1": "оценить в 1 звезду",
"hotkey_rate2": "оценить в 2 звезды",
"hotkey_rate3": "оценить в 3 звезды",
"hotkey_rate4": "оценить в 4 звезды",
"hotkey_rate5": "оценить в 5 звёзд",
"hotkey_skipForward": "перемотать вперёд",
"hotkey_toggleCurrentSongFavorite": "добавить/удалить $t(common.currentSong) в избранное",
"hotkey_toggleFullScreenPlayer": "включение/выключение полноэкранного плеера",
"hotkey_togglePreviousSongFavorite": "добавить/удалить $t(common.previousSong) в избранное",
"hotkey_toggleRepeat": "переключить режим повтора",
"hotkey_toggleShuffle": "переключить перемешивание",
"hotkey_unfavoriteCurrentSong": "удалить $t(common.currentSong) из избранного",
"hotkey_unfavoritePreviousSong": "удалить $t(common.previousSong) из избранного",
"hotkey_volumeUp": "увеличить громкость",
"hotkey_zoomIn": "увеличить масштаб",
"language": "язык",
"lyricFetchProvider_description": "выберите источники для получения текстов песен. порядок источников соответствует очередности их запросов",
"minimumScrobblePercentage_description": "минимальный процент прослушивания трека, прежде чем он будет засчитан как прослушанный",
"minimumScrobbleSeconds_description": "минимальное время прослушивания трека в секундах, прежде чем он будет засчитан как прослушанный",
"playbackStyle_optionNormal": "нормальный",
"mpvExtraParameters": "параметры mpv",
"mpvExtraParameters_help": "по одному на строчку",
"playbackStyle_description": "выберите стиль воспроизведения, который будет использоваться аудиоплеером",
"playButtonBehavior_description": "устанавливает поведение кнопки воспроизведения при добавлении треков в очередь",
"playButtonBehavior": "поведение кнопки воспроизведения",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)",
"playerAlbumArtResolution_description": "разрешение большой версии обложки альбома в проигрывателе. при большем разрешении она выглядит более четкой, но может замедлить загрузку. по умолчанию равно 0 - устанавливает разрешение автоматически",
"playerbarOpenDrawer": "полноэкранный переключатель по панели проигрывателя",
"playerbarOpenDrawer_description": "позволяет перейти в полноэкранный режим воспроизведения нажатием на панель проигрывателя",
"remotePort": "порт сервера удалённого управления",
"remotePort_description": "устанавливает порт для сервера удалённого управления",
"replayGainClipping": "{{ReplayGain}} клиппинг",
"replayGainFallback": "{{ReplayGain}} по умолчанию",
"sampleRate_description": "выберите выходную частоту дискретизации, которая будет использоваться, если выбранная частота дискретизации отличается от частоты дискретизации текущего трека. при значении меньше 8000 будет использоваться частота по умолчанию",
"savePlayQueue_description": "сохранять очередь воспроизведения при закрытии приложения и восстанавливать при запуске приложения",
"showSkipButton_description": "показывать или скрывать кнопки перемотки на панели управления воспроизведением",
"sidebarConfiguration": "конфигурация боковой панели",
"sidebarConfiguration_description": "выбрать элементы и порядок их отображения на боковой панели",
"sidebarCollapsedNavigation": "кнопки навигации в боковой панели (в свёрнутом режиме)",
"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": "устанавливает процент масштабирования приложения",
"floatingQueueArea": "показать область наведения для всплывающей очереди",
"genreBehavior_description": "определяет, что отобразится при открытии на жанр — список треков или альбомов",
"globalMediaHotkeys_description": "включить или отключить использование системных мультимедийных горячих клавиш для управления воспроизведением",
"homeConfiguration_description": "позволяет настроить видимость и порядок элементов на домашней странице",
"homeFeature": "улучшенная карусель на главной",
"homeFeature_description": "определяет, показывать ли улучшенную карусель на главной странице",
"hotkey_toggleQueue": "показать/скрыть очередь воспроизведения",
"imageAspectRatio": "использовать оригинальное соотношение сторон обложки",
"imageAspectRatio_description": "если эта опция включена, обложки будут отображаться в соответствии с их собственным соотношением сторон. для обложек не 1:1 оставшееся пространство будет пустым",
"minimumScrobblePercentage": "минимальное время для скробблинга (в процентах)",
"playbackStyle": "стиль воспроизведения",
"playerAlbumArtResolution": "разрешение обложки альбома",
"remotePassword_description": "задает пароль для сервера удалённого управления. По умолчанию эти учетные данные передаются небезопасным способом, поэтому следует использовать уникальный пароль, который вам неважен",
"replayGainClipping_description": "Предотвращение клиппинга, вызванного {{ReplayGain}}, путём автоматического снижения усиления",
"replayGainFallback_description": "усиление в db для применения, если у файла нет тегов {{ReplayGain}}",
"replayGainMode_description": "регулировать усиление громкости в соответствии со значениями {{ReplayGain}}, хранящимися в метаданных файла",
"savePlayQueue": "сохранять очередь воспроизведения",
"showSkipButton": "показывать кнопки перемотки",
"theme": "тема",
"themeDark": "тема (тёмная)",
"externalLinks": "показывать ссылки на внешние ресурсы",
"gaplessAudio": "бесшовное воспроизведение",
"gaplessAudio_optionWeak": "слабое (рекомендуется)",
"gaplessAudio_description": "устанавливает настройку mpv для бесшовного воспроизведение",
"hotkey_browserBack": "кнопка браузера \"назад\"",
"hotkey_localSearch": "поиск на странице",
"hotkey_skipBackward": "перемотать назад",
"startMinimized": "запуск в свёрнутом режиме",
"themeDark_description": "устанавливает тёмную тему приложения",
"hotkey_volumeMute": "отключить звук",
"clearCache_description": "\"жесткая очистка\" feishin: кроме очистки кэша feishin, также очищает кэш браузера (сохранённые картинки и другие ресурсы). учётные данные и настройки сохраняются",
"clearCacheSuccess": "кэш успешно удалён",
"contextMenu": "конфигурация контекстного меню (нажатие правой кнопкой мыши)",
"contextMenu_description": "позволяет скрыть элементы, отображаемые в меню, появляющемся при нажатии правой кнопки мыши на элемент. все, что не отмечено, будет скрыто",
"customFontPath": "путь к пользовательскому шрифту",
"customFontPath_description": "укажите путь к пользовательскому шрифту, который будет использоваться в приложении",
"externalLinks_description": "включает отображение внешних ссылок (Last.fm, MusicBrainz) на страницах альбомов и артистов",
"floatingQueueArea_description": "включить отображение иконки наведения на правой части экрана, чтобы показать очередь воспроизведения",
"followLyric_description": "прокручивать текст трека до текущей позиции воспроизведения",
"language_description": "устанавливает язык приложения ($t(common.restartRequired))",
"lyricFetch_description": "получать тексты треков из различных интернет-источников",
"lyricFetchProvider": "источник текстов треков",
"minimumScrobbleSeconds": "минимальное время для скробблинга (в секундах)",
"replayGainPreamp": "предусиление {{ReplayGain}} (дБ)",
"sidebarCollapsedNavigation_description": "показать или скрыть кнопки навигации в свёрнутой боковой панели",
"homeConfiguration": "конфигурация домашней страницы",
"remoteUsername_description": "задает имя пользователя для сервера удалённого управления. если имя пользователя и пароль пусты, аутентификация будет отключена",
"scrobble": "скробблинг",
"replayGainPreamp_description": "настройка усиления предусилителя, применяемого к значениям {{ReplayGain}}",
"passwordStore_description": "какое хранилище паролей/секретов использовать. измените это значение, если у вас есть проблемы с хранением паролей.",
"lyricFetch": "получать тексты треков из интернета",
"sampleRate": "частота дискретизации",
"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}})",
"discordIdleStatus": "показывать состояние простоя",
"discordIdleStatus_description": "если включено, то обновляет статус, когда пользователь бездействует",
"discordUpdateInterval": "интервал обновления статуса профиля {{discord}}",
"discordUpdateInterval_description": "время в секундах между каждым обновлением (минимум 15 секунд)",
"doubleClickBehavior": "добавить в очередь все найденные треки при двойном клике",
"doubleClickBehavior_description": "есть включено: все найденные в поиске треки будут добавлены в очередь при двойном клике (иначе - только выбранный)",
"lyricOffset_description": "Смещение появления текста треков на указанное количество миллисекунд",
"skipPlaylistPage": "пропускать страницу плейлиста",
"applicationHotkeys_description": "настройка горячих клавиш приложения. поставьте галочку, чтобы сделать горячую клавишу глобальной (только для ПК)",
"artistConfiguration": "конфигурация страницы альбомов исполнителей",
"artistConfiguration_description": "позволяет настроить видимость и порядок элементов на странице альбомов исполнителей",
"fontType_description": "встроенный позволяет выбрать один из шрифтов, предоставляемых Feishin. системный позволяет выбрать любой шрифт, предоставляемый вашей операционной системой. пользовательский позволяет выбрать свой собственный шрифт",
"discordRichPresence_description": "включить статус воспроизведения в статус профиля в {{discord}}. Ключи изображений: {{icon}}, {{playing}} и {{paused}} ",
"lyricOffset": "синхронизация текста треков (мс)"
}
}
+8 -4
View File
@@ -41,7 +41,6 @@
"hotkey_playbackPause": "pauza",
"replayGainFallback": "{{ReplayGain}} alternativa",
"sidebarCollapsedNavigation_description": "prikaži ili sakrij navigaciju u sklopljenoj bočnoj traci",
"mpvExecutablePath_help": "po jedna po liniji",
"hotkey_volumeUp": "pojačaj glasnoću",
"skipDuration": "dužina preskakanja",
"discordIdleStatus_description": "kada je omogućeno, ažurira status dok je plejer u mirovanju",
@@ -210,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",
@@ -360,7 +363,8 @@
"albumArtist": "album artist",
"path": "putanja",
"discNumber": "disk",
"channels": "$t(common.channel_other)"
"channels": "$t(common.channel_other)",
"size": "$t(common.size)"
}
},
"error": {
@@ -559,7 +563,7 @@
"error_savePassword": "došlo je do greške prilikom pokušaja čuvanja lozinke"
},
"addToPlaylist": {
"success": "dodato {{message}} $t(entity.song_other) u {{numOfPlaylists}} $t(entity.playlist_other)",
"success": "dodato {{message}} $t(entity.track_other) u {{numOfPlaylists}} $t(entity.playlist_other)",
"title": "dodaj u $t(entity.playlist_one)",
"input_skipDuplicates": "preskoči duplikate",
"input_playlists": "$t(entity.playlist_other)"
+71 -8
View File
@@ -95,7 +95,8 @@
"random": "slumpmässig",
"size": "storlek",
"biography": "biografi",
"note": "anteckning"
"note": "anteckning",
"center": "center"
},
"error": {
"remotePortWarning": "starta om servern för att tillämpa den nya porten",
@@ -157,7 +158,9 @@
"toYear": "till år",
"fromYear": "från år",
"album": "$t(entity.album_one)",
"trackNumber": "spår"
"trackNumber": "spår",
"songCount": "sångräkning",
"criticRating": "kritikerbetyg"
},
"form": {
"deletePlaylist": {
@@ -187,7 +190,7 @@
"error_savePassword": "ett fel uppstod när lösenordet skulle sparas"
},
"addToPlaylist": {
"success": "tillade {{message}} $t(entity.song_other) til {{numOfPlaylists}} $t(entity.playlist_other)",
"success": "tillade {{message}} $t(entity.track_other) til {{numOfPlaylists}} $t(entity.playlist_other)",
"title": "lägg till i $t(entity.playlist_one)",
"input_skipDuplicates": "hoppa över dubbletter",
"input_playlists": "$t(entity.playlist_other)"
@@ -218,8 +221,13 @@
"opacity": "ogenomskinlighet",
"lyricSize": "låttext storlek",
"lyricAlignment": "låttext justering",
"lyricGap": "låttext mellanrum"
}
"lyricGap": "låttext mellanrum",
"synchronized": "synkroniserad",
"showLyricProvider": "visa sångtextleverantör",
"unsynchronized": "osynkroniserad"
},
"lyrics": "sångtext",
"related": "relaterad"
},
"appMenu": {
"selectServer": "välj server",
@@ -230,7 +238,8 @@
"openBrowserDevtools": "öppna webbläsarens utvecklingsverktyg",
"quit": "$t(common.quit)",
"goBack": "gå tillbaka",
"goForward": "gå framåt"
"goForward": "gå framåt",
"collapseSidebar": "växla sidofältet"
},
"contextMenu": {
"addToPlaylist": "$t(action.addToPlaylist)",
@@ -251,7 +260,7 @@
"removeFromQueue": "$t(action.removeFromQueue)"
},
"albumDetail": {
"moreFromArtist": "Mer från $t(entity.genre_one)",
"moreFromArtist": "mer från $t(entity.genre_one)",
"moreFromGeneric": "mer från {{item}}"
},
"albumArtistList": {
@@ -259,6 +268,29 @@
},
"albumList": {
"title": "$t(entity.album_other)"
},
"sidebar": {
"nowPlaying": "nu spelas"
},
"home": {
"mostPlayed": "mest spelade",
"newlyAdded": "nytillkomna utgåvor",
"explore": "utforska från ditt bibliotek",
"recentlyPlayed": "nyligen spelat"
},
"setting": {
"playbackTab": "uppspelning",
"generalTab": "allmänt",
"hotkeysTab": "snabbtangenter",
"windowTab": "fönster"
},
"globalSearch": {
"commands": {
"serverCommands": "serverkommandon",
"goToPage": "gå till sidan",
"searchFor": "sök efter {{query}}"
},
"title": "kommandon"
}
},
"entity": {
@@ -277,6 +309,37 @@
"folder_one": "mapp",
"folder_other": "mappar",
"album_one": "album",
"album_other": "album"
"album_other": "album",
"playlistWithCount_one": "{{count}} spellista",
"playlistWithCount_other": "{{count}} spellistor",
"folderWithCount_one": "{{count}} mapp",
"folderWithCount_other": "{{count}} mappar",
"track_one": "spår",
"track_other": "spår",
"trackWithCount_one": "{{count}} spår",
"trackWithCount_other": "{{count}} spår"
},
"player": {
"repeat_all": "repetera alla",
"repeat": "repetera",
"queue_remove": "ta bort markerad",
"playRandom": "spela slumpmässigt",
"previous": "föregående",
"favorite": "favorit",
"next": "nästa",
"shuffle": "blanda",
"playbackFetchNoResults": "inga låtar hittades",
"playbackFetchInProgress": "laddar låtar…",
"addNext": "lägg till nästa",
"playbackSpeed": "uppspelningshastighet",
"playbackFetchCancel": "det här tar ett tag... stäng aviseringen för att avbryta",
"play": "spela",
"repeat_off": "repetera inaktiverad",
"queue_clear": "rensa kö",
"muted": "mutad",
"queue_moveToTop": "flytta markerad till botten",
"queue_moveToBottom": "flytta markerad till toppen",
"addLast": "lägg till sist",
"mute": "muta"
}
}
+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": "மின்தடம்"
}
}
}
+236 -85
View File
@@ -1,7 +1,7 @@
{
"action": {
"editPlaylist": "编辑 $t(entity.playlist_one)",
"moveToTop": "至顶部",
"editPlaylist": "编辑$t(entity.playlist_one)",
"moveToTop": "至顶部",
"clearQueue": "清空播放队列",
"addToFavorites": "添加到$t(entity.favorite_other)",
"addToPlaylist": "添加到$t(entity.playlist_one)",
@@ -12,11 +12,16 @@
"deletePlaylist": "删除$t(entity.playlist_one)",
"removeFromQueue": "从播放队列中移除",
"deselectAll": "取消全选",
"moveToBottom": "至底部",
"moveToBottom": "至底部",
"setRating": "评分",
"toggleSmartPlaylistEditor": "切换$t(entity.smartPlaylist)编辑器",
"removeFromFavorites": "从$t(entity.favorite_other)移除",
"goToPage": "转到页面"
"goToPage": "前往页面",
"openIn": {
"lastfm": "在 Last.fm 中打开",
"musicbrainz": "在 MusicBrainz 中打开"
},
"moveToNext": "移至下一首"
},
"common": {
"increase": "增高",
@@ -63,8 +68,8 @@
"bitrate": "比特率",
"saveAndReplace": "保存并替换",
"action_other": "操作",
"confirm": "确",
"resetToDefault": "重置为默认",
"confirm": "确",
"resetToDefault": "重置为默认状态",
"home": "主页",
"comingSoon": "即将上线…",
"reset": "重置",
@@ -76,24 +81,35 @@
"quit": "退出",
"expand": "展开",
"search": "搜索",
"saveAs": "存为",
"saveAs": "存为",
"random": "随机",
"biography": "简介",
"sortOrder": "顺序",
"backward": "返回",
"backward": "后退",
"gap": "空隙",
"limit": "限制",
"duration": "时长",
"ok": "好",
"no": "否",
"playerMustBePaused": "播放器须暂停",
"playerMustBePaused": "播放器须暂停",
"channel_other": "频道",
"none": "无",
"disc": "",
"disc": "碟片",
"yes": "是",
"size": "大小",
"areYouSure": "是否继续",
"note": "注释"
"areYouSure": "是否确定",
"note": "注释",
"close": "关闭",
"albumPeak": "专辑峰值",
"mbid": "MusicBrainz ID",
"reload": "重载",
"trackGain": "音轨增益",
"trackPeak": "音轨峰值",
"albumGain": "专辑增益",
"codec": "编解码器",
"share": "分享",
"preview": "预览",
"translation": "翻译"
},
"entity": {
"albumArtist_other": "专辑艺术家",
@@ -111,16 +127,18 @@
"folder_other": "文件夹",
"smartPlaylist": "智能$t(entity.playlist_one)",
"genreWithCount_other": "{{count}} 种流派",
"trackWithCount_other": "{{count}} 首乐曲"
"trackWithCount_other": "{{count}} 首乐曲",
"play_other": "{{count}} 次播放",
"song_other": "歌曲"
},
"player": {
"repeat_all": "全部循环",
"repeat_all": "循环全部",
"stop": "停止",
"repeat": "循环",
"queue_remove": "移除所选",
"queue_remove": "移除所选",
"playRandom": "随机播放",
"skip": "跳过",
"previous": "一首",
"previous": "一首",
"toggleFullscreenPlayer": "全屏",
"skip_back": "向后跳过",
"favorite": "收藏",
@@ -131,25 +149,27 @@
"addNext": "添加为播放列表下一首",
"playbackFetchCancel": "请稍等…关闭通知以取消操作",
"play": "播放",
"repeat_off": "循环",
"repeat_off": "循环关闭",
"queue_clear": "清空播放队列",
"muted": "已静音",
"unfavorite": "取消收藏",
"queue_moveToTop": "使所选置底",
"queue_moveToBottom": "使所选置顶",
"shuffle_off": "未启用随机播放",
"addLast": "添加播放列表末尾",
"queue_moveToTop": "所选项移至底部",
"queue_moveToBottom": "所选项移至顶部",
"shuffle_off": "用随机播放",
"addLast": "添加播放列表末尾",
"mute": "静音",
"skip_forward": "向前跳过",
"playbackSpeed": "播放速度",
"pause": "暂停"
"pause": "暂停",
"playSimilarSongs": "播放类似的曲目",
"viewQueue": "查看播放队列"
},
"setting": {
"crossfadeStyle_description": "选择用于音频播放器的淡入淡出风格",
"hotkey_favoriteCurrentSong": "收藏 $t(common.currentSong)",
"hotkey_favoriteCurrentSong": "收藏$t(common.currentSong)",
"crossfadeStyle": "淡入淡出风格",
"audioExclusiveMode_description": "启用独占输出模式。在此模式下,系统通常被锁定只有 mpv 能够输出音频",
"disableLibraryUpdateOnStartup": "禁用启动时查新版本",
"audioExclusiveMode_description": "启用独占输出模式。在此模式下,系统通常被锁定只有 mpv 能够输出音频",
"disableLibraryUpdateOnStartup": "禁用启动时查新版本",
"gaplessAudio": "无缝音频",
"audioPlayer_description": "选择用于播放的音频播放器",
"globalMediaHotkeys": "全局媒体快捷键",
@@ -176,48 +196,48 @@
"audioDevice_description": "选择用于播放的音频设备(仅 web 播放器)",
"enableRemote_description": "启用远程控制服务器,以允许其他设备控制此应用",
"remotePort_description": "设置远程服务器端口",
"hotkey_skipBackward": "向跳过",
"hotkey_skipBackward": "向跳过",
"replayGainMode_description": "根据乐曲元数据中存储的{{ReplayGain}}值调整音量增益",
"volumeWheelStep_description": "在音量滑块上滚动鼠标滚轮时要更改的音量大小",
"theme_description": "设置应用的主题",
"hotkey_playbackPause": "暂停",
"replayGainFallback": "{{ReplayGain}}后备替代",
"replayGainFallback": "{{ReplayGain}}后备方案",
"sidebarCollapsedNavigation_description": "在折叠的侧边栏中显示或隐藏导航",
"hotkey_volumeUp": "音量增高",
"skipDuration": "跳过时长",
"showSkipButtons": "显示跳过按钮",
"playButtonBehavior_optionPlay": "$t(player.play)",
"minimumScrobblePercentage": "最小 scrobble 时长(百分比)",
"minimumScrobblePercentage": "最小记录时长(百分比)",
"lyricFetch": "从互联网获取歌词",
"scrobble": "记录播放信息Scrobble",
"scrobble": "记录播放信息",
"skipDuration_description": "设置每次按下跳过按钮将会跳过的时长",
"fontType_optionSystem": "系统字体",
"mpvExecutablePath_description": "设置 mpv 二进制文件的路径",
"mpvExecutablePath_description": "设置 mpv 可执行文件的路径。如果留空,则使用默认路径",
"sampleRate": "采样率",
"sidePlayQueueStyle_optionAttached": "吸附",
"sidebarConfiguration": "侧边栏设定",
"sampleRate_description": "所选的采样率与当前媒体的频率不同时,用于输出采样率",
"sampleRate_description": "如果选择的采样率与当前媒体的采样频率不同,请选择要使用的输出采样率。小于 8000 的值将使用默认频率",
"replayGainMode_optionNone": "$t(common.none)",
"hotkey_zoomIn": "放大",
"scrobble_description": "在你的社交媒体中记录播放信息",
"scrobble_description": "在你的媒体服务器中记录播放信息",
"hotkey_browserForward": "浏览器前进",
"themeLight": "主题(浅色)",
"fontType_optionBuiltIn": "内置字体",
"hotkey_playbackPlayPause": "播放/暂停",
"hotkey_rate1": "评为 1 星",
"hotkey_skipForward": "向跳过",
"hotkey_skipForward": "向跳过",
"sidePlayQueueStyle": "侧边播放列表样式",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"zoom": "放率",
"zoom": "放率",
"minimizeToTray_description": "将应用程序最小化到系统托盘",
"hotkey_playbackPlay": "播放",
"hotkey_togglePreviousSongFavorite": "收藏 / 取消收藏$t(common.previousSong)",
"hotkey_togglePreviousSongFavorite": "切换收藏$t(common.previousSong)",
"hotkey_volumeDown": "音量降低",
"hotkey_unfavoritePreviousSong": "取消收藏$t(common.previousSong)",
"hotkey_globalSearch": "全局搜索",
"remoteUsername_description": "设置远程控制服务器的用户名。如果用户名和密码都为空,则身份验证将被禁用",
"exitToTray_description": "退出应用时最小化到系统托盘而非关闭",
"hotkey_favoritePreviousSong": "收藏 $t(common.previousSong)",
"exitToTray_description": "退出应用时最小化到系统托盘",
"hotkey_favoritePreviousSong": "收藏$t(common.previousSong)",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"lyricOffset": "歌词偏移(毫秒)",
"fontType_optionCustom": "自定义字体",
@@ -225,49 +245,49 @@
"remotePassword": "远程控制服务器密码",
"lyricFetchProvider": "歌词源",
"language_description": "设置应用的语言($t(common.restartRequired)",
"playbackStyle_optionCrossFade": "交叉淡入淡出",
"playbackStyle_optionCrossFade": "淡入淡出",
"hotkey_rate3": "评为 3 星",
"mpvExtraParameters": "mpv 参数",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"themeLight_description": "应用将使用浅色主题",
"hotkey_toggleFullScreenPlayer": "全屏播放",
"hotkey_localSearch": "页面内搜索",
"hotkey_toggleQueue": "显示 / 隐藏播放队列",
"zoom_description": "设置应用的放大率",
"hotkey_toggleQueue": "切换播放队列",
"zoom_description": "设置应用程序的缩放率",
"remotePassword_description": "设置远程控制服务器的密码。这些凭据默认以不安全的方式传输,因此您应该使用一个您不在意的唯一密码",
"hotkey_rate5": "评为 5 星",
"hotkey_playbackPrevious": "上一",
"showSkipButtons_description": "在播放条显示/隐藏播放按钮",
"hotkey_playbackPrevious": "上一",
"showSkipButtons_description": "在播放条显示隐藏播放按钮",
"language": "语言",
"playbackStyle": "播放风格",
"hotkey_toggleShuffle": "切换随机播放设定",
"hotkey_toggleShuffle": "切换随机",
"theme": "主题",
"playbackStyle_description": "选择播放器的播放风格",
"mpvExecutablePath": "mpv 二进制文件路径",
"playbackStyle_description": "选择音频播放器的播放风格",
"mpvExecutablePath": "mpv 可执行文件路径",
"hotkey_rate2": "评为 2 星",
"playButtonBehavior_description": "设置将歌曲添加到队列时播放按钮的默认行为",
"minimumScrobblePercentage_description": "歌曲被记录为已播放scrobble所需的最小播放百分比",
"playButtonBehavior_description": "设置将歌曲添加到播放队列时播放按钮的默认行为",
"minimumScrobblePercentage_description": "歌曲被记录为已播放所需的最小播放百分比",
"exitToTray": "退出时最小化到托盘",
"hotkey_rate4": "评为 4 星",
"showSkipButton_description": "在播放条上显示/隐藏跳过按钮",
"savePlayQueue": "保存播放列",
"minimumScrobbleSeconds_description": "歌曲被记录为已播放scrobble所需的最小播放时间",
"showSkipButton_description": "在播放条上显示隐藏跳过按钮",
"savePlayQueue": "保存播放列",
"minimumScrobbleSeconds_description": "歌曲被记录为已播放所需的最小播放时间",
"skipPlaylistPage_description": "打开歌单时,直接查看歌曲列表而非查看默认页面",
"fontType_description": "内置字体可以选择 Feishin 提供的字体之一。系统字体允许您选择操作系统提供的任何字体。自定义选项允许您使用自己的字体",
"playButtonBehavior": "播放按钮行为",
"volumeWheelStep": "音量滚轮步长",
"volumeWheelStep": "音量滚轮分度",
"sidebarPlaylistList_description": "显示或隐藏侧边栏歌单列表",
"sidePlayQueueStyle_description": "设置侧边播放列表样式",
"replayGainMode": "{{ReplayGain}}模式",
"playbackStyle_optionNormal": "常",
"playbackStyle_optionNormal": "常",
"windowBarStyle": "窗口顶栏风格",
"floatingQueueArea": "显示浮动队列悬停区域",
"replayGainFallback_description": "乐曲没有{{ReplayGain}}标签时应用的增益(以分贝为单位)",
"hotkey_toggleRepeat": "切换循环播放设定",
"hotkey_toggleRepeat": "切换循环",
"lyricOffset_description": "将歌词偏移指定的毫秒数",
"sidebarConfiguration_description": "选择侧边栏包含的项目与顺序",
"remotePort": "远程服务器端口",
"hotkey_playbackNext": "下一",
"hotkey_playbackNext": "下一",
"useSystemTheme_description": "使用系统定义的浅色或深色主题",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"lyricFetch_description": "从多个互联网源获取歌词",
@@ -278,21 +298,20 @@
"hotkey_rate0": "清除评分",
"floatingQueueArea_description": "在屏幕右侧显示一个悬停图标,以查看播放队列",
"hotkey_volumeMute": "静音",
"hotkey_toggleCurrentSongFavorite": "收藏 / 取消收藏$t(common.currentSong)",
"remoteUsername": "远程服务器用户名",
"hotkey_toggleCurrentSongFavorite": "切换收藏$t(common.currentSong)",
"remoteUsername": "远程控制服务器用户名",
"hotkey_browserBack": "浏览器后退",
"showSkipButton": "显示跳过按钮",
"sidebarPlaylistList": "侧边栏歌单列表",
"minimizeToTray": "最小化到托盘",
"skipPlaylistPage": "跳过歌单页面",
"skipPlaylistPage": "跳过播放列表页面",
"themeDark": "主题(深色)",
"sidebarCollapsedNavigation": "侧边栏(已折叠)导航",
"minimumScrobbleSeconds": "最小 scrobble 时间(秒)",
"minimumScrobbleSeconds": "最小记录时间(秒)",
"hotkey_playbackStop": "停止",
"windowBarStyle_description": "选择窗口顶栏的风格",
"savePlayQueue_description": "当应用程序关闭时保存播放队列,并在应用程序打开时恢复它",
"useSystemTheme": "跟随系统",
"mpvExecutablePath_help": "每行一个",
"discordIdleStatus_description": "启用后将会在播放器闲置时更新状态",
"replayGainClipping_description": "自动降低增益以防止{{ReplayGain}}造成削波",
"replayGainPreamp": "{{ReplayGain}}前置放大(分贝)",
@@ -305,7 +324,72 @@
"accentColor_description": "设置应用的强调色",
"replayGainPreamp_description": "调整应用在{{ReplayGain}}值上的前置放大增益",
"discordIdleStatus": "显示 rich presence 闲置状态",
"discordRichPresence": "{{discord}} rich presence"
"discordRichPresence": "{{discord}} rich presence",
"clearCache": "清除浏览器缓存",
"buttonSize": "播放器栏按钮大小",
"buttonSize_description": "播放器栏按钮大小",
"clearCache_description": "feishin的“硬清除”。除了清除feishin的缓存,清空浏览器缓存(保存的图像和其他资源)。服务器凭据和设置会被保留",
"clearQueryCache_description": "feishin的“软清除”。这将会刷新播放列表、元数据并重置保存的歌词。设置、服务器凭据和缓存图像会被保留",
"clearQueryCache": "清除feishin缓存",
"externalLinks": "显示外部链接",
"externalLinks_description": "允许在艺术家/专辑页面上显示外部链接(Last.fm、MusicBrainz",
"mpvExtraParameters_help": "每行一个",
"startMinimized": "启动最小化",
"startMinimized_description": "在系统托盘中启动应用程序",
"passwordStore_description": "使用什么密码/密钥存储。如果您在存储密码时遇到问题,请更改此设置。",
"clearCacheSuccess": "缓存清除成功",
"playerAlbumArtResolution": "播放器专辑封面分辨率",
"playerAlbumArtResolution_description": "大型播放器专辑封面预览的分辨率。较大使其看起来更清晰,但可能会减慢加载速度。默认为0,表示自动",
"genreBehavior": "类型页面默认行为",
"genreBehavior_description": "确定单击流派是否默认在曲目或专辑列表中打开",
"homeConfiguration": "主页配置",
"homeConfiguration_description": "配置主页上显示的项目以及显示顺序",
"passwordStore": "密码/密钥存储",
"homeFeature_description": "控制是否在主页上显示大型特色轮播",
"homeFeature": "首页 精选 轮播",
"imageAspectRatio": "保留封面图像纵横比",
"imageAspectRatio_description": "如果启用,封面图像将保留纵横比显示。对于不是1:1的图像,剩余的空间将是空的",
"doubleClickBehavior_description": "如果为真,则曲目搜索中所有匹配的曲目都将被加入播放队列。否则,只有单击的曲目才会被加入播放队列",
"doubleClickBehavior": "双击时将所有搜索到的曲目加入播放队列",
"volumeWidth": "音量滑块宽度",
"volumeWidth_description": "音量滑块的宽度",
"discordListening": "显示状态为正在监听",
"discordListening_description": "将状态显示为正在监听,而不是正在播放",
"contextMenu_description": "允许您隐藏右键单击项目时显示在菜单中的项目。未选中的项目将被隐藏",
"customCssEnable_description": "允许编写自定义 css。",
"customCss": "自定义css",
"customCss_description": "自定义css内容。注意:内容和远程url是不允许的属性。内容预览展示如下。出于安全考虑,您未设置的其它字段也会显示。",
"contextMenu": "上下文菜单(右键单击)配置",
"customCssEnable": "启用自定义 css",
"customCssNotice": "警告:虽然预设了一些安全限制(不允许 url() 和 content:),但使用自定义 CSS 仍然会因更改界面而带来风险。",
"transcodeNote": "1web-2mpv)首歌曲后生效",
"transcode": "启用转码",
"transcode_description": "可以转码为不同的格式",
"transcodeBitrate": "转码比特率",
"albumBackground": "专辑背景图片",
"albumBackground_description": "为包含专辑封面的专辑页面添加背景图像",
"albumBackgroundBlur": "专辑背景图像模糊大小",
"albumBackgroundBlur_description": "调整相册背景图片的模糊程度",
"playerbarOpenDrawer": "播放器栏全屏切换",
"playerbarOpenDrawer_description": "允许点击播放器栏打开全屏播放器",
"transcodeBitrate_description": "选择要转码的比特率。0 表示让服务器选择",
"transcodeFormat": "转码格式",
"transcodeFormat_description": "选择要转码的格式。留空让服务器决定",
"webAudio_description": "使用 web 音频。这将启用重播增益等高级功能。如果您遇到其他情况,请禁用",
"artistConfiguration_description": "配置专辑艺术家页面上显示的项目及其显示顺序",
"webAudio": "使用 web 音频",
"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": "重启服务器使新端口生效",
@@ -315,7 +399,7 @@
"remotePortError": "设置远程服务器端口时发生错误",
"serverRequired": "需要服务器",
"authenticationFailed": "认证失败",
"apiRouteError": "请求失败:无法路由",
"apiRouteError": "无法路由请求",
"genericError": "发生了错误",
"credentialsRequired": "需要凭证",
"sessionExpiredError": "会话已过期",
@@ -326,10 +410,13 @@
"mpvRequired": "需要 MPV",
"audioDeviceFetchError": "无法获取音频设备",
"invalidServer": "无效的服务器",
"loginRateError": "登录请求尝试次数过多,请稍后再试"
"loginRateError": "登录请求尝试次数过多,请稍后再试",
"badAlbum": "您看到此页面是因为这首歌不是专辑的一部分。如果您的音乐文件夹顶层有一首歌曲,您很可能会遇到此问题。jellyfin 仅对位于文件夹中的曲目进行分组。",
"networkError": "发生网络错误",
"openError": "无法打开文件"
},
"filter": {
"mostPlayed": "播放最多",
"mostPlayed": "最多播放过",
"playCount": "播放次数",
"recentlyPlayed": "最近播放",
"title": "标题",
@@ -346,7 +433,7 @@
"albumArtist": "$t(entity.albumArtist_one)",
"releaseYear": "发布年份",
"biography": "个人简介",
"songCount": "曲目数",
"songCount": "曲目数",
"random": "随机",
"lastPlayed": "上次播放过",
"toYear": "从年份",
@@ -368,7 +455,7 @@
"note": "注释",
"albumCount": "$t(entity.album_other)数",
"id": "id",
"disc": "",
"disc": "碟片",
"duration": "时长",
"album": "$t(entity.album_one)"
},
@@ -384,7 +471,8 @@
"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)"
},
"fullscreenPlayer": {
"config": {
@@ -398,11 +486,16 @@
"lyricAlignment": "歌词对齐",
"useImageAspectRatio": "使用图片纵横比",
"lyricGap": "歌词间距",
"followCurrentLyric": "跟随当前歌词"
"followCurrentLyric": "跟随当前歌词",
"dynamicImageBlur": "图像模糊大小",
"dynamicIsImage": "启用背景图像",
"lyricOffset": "歌词延迟补偿(毫秒)"
},
"lyrics": "歌词",
"related": "相关",
"upNext": "即将播放"
"upNext": "即将播放",
"visualizer": "可视化",
"noLyrics": "未找到歌词"
},
"appMenu": {
"selectServer": "选择服务器",
@@ -425,19 +518,21 @@
},
"albumDetail": {
"moreFromArtist": "更多该$t(entity.artist_one)作品",
"moreFromGeneric": "更多{{item}}作品"
"moreFromGeneric": "更多{{item}}作品",
"released": "已发布"
},
"setting": {
"playbackTab": "播放",
"generalTab": "通用",
"hotkeysTab": "快捷键",
"windowTab": "窗口"
"windowTab": "窗口",
"advanced": "高级"
},
"globalSearch": {
"commands": {
"serverCommands": "服务器命令",
"goToPage": "跳至页面",
"searchFor": "搜索 {{query}}"
"searchFor": "搜索{{query}}"
},
"title": "命令"
},
@@ -457,22 +552,61 @@
"addNext": "$t(player.addNext)",
"deselectAll": "$t(action.deselectAll)",
"addLast": "$t(player.addLast)",
"addFavorite": "$t(action.addToFavorites)"
"addFavorite": "$t(action.addToFavorites)",
"showDetails": "获取信息",
"shareItem": "分享项目",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "下载",
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"trackList": {
"title": "$t(entity.track_other)"
"title": "$t(entity.track_other)",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
"artistTracks": "{{artist}}的曲目"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
"title": "$t(entity.album_other)",
"artistAlbums": "{{artist}}的专辑",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
"title": "$t(entity.genre_other)",
"showAlbums": "显示$t(entity.genre_one) $t(entity.album_other)",
"showTracks": "显示$t(entity.genre_one) $t(entity.track_other)"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"albumArtistDetail": {
"recentReleases": "最近发布",
"viewDiscography": "查看唱片目录",
"relatedArtists": "相关$t(entity.artist_other)",
"topSongs": "热门歌曲",
"topSongsFrom": "{{title}}的热门歌曲",
"viewAllTracks": "查看所有$t(entity.track_other)",
"about": "关于{{artist}}",
"appearsOn": "出现在",
"viewAll": "查看全部"
},
"itemDetail": {
"copyPath": "将路径复制到剪贴板",
"copiedPath": "路径复制成功",
"openFile": "在文件管理器中显示曲目"
},
"playlist": {
"reorder": "仅在按 ID 排序时启用重排序"
},
"manageServers": {
"url": "URL",
"title": "管理服务器",
"serverDetails": "服务器详细信息",
"username": "用户名",
"editServerDetailsTooltip": "编辑服务器详细信息",
"removeServer": "移除服务器"
}
},
"form": {
@@ -486,7 +620,7 @@
"input_username": "用户名",
"input_password": "密码",
"input_legacyAuthentication": "启用旧版认证方式",
"input_name": "服务器名",
"input_name": "服务器名",
"success": "服务器添加成功",
"input_savePassword": "保存密码",
"ignoreSsl": "忽略 ssl $t(common.restartRequired)",
@@ -495,7 +629,7 @@
"input_url": "url"
},
"addToPlaylist": {
"success": "添加 {{message}} $t(entity.song_other) 到 {{numOfPlaylists}} $t(entity.playlist_other)",
"success": "添加$t(entity.trackWithCount, {\"count\": {{message}} })到$t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "添加到$t(entity.playlist_one)",
"input_skipDuplicates": "跳过重复",
"input_playlists": "$t(entity.playlist_other)"
@@ -517,22 +651,35 @@
"input_optionMatchAny": "匹配任何"
},
"editPlaylist": {
"title": "编辑$t(entity.playlist_one)"
"title": "编辑$t(entity.playlist_one)",
"publicJellyfinNote": "Jellyfin 出于某种原因不会显示播放列表是否公开。如果您希望保持公开,请选择以下输入",
"success": "$t(entity.playlist_one)更新成功"
},
"lyricSearch": {
"title": "搜索歌词",
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)"
},
"shareItem": {
"expireInvalid": "过期时间必须是将来的时间",
"createFailed": "创建共享失败(是否已启用共享?)",
"allowDownloading": "允许下载",
"description": "描述",
"setExpiration": "设置过期时间",
"success": "共享链接已复制到剪贴板(或单击此处打开)"
}
},
"table": {
"config": {
"general": {
"displayType": "显示风格",
"displayType": "显示类型",
"gap": "$t(common.gap)",
"tableColumns": "列",
"autoFitColumns": "列宽自适应",
"size": "$t(common.size)"
"size": "$t(common.size)",
"itemGap": "项目间隙(px",
"itemSize": "项目大小 (px)",
"followCurrentSong": "关注当前播放的歌曲"
},
"view": {
"table": "表格",
@@ -548,7 +695,7 @@
"bpm": "$t(common.bpm)",
"lastPlayed": "最后播放",
"trackNumber": "音轨编号",
"rowIndex": "行",
"rowIndex": "行索引",
"rating": "$t(common.rating)",
"artist": "$t(entity.artist_one)",
"album": "$t(entity.album_one)",
@@ -557,7 +704,7 @@
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"channels": "$t(common.channel_other)",
"playCount": "播放数",
"playCount": "播放数",
"bitrate": "$t(common.bitrate)",
"actions": "$t(common.action_other)",
"genre": "$t(entity.genre_one)",
@@ -565,13 +712,15 @@
"favorite": "$t(common.favorite)",
"year": "$t(common.year)",
"albumArtist": "$t(entity.albumArtist_one)",
"titleCombined": "$t(common.title)(合并)"
"titleCombined": "$t(common.title)(合并)",
"codec": "$t(common.codec)",
"songCount": "$t(entity.track_other)"
}
},
"column": {
"comment": "评论",
"album": "专辑",
"rating": "评",
"rating": "评",
"favorite": "收藏",
"playCount": "播放次数",
"albumCount": "$t(entity.album_other)",
@@ -590,7 +739,9 @@
"albumArtist": "专辑艺术家",
"path": "路径",
"channels": "$t(common.channel_other)",
"discNumber": "盘"
"discNumber": "碟片",
"size": "$t(common.size)",
"codec": "$t(common.codec)"
}
}
}
+601 -1
View File
@@ -1 +1,601 @@
{}
{
"common": {
"backward": "返回",
"biography": "簡介",
"bitrate": "比特率",
"bpm": "bpm",
"clear": "清空",
"collapse": "折疊",
"comingSoon": "即將上線…",
"confirm": "確認",
"decrease": "降低",
"delete": "刪除",
"descending": "降序",
"description": "描述",
"forceRestartRequired": "重啓應用使更改生效…關閉通知即可重啓",
"menu": "菜單",
"action_other": "操作",
"add": "添加",
"areYouSure": "是否繼續?",
"ascending": "升序",
"disable": "禁用",
"disc": "盤",
"dismiss": "忽略",
"duration": "時長",
"edit": "編輯",
"enable": "啓用",
"expand": "展開",
"favorite": "收藏",
"filter_other": "篩選",
"filters": "篩選",
"forward": "前進",
"gap": "空隙",
"home": "主頁",
"increase": "增高",
"left": "左",
"limit": "限制",
"manage": "管理",
"maximize": "最大化",
"ok": "好",
"owner": "所有者",
"path": "路徑",
"playerMustBePaused": "播放器須被暫停",
"previousSong": "上壹首$t(entity.track_one)",
"quit": "退出",
"random": "隨機",
"rating": "評分",
"refresh": "刷新",
"reset": "重置",
"resetToDefault": "重置爲默認",
"restartRequired": "需要重啓應用",
"right": "右",
"save": "保存",
"saveAndReplace": "保存並替換",
"saveAs": "保存爲",
"search": "搜索",
"sortOrder": "順序",
"title": "標題",
"trackNumber": "音軌編號",
"unknown": "未知",
"size": "大小",
"version": "版本",
"year": "年份",
"yes": "是",
"cancel": "取消",
"center": "中央",
"channel_other": "頻道",
"configure": "配置",
"create": "創建",
"currentSong": "當前$t(entity.track_one)",
"minimize": "最小化",
"modified": "已修改",
"name": "名稱",
"no": "否",
"none": "無",
"noResultsFromQuery": "未查詢到匹配結果",
"note": "注釋"
},
"error": {
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
"apiRouteError": "請求失敗:無法路由",
"audioDeviceFetchError": "無法獲取音頻設備",
"authenticationFailed": "認證失敗",
"credentialsRequired": "需要憑證",
"genericError": "發生了錯誤",
"invalidServer": "無效的服務器",
"localFontAccessDenied": "無法獲取本地字體",
"loginRateError": "登錄請求嘗試次數過多,請稍後再試",
"remoteDisableError": "$t(common.disable)遠程服務器時出現錯誤",
"remoteEnableError": "$t(common.enable)遠程服務器時出現錯誤",
"remotePortError": "設置遠程服務器端口時發生錯誤",
"remotePortWarning": "重啓服務器使新端口生效",
"serverRequired": "需要服務器",
"sessionExpiredError": "會話已過期",
"systemFontError": "獲取系統字體時出現錯誤",
"serverNotSelectedError": "未選擇服務器",
"mpvRequired": "需要 MPV",
"playbackError": "無法播放媒體"
},
"page": {
"contextMenu": {
"removeFromFavorites": "$t(action.removeFromFavorites)",
"addToFavorites": "$t(action.addToFavorites)",
"addToPlaylist": "$t(action.addToPlaylist)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromQueue": "$t(action.removeFromQueue)",
"addFavorite": "$t(action.addToFavorites)",
"addLast": "$t(player.addLast)",
"addNext": "$t(player.addNext)",
"createPlaylist": "$t(action.createPlaylist)",
"deletePlaylist": "$t(action.deletePlaylist)",
"deselectAll": "$t(action.deselectAll)",
"moveToBottom": "$t(action.moveToBottom)",
"setRating": "$t(action.setRating)",
"moveToTop": "$t(action.moveToTop)",
"numberSelected": "{{count}} 已選擇",
"play": "$t(player.play)"
},
"globalSearch": {
"title": "命令",
"commands": {
"goToPage": "跳至頁面",
"searchFor": "搜索 {{query}}",
"serverCommands": "服務器命令"
}
},
"home": {
"explore": "從庫中搜索",
"recentlyPlayed": "最近播放",
"title": "$t(common.home)",
"mostPlayed": "最多播放",
"newlyAdded": "最近添加的發布"
},
"appMenu": {
"openBrowserDevtools": "打開浏覽器開發者工具",
"collapseSidebar": "折疊側邊欄",
"expandSidebar": "展開側邊欄",
"goBack": "返回",
"goForward": "前進",
"quit": "$t(common.quit)",
"selectServer": "選擇服務器",
"settings": "$t(common.setting_other)",
"version": "版本 {{version}}",
"manageServers": "管理服務器"
},
"fullscreenPlayer": {
"config": {
"showLyricProvider": "顯示歌詞提供者",
"useImageAspectRatio": "使用圖片縱橫比",
"dynamicBackground": "動態背景",
"followCurrentLyric": "跟隨當前歌詞",
"lyricAlignment": "歌詞對齊",
"lyricGap": "歌詞間距",
"lyricSize": "歌詞字體大小",
"synchronized": "已同步",
"unsynchronized": "未同步",
"opacity": "透明度",
"showLyricMatch": "顯示匹配的歌詞"
},
"lyrics": "歌詞",
"related": "相關",
"upNext": "即將播放"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"setting": {
"hotkeysTab": "快捷鍵",
"playbackTab": "播放",
"windowTab": "窗口",
"generalTab": "通用"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"albumDetail": {
"moreFromArtist": "更多該$t(entity.artist_one)作品",
"moreFromGeneric": "更多{{item}}作品"
},
"albumList": {
"title": "$t(entity.album_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
},
"sidebar": {
"albumArtists": "$t(entity.albumArtist_other)",
"albums": "$t(entity.album_other)",
"artists": "$t(entity.artist_other)",
"folders": "$t(entity.folder_other)",
"search": "$t(common.search)",
"settings": "$t(common.setting_other)",
"tracks": "$t(entity.track_other)",
"genres": "$t(entity.genre_other)",
"home": "$t(common.home)",
"nowPlaying": "正在播放",
"playlists": "$t(entity.playlist_other)"
},
"trackList": {
"title": "$t(entity.track_other)"
}
},
"player": {
"playbackFetchInProgress": "正在加載歌曲…",
"addLast": "添加到播放列表末尾",
"addNext": "添加爲播放列表下壹首",
"favorite": "收藏",
"mute": "靜音",
"muted": "已靜音",
"playbackFetchNoResults": "未找到歌曲",
"playbackSpeed": "播放速度",
"playRandom": "隨機播放",
"previous": "上壹首",
"queue_clear": "清空播放隊列",
"queue_remove": "移除所選",
"repeat": "循環",
"repeat_all": "全部循環",
"repeat_off": "不循環",
"shuffle": "隨機播放",
"shuffle_off": "未啓用隨機播放",
"skip": "跳過",
"skip_back": "向後跳過",
"skip_forward": "向前跳過",
"stop": "停止",
"toggleFullscreenPlayer": "全屏",
"unfavorite": "取消收藏",
"pause": "暫停",
"next": "下壹首",
"play": "播放",
"playbackFetchCancel": "請稍等…關閉通知以取消操作",
"queue_moveToBottom": "使所選置頂",
"queue_moveToTop": "使所選置底"
},
"setting": {
"audioPlayer_description": "選擇用于播放的音頻播放器",
"themeLight": "主題(淺色)",
"themeLight_description": "應用將使用淺色主題",
"discordRichPresence": "{{discord}} rich presence",
"hotkey_volumeDown": "音量降低",
"hotkey_volumeMute": "靜音",
"minimumScrobblePercentage": "最小 scrobble 時長(百分比)",
"minimumScrobblePercentage_description": "歌曲被記錄爲已播放(scrobble)所需的最小播放百分比",
"theme_description": "設置應用的主題",
"accentColor": "強調色",
"accentColor_description": "設置應用的強調色",
"applicationHotkeys": "應用快捷鍵",
"applicationHotkeys_description": "配置應用快捷鍵。勾選設爲全局快捷鍵(僅桌面端)",
"audioDevice": "音頻設備",
"audioDevice_description": "選擇用于播放的音頻設備(僅 web 播放器)",
"audioExclusiveMode": "音頻獨占模式",
"audioExclusiveMode_description": "啓用獨占輸出模式。在此模式下,系統通常被鎖定,只有 mpv 能夠輸出音頻",
"audioPlayer": "音頻播放器",
"crossfadeDuration": "淡入淡出持續時間",
"crossfadeDuration_description": "設置淡入淡出持續時間",
"crossfadeStyle": "淡入淡出風格",
"crossfadeStyle_description": "選擇用于音頻播放器的淡入淡出風格",
"customFontPath": "自定義字體路徑",
"customFontPath_description": "設置應用使用的自定義字體路徑",
"disableAutomaticUpdates": "禁用自動更新",
"disableLibraryUpdateOnStartup": "禁用啓動時查找新版本",
"discordApplicationId": "{{discord}} 應用 id",
"discordApplicationId_description": "{{discord}} rich presence 應用 id(默認爲 {{defaultId}}",
"discordIdleStatus": "顯示 rich presence 閑置狀態",
"discordIdleStatus_description": "啓用後將會在播放器閑置時更新狀態",
"discordRichPresence_description": "在 {{discord}} rich presence 中顯示播放狀態。圖片鍵爲:{{icon}}、{{playing}} 和 {{paused}} ",
"discordUpdateInterval": "{{discord}} rich presence 更新間隔",
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
"enableRemote": "啓用遠程控制服務器",
"enableRemote_description": "啓用遠程控制服務器,以允許其他設備控制此應用",
"exitToTray": "退出時最小化到托盤",
"floatingQueueArea_description": "在屏幕右側顯示壹個懸停圖標,以查看播放隊列",
"followLyric": "跟隨當前歌詞",
"font_description": "設置應用使用的字體",
"fontType": "字體類型",
"fontType_description": "內置字體可以選擇 Feishin 提供的字體之壹。系統字體允許您選擇操作系統提供的任何字體。自定義選項允許您使用自己的字體",
"fontType_optionBuiltIn": "內置字體",
"fontType_optionCustom": "自定義字體",
"fontType_optionSystem": "系統字體",
"gaplessAudio": "無縫音頻",
"gaplessAudio_description": "調整 mpv 無縫音頻設置",
"gaplessAudio_optionWeak": "弱(推薦)",
"globalMediaHotkeys": "全局媒體快捷鍵",
"hotkey_browserForward": "浏覽器前進",
"hotkey_favoritePreviousSong": "收藏 $t(common.previousSong)",
"hotkey_globalSearch": "全局搜索",
"hotkey_localSearch": "頁面內搜索",
"hotkey_playbackNext": "下壹曲",
"hotkey_playbackPause": "暫停",
"hotkey_playbackPlay": "播放",
"hotkey_playbackPlayPause": "播放/暫停",
"hotkey_playbackPrevious": "上壹曲",
"hotkey_rate2": "評爲 2 星",
"hotkey_rate1": "評爲 1 星",
"hotkey_rate3": "評爲 3 星",
"hotkey_rate4": "評爲 4 星",
"hotkey_rate5": "評爲 5 星",
"hotkey_skipBackward": "向回跳過",
"hotkey_skipForward": "向後跳過",
"hotkey_toggleCurrentSongFavorite": "收藏 / 取消收藏$t(common.currentSong)",
"hotkey_toggleFullScreenPlayer": "全屏播放",
"hotkey_togglePreviousSongFavorite": "收藏 / 取消收藏$t(common.previousSong)",
"hotkey_toggleQueue": "顯示 / 隱藏播放隊列",
"hotkey_toggleRepeat": "切換循環播放設定",
"hotkey_toggleShuffle": "切換隨機播放設定",
"hotkey_unfavoriteCurrentSong": "取消收藏$t(common.currentSong)",
"hotkey_unfavoritePreviousSong": "取消收藏$t(common.previousSong)",
"hotkey_zoomIn": "放大",
"hotkey_zoomOut": "縮小",
"language": "語言",
"language_description": "設置應用的語言($t(common.restartRequired)",
"lyricFetch": "從互聯網獲取歌詞",
"lyricFetch_description": "從多個互聯網源獲取歌詞",
"lyricFetchProvider": "歌詞源",
"lyricOffset": "歌詞偏移(毫秒)",
"lyricOffset_description": "將歌詞偏移指定的毫秒數",
"lyricFetchProvider_description": "選擇歌詞源。 歌詞源順序與查詢順序壹致",
"minimizeToTray": "最小化到托盤",
"minimizeToTray_description": "將應用程序最小化到系統托盤",
"minimumScrobbleSeconds": "最小 scrobble 時間(秒)",
"minimumScrobbleSeconds_description": "歌曲被記錄爲已播放(scrobble)所需的最小播放時間",
"mpvExecutablePath": "mpv 二進制文件路徑",
"playbackStyle_optionCrossFade": "交叉淡入淡出",
"playbackStyle_optionNormal": "通常",
"playButtonBehavior": "播放按鈕行爲",
"playButtonBehavior_description": "設置將歌曲添加到隊列時播放按鈕的默認行爲",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"remotePort": "遠程服務器端口",
"remoteUsername": "遠程服務器用戶名",
"replayGainClipping": "{{ReplayGain}}削波",
"replayGainFallback": "{{ReplayGain}}後備替代",
"replayGainFallback_description": "樂曲沒有{{ReplayGain}}標簽時應用的增益(以分貝爲單位)",
"replayGainMode": "{{ReplayGain}}模式",
"replayGainMode_description": "根據樂曲元數據中存儲的{{ReplayGain}}值調整音量增益",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"replayGainPreamp": "{{ReplayGain}}前置放大(分貝)",
"replayGainPreamp_description": "調整應用在{{ReplayGain}}值上的前置放大增益",
"savePlayQueue": "保存播放列表",
"sampleRate_description": "如果選擇的采樣頻率與當前媒體的采樣頻率不同,請選擇要使用的輸出采樣率。小于 8000 的值將使用默認頻率",
"savePlayQueue_description": "當應用程序關閉時保存播放隊列,並在應用程序打開時恢複它",
"scrobble": "記錄播放信息(Scrobble",
"scrobble_description": "在妳的社交媒體中記錄播放信息",
"showSkipButton": "顯示跳過按鈕",
"showSkipButton_description": "在播放條上顯示/隱藏跳過按鈕",
"sidebarPlaylistList": "側邊欄歌單列表",
"sidebarCollapsedNavigation": "側邊欄(已折疊)導航",
"sidebarCollapsedNavigation_description": "在折疊的側邊欄中顯示或隱藏導航",
"sidebarConfiguration": "側邊欄設定",
"sidebarConfiguration_description": "選擇側邊欄包含的項目與順序",
"sidebarPlaylistList_description": "顯示或隱藏側邊欄歌單列表",
"sidePlayQueueStyle": "側邊播放列表樣式",
"sidePlayQueueStyle_description": "設置側邊播放列表樣式",
"sidePlayQueueStyle_optionAttached": "吸附",
"sidePlayQueueStyle_optionDetached": "不吸附",
"skipDuration": "跳過時長",
"skipDuration_description": "設置每次按下跳過按鈕將會跳過的時長",
"skipPlaylistPage": "跳過歌單頁面",
"skipPlaylistPage_description": "打開歌單時,直接查看歌曲列表而非查看默認頁面",
"theme": "主題",
"themeDark": "主題(深色)",
"useSystemTheme_description": "使用系統定義的淺色或深色主題",
"useSystemTheme": "跟隨系統",
"volumeWheelStep": "音量滾輪步長",
"volumeWheelStep_description": "在音量滑塊上滾動鼠標滾輪時要更改的音量大小",
"windowBarStyle": "窗口頂欄風格",
"windowBarStyle_description": "選擇窗口頂欄的風格",
"zoom": "縮放率",
"zoom_description": "設置應用程序的縮放率",
"hotkey_volumeUp": "音量增高",
"sampleRate": "采樣率",
"showSkipButtons_description": "在播放條顯示/隱藏播放按鈕",
"playbackStyle": "播放風格",
"exitToTray_description": "退出應用時最小化到系統托盤而非關閉",
"floatingQueueArea": "顯示浮動隊列懸停區域",
"followLyric_description": "滾動歌詞到當前播放位置",
"font": "字體",
"globalMediaHotkeys_description": "啓用或禁用系統媒體快捷鍵以控制播放",
"hotkey_browserBack": "浏覽器後退",
"hotkey_favoriteCurrentSong": "收藏 $t(common.currentSong)",
"hotkey_playbackStop": "停止",
"hotkey_rate0": "清除評分",
"mpvExecutablePath_description": "設置 mpv 二進制文件的路徑。如果留空,則使用默認路徑",
"mpvExtraParameters": "mpv 參數",
"playbackStyle_description": "選擇播放器的播放風格",
"playButtonBehavior_optionPlay": "$t(player.play)",
"remotePassword": "遠程控制服務器密碼",
"remotePassword_description": "設置遠程控制服務器的密碼。這些憑據默認以不安全的方式傳輸,因此您應該使用壹個您不在意的唯壹密碼",
"remotePort_description": "設置遠程服務器端口",
"remoteUsername_description": "設置遠程控制服務器的用戶名。如果用戶名和密碼都爲空,則身份驗證將被禁用",
"replayGainClipping_description": "自動降低增益以防止{{ReplayGain}}造成削波",
"showSkipButtons": "顯示跳過按鈕",
"themeDark_description": "應用將使用深色主題",
"clearQueryCache_description": "feishin的“軟清除”。這將會刷新播放列表、元數據並重置保存的歌詞。會保留設置、服務器憑據和緩存圖像",
"clearCache": "清除浏覽器緩存",
"clearCache_description": "feishin的“硬清除”。除了清除feishin的緩存,清空浏覽器緩存(保存的圖像和其他資源)。會保留服務器憑據和設置",
"clearQueryCache": "清除feishin緩存",
"buttonSize": "播放器欄按鈕大小",
"buttonSize_description": "播放器欄按鈕大小"
},
"table": {
"config": {
"general": {
"displayType": "顯示風格",
"gap": "$t(common.gap)",
"size": "$t(common.size)",
"tableColumns": "列",
"autoFitColumns": "列寬自適應"
},
"label": {
"actions": "$t(common.action_other)",
"album": "$t(entity.album_one)",
"albumArtist": "$t(entity.albumArtist_one)",
"artist": "$t(entity.artist_one)",
"bpm": "$t(common.bpm)",
"biography": "$t(common.biography)",
"bitrate": "$t(common.bitrate)",
"channels": "$t(common.channel_other)",
"dateAdded": "添加日期",
"discNumber": "碟片編號",
"duration": "$t(common.duration)",
"favorite": "$t(common.favorite)",
"genre": "$t(entity.genre_one)",
"lastPlayed": "最後播放",
"note": "$t(common.note)",
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"playCount": "播放數",
"releaseDate": "發布日期",
"rowIndex": "行號",
"size": "$t(common.size)",
"title": "$t(common.title)",
"titleCombined": "$t(common.title)(合並)",
"trackNumber": "音軌編號",
"year": "$t(common.year)",
"rating": "$t(common.rating)"
},
"view": {
"card": "卡片",
"poster": "海報",
"table": "表格"
}
},
"column": {
"album": "專輯",
"albumArtist": "專輯藝術家",
"albumCount": "$t(entity.album_other)",
"artist": "$t(entity.artist_one)",
"biography": "簡介",
"bitrate": "比特率",
"channels": "$t(common.channel_other)",
"comment": "評論",
"dateAdded": "添加日期",
"discNumber": "盤",
"favorite": "收藏",
"lastPlayed": "最後播放",
"path": "路徑",
"playCount": "播放次數",
"rating": "評價",
"releaseDate": "發布日期",
"releaseYear": "年份",
"genre": "$t(entity.genre_one)",
"bpm": "bpm",
"songCount": "$t(entity.track_other)",
"title": "標題",
"trackNumber": "音軌編號",
"size": "$t(common.size)"
}
},
"action": {
"addToFavorites": "添加到$t(entity.favorite_other)",
"clearQueue": "清空播放隊列",
"createPlaylist": "創建$t(entity.playlist_one)",
"deletePlaylist": "刪除$t(entity.playlist_one)",
"addToPlaylist": "添加到$t(entity.playlist_one)",
"deselectAll": "取消全選",
"editPlaylist": "編輯 $t(entity.playlist_one)",
"goToPage": "轉到頁面",
"moveToBottom": "跳至底部",
"moveToTop": "跳至頂部",
"refresh": "$t(common.refresh)",
"removeFromFavorites": "從$t(entity.favorite_other)移除",
"removeFromPlaylist": "從$t(entity.playlist_one)移除",
"removeFromQueue": "從播放隊列中移除",
"setRating": "評分",
"toggleSmartPlaylistEditor": "切換$t(entity.smartPlaylist)編輯器",
"viewPlaylists": "查看$t(entity.playlist_other)"
},
"entity": {
"album_other": "專輯",
"albumArtist_other": "專輯藝術家",
"albumArtistCount_other": "{{count}} 位專輯藝術家",
"artist_other": "藝術家",
"artistWithCount_other": "{{count}} 位藝術家",
"favorite_other": "收藏",
"folder_other": "文件夾",
"folderWithCount_other": "{{count}} 個文件夾",
"genre_other": "流派",
"genreWithCount_other": "{{count}} 種流派",
"playlist_other": "播放列表",
"playlistWithCount_other": "{{count}} 個播放列表",
"smartPlaylist": "智能$t(entity.playlist_one)",
"track_other": "樂曲",
"trackWithCount_other": "{{count}} 首樂曲",
"albumWithCount_other": "{{count}} 張專輯"
},
"filter": {
"albumCount": "$t(entity.album_other)數",
"artist": "$t(entity.artist_one)",
"biography": "個人簡介",
"bitrate": "比特率",
"bpm": "bpm",
"channels": "$t(common.channel_other)",
"comment": "評論",
"communityRating": "社區評分",
"criticRating": "評論家評分",
"dateAdded": "已添加日期",
"disc": "盤",
"duration": "時長",
"id": "id",
"fromYear": "從年份",
"genre": "$t(entity.genre_one)",
"isCompilation": "爲合輯",
"isFavorited": "已收藏",
"isPublic": "已公開",
"isRated": "已評分",
"name": "名稱",
"note": "注釋",
"isRecentlyPlayed": "最近播放過",
"lastPlayed": "上次播放過",
"mostPlayed": "播放最多",
"owner": "$t(common.owner)",
"path": "路徑",
"playCount": "播放次數",
"random": "隨機",
"rating": "評分",
"recentlyPlayed": "最近播放",
"recentlyUpdated": "最近更新",
"releaseDate": "發布日期",
"songCount": "曲目數",
"album": "$t(entity.album_one)",
"albumArtist": "$t(entity.albumArtist_one)",
"favorited": "已收藏",
"recentlyAdded": "最近添加",
"releaseYear": "發布年份",
"search": "搜索",
"title": "標題",
"toYear": "從年份",
"trackNumber": "曲目"
},
"form": {
"addServer": {
"input_legacyAuthentication": "啓用舊版認證方式",
"input_name": "服務器名",
"input_password": "密碼",
"input_savePassword": "保存密碼",
"input_url": "url",
"input_username": "用戶名",
"success": "服務器添加成功",
"title": "添加服務器",
"error_savePassword": "保存密碼時出現錯誤",
"ignoreCors": "忽略 cors $t(common.restartRequired)",
"ignoreSsl": "忽略 ssl $t(common.restartRequired)"
},
"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)"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"input_name": "$t(common.name)",
"input_owner": "$t(common.owner)",
"input_public": "公開",
"success": "已成功創建 $t(entity.playlist_one)",
"title": "創建$t(entity.playlist_one)"
},
"lyricSearch": {
"input_name": "$t(common.name)",
"title": "搜索歌詞",
"input_artist": "$t(entity.artist_one)"
},
"queryEditor": {
"input_optionMatchAll": "匹配全部",
"input_optionMatchAny": "匹配任何"
},
"updateServer": {
"success": "服務器已更新成功",
"title": "更新服務器"
},
"deletePlaylist": {
"input_confirm": "輸入$t(entity.playlist_one)的名稱進行確認",
"title": "刪除$t(entity.playlist_one)",
"success": "$t(entity.playlist_one)已成功刪除"
},
"editPlaylist": {
"title": "編輯$t(entity.playlist_one)"
}
}
}
+1 -1
View File
@@ -72,7 +72,7 @@ const getRemoteLyrics = async (song: QueueSong) => {
const params = {
album: song.album || song.name,
artist: song.artistName,
duration: song.duration,
duration: song.duration / 1000.0,
name: song.name,
};
const response = await FETCHERS[source](params);
+365 -116
View File
@@ -1,7 +1,12 @@
import console from 'console';
import { ipcMain } from 'electron';
import { getMpvInstance } from '../../../main';
import { PlayerData } from '/@/renderer/store';
import { rm } from 'fs/promises';
import { pid } from 'node:process';
import { app, ipcMain } from 'electron';
import uniq from 'lodash/uniq';
import MpvAPI from 'node-mpv';
import { getMainWindow, sendToastToRenderer } from '../../../main';
import { createLog, isWindows } from '../../../utils';
import { store } from '../settings';
declare module 'node-mpv';
@@ -13,6 +18,221 @@ declare module 'node-mpv';
// });
// }
let mpvInstance: MpvAPI | null = null;
const socketPath = isWindows() ? `\\\\.\\pipe\\mpvserver-${pid}` : `/tmp/node-mpv-${pid}.sock`;
const NodeMpvErrorCode = {
0: 'Unable to load file or stream',
1: 'Invalid argument',
2: 'Binary not found',
3: 'IPC command invalid',
4: 'Unable to bind IPC socket',
5: 'Connection timeout',
6: 'MPV is already running',
7: 'Could not send IPC message',
8: 'MPV is not running',
9: 'Unsupported protocol',
};
type NodeMpvError = {
errcode: number;
method: string;
stackTrace: string;
verbose: string;
};
const mpvLog = (
data: { action: string; toast?: 'info' | 'success' | 'warning' },
err?: NodeMpvError,
) => {
const { action, toast } = data;
if (err) {
const message = `[AUDIO PLAYER] ${action} - mpv errorcode ${err.errcode} - ${
NodeMpvErrorCode[err.errcode as keyof typeof NodeMpvErrorCode]
}`;
sendToastToRenderer({ message, type: 'error' });
createLog({ message, type: 'error' });
}
const message = `[AUDIO PLAYER] ${action}`;
createLog({ message, type: 'error' });
if (toast) {
sendToastToRenderer({ message, type: toast });
}
};
const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined;
const prefetchPlaylistParams = [
'--prefetch-playlist=no',
'--prefetch-playlist=yes',
'--prefetch-playlist',
];
const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => {
const parameters = ['--idle=yes', '--no-config', '--load-scripts=no'];
if (!extraParameters?.some((param) => prefetchPlaylistParams.includes(param))) {
parameters.push('--prefetch-playlist=yes');
}
return parameters;
};
const createMpv = async (data: {
binaryPath?: string;
extraParameters?: string[];
properties?: Record<string, any>;
}): Promise<MpvAPI> => {
const { extraParameters, properties, binaryPath } = data;
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
const mpv = new MpvAPI(
{
audio_only: true,
auto_restart: false,
binary: binaryPath || MPV_BINARY_PATH || undefined,
socket: socketPath,
time_update: 1,
},
params,
);
try {
await mpv.start();
} catch (error: any) {
console.log('mpv failed to start', error);
} finally {
await mpv.setMultipleProperties(properties || {});
}
mpv.on('status', (status) => {
if (status.property === 'playlist-pos') {
if (status.value === -1) {
mpv?.stop();
}
if (status.value !== 0) {
getMainWindow()?.webContents.send('renderer-player-auto-next');
}
}
});
// Automatically updates the play button when the player is playing
mpv.on('resumed', () => {
getMainWindow()?.webContents.send('renderer-player-play');
});
// Automatically updates the play button when the player is stopped
mpv.on('stopped', () => {
getMainWindow()?.webContents.send('renderer-player-stop');
});
// Automatically updates the play button when the player is paused
mpv.on('paused', () => {
getMainWindow()?.webContents.send('renderer-player-pause');
});
// Event output every interval set by time_update, used to update the current time
mpv.on('timeposition', (time: number) => {
getMainWindow()?.webContents.send('renderer-player-current-time', time);
});
return mpv;
};
export const getMpvInstance = () => {
return mpvInstance;
};
const quit = async () => {
const instance = getMpvInstance();
if (instance) {
await instance.quit();
if (!isWindows()) {
await rm(socketPath);
}
}
};
const setAudioPlayerFallback = (isError: boolean) => {
getMainWindow()?.webContents.send('renderer-player-fallback', isError);
};
ipcMain.on('player-set-properties', async (_event, data: Record<string, any>) => {
mpvLog({ action: `Setting properties: ${JSON.stringify(data)}` });
if (data.length === 0) {
return;
}
try {
if (data.length === 1) {
getMpvInstance()?.setProperty(Object.keys(data)[0], Object.values(data)[0]);
} else {
getMpvInstance()?.setMultipleProperties(data);
}
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to set properties: ${JSON.stringify(data)}` }, err);
}
});
ipcMain.handle(
'player-restart',
async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
try {
mpvLog({
action: `Attempting to initialize mpv with parameters: ${JSON.stringify(data)}`,
});
// Clean up previous mpv instance
getMpvInstance()?.stop();
getMpvInstance()
?.quit()
.catch((error) => {
mpvLog({ action: 'Failed to quit existing MPV' }, error);
});
mpvInstance = null;
mpvInstance = await createMpv(data);
mpvLog({ action: 'Restarted mpv', toast: 'success' });
setAudioPlayerFallback(false);
} catch (err: NodeMpvError | any) {
mpvLog({ action: 'Failed to restart mpv, falling back to web player' }, err);
setAudioPlayerFallback(true);
}
},
);
ipcMain.handle(
'player-initialize',
async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
try {
mpvLog({
action: `Attempting to initialize mpv with parameters: ${JSON.stringify(data)}`,
});
mpvInstance = await createMpv(data);
setAudioPlayerFallback(false);
} catch (err: NodeMpvError | any) {
mpvLog({ action: 'Failed to initialize mpv, falling back to web player' }, err);
setAudioPlayerFallback(true);
}
},
);
ipcMain.on('player-quit', async () => {
try {
await getMpvInstance()?.stop();
await quit();
} catch (err: NodeMpvError | any) {
mpvLog({ action: 'Failed to quit mpv' }, err);
} finally {
mpvInstance = null;
}
});
ipcMain.handle('player-is-running', async () => {
return getMpvInstance()?.isRunning();
});
@@ -23,183 +243,212 @@ ipcMain.handle('player-clean-up', async () => {
});
ipcMain.on('player-start', async () => {
await getMpvInstance()
?.play()
.catch((err) => {
console.log('MPV failed to play', err);
});
try {
await getMpvInstance()?.play();
} catch (err: NodeMpvError | any) {
mpvLog({ action: 'Failed to start mpv playback' }, err);
}
});
// Starts the player
ipcMain.on('player-play', async () => {
await getMpvInstance()
?.play()
.catch((err) => {
console.log('MPV failed to play', err);
});
try {
await getMpvInstance()?.play();
} catch (err: NodeMpvError | any) {
mpvLog({ action: 'Failed to start mpv playback' }, err);
}
});
// Pauses the player
ipcMain.on('player-pause', async () => {
await getMpvInstance()
?.pause()
.catch((err) => {
console.log('MPV failed to pause', err);
});
try {
await getMpvInstance()?.pause();
} catch (err: NodeMpvError | any) {
mpvLog({ action: 'Failed to pause mpv playback' }, err);
}
});
// Stops the player
ipcMain.on('player-stop', async () => {
await getMpvInstance()
?.stop()
.catch((err) => {
console.log('MPV failed to stop', err);
});
try {
await getMpvInstance()?.stop();
} catch (err: NodeMpvError | any) {
mpvLog({ action: 'Failed to stop mpv playback' }, err);
}
});
// Goes to the next track in the playlist
ipcMain.on('player-next', async () => {
await getMpvInstance()
?.next()
.catch((err) => {
console.log('MPV failed to go to next', err);
});
try {
await getMpvInstance()?.next();
} catch (err: NodeMpvError | any) {
mpvLog({ action: 'Failed to go to next track' }, err);
}
});
// Goes to the previous track in the playlist
ipcMain.on('player-previous', async () => {
await getMpvInstance()
?.prev()
.catch((err) => {
console.log('MPV failed to go to previous', err);
});
try {
await getMpvInstance()?.prev();
} catch (err: NodeMpvError | any) {
mpvLog({ action: 'Failed to go to previous track' }, err);
}
});
// Seeks forward or backward by the given amount of seconds
ipcMain.on('player-seek', async (_event, time: number) => {
await getMpvInstance()
?.seek(time)
.catch((err) => {
console.log('MPV failed to seek', err);
});
try {
await getMpvInstance()?.seek(time);
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to seek by ${time} seconds` }, err);
}
});
// Seeks to the given time in seconds
ipcMain.on('player-seek-to', async (_event, time: number) => {
await getMpvInstance()
?.goToPosition(time)
.catch((err) => {
console.log(`MPV failed to seek to ${time}`, err);
});
try {
await getMpvInstance()?.goToPosition(time);
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to seek to ${time} seconds` }, err);
}
});
// Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons
ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) => {
if (!data.queue.current && !data.queue.next) {
await getMpvInstance()
?.clearPlaylist()
.catch((err) => {
console.log('MPV failed to clear playlist', err);
});
await getMpvInstance()
?.pause()
.catch((err) => {
console.log('MPV failed to pause', err);
});
return;
ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, pause?: boolean) => {
if (!current && !next) {
try {
await getMpvInstance()?.clearPlaylist();
await getMpvInstance()?.pause();
return;
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to clear play queue` }, err);
}
}
try {
if (data.queue.current) {
await getMpvInstance()
?.load(data.queue.current.streamUrl, 'replace')
.catch((err) => {
console.log('MPV failed to load song', err);
getMpvInstance()?.play();
});
if (current) {
try {
await getMpvInstance()?.load(current, 'replace');
} catch (error) {
await getMpvInstance()?.play();
}
if (data.queue.next) {
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
if (next) {
await getMpvInstance()?.load(next, 'append');
}
}
} catch (err) {
console.error(err);
}
if (pause) {
getMpvInstance()?.pause();
if (pause) {
await getMpvInstance()?.pause();
} else if (pause === false) {
// Only force play if pause is explicitly false
await getMpvInstance()?.play();
}
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to set play queue` }, err);
}
});
// Replaces the queue in position 1 to the given data
ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => {
const size = await getMpvInstance()
?.getPlaylistSize()
.catch((err) => {
console.log('MPV failed to get playlist size', err);
});
ipcMain.on('player-set-queue-next', async (_event, url?: string) => {
try {
const size = await getMpvInstance()?.getPlaylistSize();
if (!size) {
return;
}
if (!size) {
return;
}
if (size > 1) {
await getMpvInstance()
?.playlistRemove(1)
.catch((err) => {
console.log('MPV failed to remove song from playlist', err);
});
}
if (size > 1) {
await getMpvInstance()?.playlistRemove(1);
}
if (data.queue.next) {
await getMpvInstance()
?.load(data.queue.next.streamUrl, 'append')
.catch((err) => {
console.log('MPV failed to load next song', err);
});
if (url) {
await getMpvInstance()?.load(url, 'append');
}
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to set play queue` }, err);
}
});
// Sets the next song in the queue when reaching the end of the queue
ipcMain.on('player-auto-next', async (_event, data: PlayerData) => {
ipcMain.on('player-auto-next', async (_event, url?: string) => {
// Always keep the current song as position 0 in the mpv queue
// This allows us to easily set update the next song in the queue without
// disturbing the currently playing song
await getMpvInstance()
?.playlistRemove(0)
.catch((err) => {
console.log('MPV failed to remove song from playlist', err);
getMpvInstance()?.pause();
});
if (data.queue.next) {
try {
await getMpvInstance()
?.load(data.queue.next.streamUrl, 'append')
.catch((err) => {
console.log('MPV failed to load next song', err);
?.playlistRemove(0)
.catch(() => {
getMpvInstance()?.pause();
});
if (url) {
await getMpvInstance()?.load(url, 'append');
}
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to load next song` }, err);
}
});
// Sets the volume to the given value (0-100)
ipcMain.on('player-volume', async (_event, value: number) => {
await getMpvInstance()
?.volume(value)
.catch((err) => {
console.log('MPV failed to set volume', err);
});
try {
if (!value || value < 0 || value > 100) {
return;
}
await getMpvInstance()?.volume(value);
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to set volume to ${value}` }, err);
}
});
// Toggles the mute status
ipcMain.on('player-mute', async (_event, mute: boolean) => {
await getMpvInstance()
?.mute(mute)
.catch((err) => {
console.log('MPV failed to toggle mute', err);
});
try {
await getMpvInstance()?.mute(mute);
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to set mute status` }, err);
}
});
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
return getMpvInstance()?.getTimePosition();
try {
return getMpvInstance()?.getTimePosition();
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to get current time` }, err);
return 0;
}
});
enum MpvState {
STARTED,
IN_PROGRESS,
DONE,
}
let mpvState = MpvState.STARTED;
app.on('before-quit', async (event) => {
switch (mpvState) {
case MpvState.DONE:
return;
case MpvState.IN_PROGRESS:
event.preventDefault();
break;
case MpvState.STARTED: {
try {
mpvState = MpvState.IN_PROGRESS;
event.preventDefault();
await getMpvInstance()?.stop();
await quit();
} catch (err: NodeMpvError | any) {
mpvLog({ action: `Failed to cleanly before-quit` }, err);
} finally {
mpvState = MpvState.DONE;
app.quit();
}
break;
}
}
});
+22 -1
View File
@@ -1,7 +1,28 @@
/* eslint-disable promise/always-return */
import { BrowserWindow, globalShortcut } from 'electron';
import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
import { isMacOS } from '../../../utils';
import { store } from '../settings';
export const enableMediaKeys = (window: BrowserWindow | null) => {
if (isMacOS()) {
const shouldPrompt = store.get('should_prompt_accessibility', true) as boolean;
const shownWarning = store.get('shown_accessibility_warning', false) as boolean;
const trusted = systemPreferences.isTrustedAccessibilityClient(shouldPrompt);
if (shouldPrompt) {
store.set('should_prompt_accessibility', false);
}
if (!trusted && !shownWarning) {
window?.webContents.send('toast-from-main', {
message:
'Feishin is not a trusted accessibility client. Media keys will not work until this setting is changed',
type: 'warning',
});
store.set('shown_accessibility_warning', true);
}
}
globalShortcut.register('MediaStop', () => {
window?.webContents.send('renderer-player-stop');
});
+59 -55
View File
@@ -8,9 +8,10 @@ import { app, ipcMain } from 'electron';
import { Server as WsServer, WebSocketServer, WebSocket } from 'ws';
import manifest from './manifest.json';
import { ClientEvent, ServerEvent } from '../../../../remote/types';
import { PlayerRepeat, SongUpdate } from '../../../../renderer/types';
import { PlayerRepeat, PlayerStatus, SongState } from '../../../../renderer/types';
import { getMainWindow } from '../../../main';
import { isLinux } from '../../../utils';
import type { QueueSong } from '/@/renderer/api/types';
let mprisPlayer: any | undefined;
@@ -33,13 +34,14 @@ interface MimeType {
js: string;
}
interface StatefulWebSocket extends WebSocket {
declare class StatefulWebSocket extends WebSocket {
alive: boolean;
auth: boolean;
}
let server: Server | undefined;
let wsServer: WsServer<StatefulWebSocket> | undefined;
let wsServer: WsServer<typeof StatefulWebSocket> | undefined;
const settings: RemoteConfig = {
enabled: false,
@@ -100,9 +102,7 @@ enum Encoding {
const GZIP_REGEX = /\bgzip\b/;
const ZLIB_REGEX = /bdeflate\b/;
let currentSong: SongUpdate = {
currentTime: 0,
};
const currentState: SongState = {};
const getEncoding = (encoding: string | string[]): Encoding => {
const encodingArray = Array.isArray(encoding) ? encoding : [encoding];
@@ -328,9 +328,9 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
});
server.listen(config.port, resolve);
wsServer = new WebSocketServer({ server });
wsServer = new WebSocketServer<typeof StatefulWebSocket>({ server });
wsServer.on('connection', (ws) => {
wsServer!.on('connection', (ws: StatefulWebSocket) => {
let authFail: number | undefined;
ws.alive = true;
@@ -388,7 +388,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
break;
}
case 'proxy': {
const toFetch = currentSong.song?.imageUrl?.replaceAll(
const toFetch = currentState.song?.imageUrl?.replaceAll(
/&(size|width|height=\d+)/g,
'',
);
@@ -438,9 +438,9 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
volume = 0;
}
currentSong.volume = volume;
currentState.volume = volume;
broadcast({ data: { volume }, event: 'song' });
broadcast({ data: volume, event: 'volume' });
getMainWindow()?.webContents.send('request-volume', {
volume,
});
@@ -452,26 +452,35 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
}
case 'favorite': {
const { favorite, id } = json;
if (id && id === currentSong.song?.id) {
if (id && id === currentState.song?.id) {
getMainWindow()?.webContents.send('request-favorite', {
favorite,
id,
serverId: currentSong.song.serverId,
serverId: currentState.song.serverId,
});
}
break;
}
case 'rating': {
const { rating, id } = json;
if (id && id === currentSong.song?.id) {
if (id && id === currentState.song?.id) {
getMainWindow()?.webContents.send('request-rating', {
id,
rating,
serverId: currentSong.song.serverId,
serverId: currentState.song.serverId,
});
}
break;
}
case 'position': {
const { position } = json;
if (mprisPlayer) {
mprisPlayer.getPosition = () => position * 1e6;
}
getMainWindow()?.webContents.send('request-position', {
position,
});
}
}
} catch (error) {
console.error(error);
@@ -482,7 +491,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
ws.alive = true;
});
ws.send(JSON.stringify({ data: currentSong, event: 'song' }));
ws.send(JSON.stringify({ data: currentState, event: 'state' }));
});
const heartBeat = setInterval(() => {
@@ -497,7 +506,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
});
}, PING_TIMEOUT_MS);
wsServer.on('close', () => {
wsServer!.on('close', () => {
clearInterval(heartBeat);
});
@@ -564,13 +573,13 @@ ipcMain.on('remote-username', (_event, username: string) => {
});
ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids: string[]) => {
if (currentSong.song?.serverId !== serverId) return;
if (currentState.song?.serverId !== serverId) return;
const id = currentSong.song.id;
const id = currentState.song.id;
for (const songId of ids) {
if (songId === id) {
currentSong.song.userFavorite = favorite;
currentState.song.userFavorite = favorite;
broadcast({ data: { favorite, id: songId }, event: 'favorite' });
return;
}
@@ -578,13 +587,13 @@ ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids:
});
ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: string[]) => {
if (currentSong.song?.serverId !== serverId) return;
if (currentState.song?.serverId !== serverId) return;
const id = currentSong.song.id;
const id = currentState.song.id;
for (const songId of ids) {
if (songId === id) {
currentSong.song.userRating = rating;
currentState.song.userRating = rating;
broadcast({ data: { id: songId, rating }, event: 'rating' });
return;
}
@@ -592,42 +601,32 @@ ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: stri
});
ipcMain.on('update-repeat', (_event, repeat: PlayerRepeat) => {
currentSong.repeat = repeat;
broadcast({ data: { repeat }, event: 'song' });
currentState.repeat = repeat;
broadcast({ data: repeat, event: 'repeat' });
});
ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
currentSong.shuffle = shuffle;
broadcast({ data: { shuffle }, event: 'song' });
currentState.shuffle = shuffle;
broadcast({ data: shuffle, event: 'shuffle' });
});
ipcMain.on('update-song', (_event, data: SongUpdate) => {
const { song, ...rest } = data;
const songChanged = song?.id !== currentSong.song?.id;
ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
currentState.status = status;
broadcast({ data: status, event: 'playback' });
});
if (!song?.id) {
currentSong = {
...currentSong,
...data,
song: undefined,
};
} else {
currentSong = {
...currentSong,
...data,
};
}
ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
const songChanged = song?.id !== currentState.song?.id;
currentState.song = song;
if (songChanged) {
broadcast({ data: { ...rest, song: song || null }, event: 'song' });
} else {
broadcast({ data: rest, event: 'song' });
broadcast({ data: song || null, event: 'song' });
}
});
ipcMain.on('update-volume', (_event, volume: number) => {
currentSong.volume = volume;
broadcast({ data: { volume }, event: 'song' });
currentState.volume = volume;
broadcast({ data: volume, event: 'volume' });
});
if (mprisPlayer) {
@@ -636,16 +635,16 @@ if (mprisPlayer) {
event === 'Playlist'
? PlayerRepeat.ALL
: event === 'Track'
? PlayerRepeat.ONE
: PlayerRepeat.NONE;
? PlayerRepeat.ONE
: PlayerRepeat.NONE;
currentSong.repeat = repeat;
broadcast({ data: { repeat }, event: 'song' });
currentState.repeat = repeat;
broadcast({ data: repeat, event: 'repeat' });
});
mprisPlayer.on('shuffle', (shuffle: boolean) => {
currentSong.shuffle = shuffle;
broadcast({ data: { shuffle }, event: 'song' });
currentState.shuffle = shuffle;
broadcast({ data: shuffle, event: 'shuffle' });
});
mprisPlayer.on('volume', (vol: number) => {
@@ -656,7 +655,12 @@ if (mprisPlayer) {
} else if (volume < 0) {
volume = 0;
}
currentSong.volume = volume;
broadcast({ data: { volume }, event: 'song' });
currentState.volume = volume;
broadcast({ data: volume, event: 'volume' });
});
}
ipcMain.on('update-position', (_event, position: number) => {
currentState.position = position;
broadcast({ data: position, event: 'position' });
});
+7 -1
View File
@@ -1,5 +1,6 @@
import { ipcMain, safeStorage } from 'electron';
import { ipcMain, nativeTheme, safeStorage } from 'electron';
import Store from 'electron-store';
import type { TitleTheme } from '/@/renderer/types';
export const store = new Store();
@@ -48,3 +49,8 @@ ipcMain.handle('password-set', (_event, password: string, server: string) => {
}
return false;
});
ipcMain.on('theme-set', (_event, theme: TitleTheme) => {
store.set('theme', theme);
nativeTheme.themeSource = theme;
});
+29 -15
View File
@@ -1,7 +1,8 @@
import { ipcMain } from 'electron';
import Player from 'mpris-service';
import { PlayerRepeat, PlayerStatus, SongUpdate } from '../../../renderer/types';
import { PlayerRepeat, PlayerStatus } from '../../../renderer/types';
import { getMainWindow } from '../../main';
import { QueueSong } from '/@/renderer/api/types';
const mprisPlayer = Player({
identity: 'Feishin',
@@ -18,22 +19,29 @@ mprisPlayer.on('quit', () => {
process.exit();
});
const hasData = (): boolean => {
return mprisPlayer.metadata && !!mprisPlayer.metadata['mpris:length'];
};
mprisPlayer.on('stop', () => {
getMainWindow()?.webContents.send('renderer-player-stop');
mprisPlayer.playbackStatus = 'Paused';
});
mprisPlayer.on('pause', () => {
if (!hasData()) return;
getMainWindow()?.webContents.send('renderer-player-pause');
mprisPlayer.playbackStatus = 'Paused';
});
mprisPlayer.on('play', () => {
if (!hasData()) return;
getMainWindow()?.webContents.send('renderer-player-play');
mprisPlayer.playbackStatus = 'Playing';
});
mprisPlayer.on('playpause', () => {
if (!hasData()) return;
getMainWindow()?.webContents.send('renderer-player-play-pause');
if (mprisPlayer.playbackStatus !== 'Playing') {
mprisPlayer.playbackStatus = 'Playing';
@@ -43,6 +51,7 @@ mprisPlayer.on('playpause', () => {
});
mprisPlayer.on('next', () => {
if (!hasData()) return;
getMainWindow()?.webContents.send('renderer-player-next');
if (mprisPlayer.playbackStatus !== 'Playing') {
@@ -51,6 +60,7 @@ mprisPlayer.on('next', () => {
});
mprisPlayer.on('previous', () => {
if (!hasData()) return;
getMainWindow()?.webContents.send('renderer-player-previous');
if (mprisPlayer.playbackStatus !== 'Playing') {
@@ -70,6 +80,8 @@ mprisPlayer.on('volume', (vol: number) => {
getMainWindow()?.webContents.send('request-volume', {
volume,
});
mprisPlayer.volume = volume / 100;
});
mprisPlayer.on('shuffle', (event: boolean) => {
@@ -94,7 +106,7 @@ mprisPlayer.on('seek', (event: number) => {
});
});
ipcMain.on('mpris-update-position', (_event, arg) => {
ipcMain.on('update-position', (_event, arg: number) => {
mprisPlayer.getPosition = () => arg * 1e6;
});
@@ -106,6 +118,10 @@ ipcMain.on('update-volume', (_event, volume) => {
mprisPlayer.volume = Number(volume) / 100;
});
ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
mprisPlayer.playbackStatus = status === PlayerStatus.PLAYING ? 'Playing' : 'Paused';
});
const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = {
[PlayerRepeat.ALL]: 'Playlist',
[PlayerRepeat.ONE]: 'Track',
@@ -120,22 +136,13 @@ ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
mprisPlayer.shuffle = shuffle;
});
ipcMain.on('update-song', (_event, args: SongUpdate) => {
const { song, status, repeat, shuffle } = args || {};
ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
try {
mprisPlayer.playbackStatus = status === PlayerStatus.PLAYING ? 'Playing' : 'Paused';
if (repeat) {
mprisPlayer.loopStatus = REPEAT_TO_MPRIS[repeat];
if (!song?.id) {
mprisPlayer.metadata = {};
return;
}
if (shuffle) {
mprisPlayer.shuffle = shuffle;
}
if (!song) return;
const upsizedImageUrl = song.imageUrl
? song.imageUrl
?.replace(/&size=\d+/, '&size=300')
@@ -154,12 +161,19 @@ ipcMain.on('update-song', (_event, args: SongUpdate) => {
? song.albumArtists.map((artist) => artist.name)
: null,
'xesam:artist': song.artists?.length ? song.artists.map((artist) => artist.name) : null,
'xesam:audioBpm': song.bpm,
// Comment is a `list of strings` type
'xesam:comment': song.comment ? [song.comment] : null,
'xesam:contentCreated': song.releaseDate,
'xesam:discNumber': song.discNumber ? song.discNumber : null,
'xesam:genre': song.genres?.length ? song.genres.map((genre: any) => genre.name) : null,
'xesam:lastUsed': song.lastPlayedAt,
'xesam:title': song.name || null,
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
'xesam:useCount':
song.playCount !== null && song.playCount !== undefined ? song.playCount : null,
// User ratings are only on Navidrome/Subsonic and are on a scale of 1-5
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
};
} catch (err) {
console.log(err);
+137 -155
View File
@@ -20,27 +20,37 @@ import {
Tray,
Menu,
nativeImage,
nativeTheme,
BrowserWindowConstructorOptions,
protocol,
net,
Rectangle,
screen,
} from 'electron';
import electronLocalShortcut from 'electron-localshortcut';
import log from 'electron-log';
import log from 'electron-log/main';
import { autoUpdater } from 'electron-updater';
import uniq from 'lodash/uniq';
import MpvAPI from 'node-mpv';
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
import { store } from './features/core/settings/index';
import MenuBuilder from './menu';
import { hotkeyToElectronAccelerator, isLinux, isMacOS, isWindows, resolveHtmlPath } from './utils';
import {
hotkeyToElectronAccelerator,
isLinux,
isMacOS,
isWindows,
resolveHtmlPath,
createLog,
autoUpdaterLogInterface,
} from './utils';
import './features';
import type { TitleTheme } from '/@/renderer/types';
declare module 'node-mpv';
export default class AppUpdater {
constructor() {
log.transports.file.level = 'info';
autoUpdater.logger = log;
autoUpdater.logger = autoUpdaterLogInterface;
autoUpdater.checkForUpdatesAndNotify();
}
}
@@ -55,6 +65,12 @@ if (store.get('ignore_ssl')) {
app.commandLine.appendSwitch('ignore-certificate-errors');
}
// From https://github.com/tutao/tutanota/commit/92c6ed27625fcf367f0fbcc755d83d7ff8fde94b
if (isLinux() && !process.argv.some((a) => a.startsWith('--password-store='))) {
const passwordStore = store.get('password_store', 'gnome-libsecret') as string;
app.commandLine.appendSwitch('password-store', passwordStore);
}
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
let exitFromTray = false;
@@ -96,6 +112,19 @@ export const getMainWindow = () => {
return mainWindow;
};
export const sendToastToRenderer = ({
message,
type,
}: {
message: string;
type: 'success' | 'error' | 'warning' | 'info';
}) => {
getMainWindow()?.webContents.send('toast-from-main', {
message,
type,
});
};
const createWinThumbarButtons = () => {
if (isWindows()) {
getMainWindow()?.setThumbarButtons([
@@ -170,7 +199,7 @@ const createTray = () => {
},
]);
tray.on('double-click', () => {
tray.on('click', () => {
mainWindow?.show();
createWinThumbarButtons();
});
@@ -179,9 +208,9 @@ const createTray = () => {
tray.setContextMenu(contextMenu);
};
const createWindow = async () => {
const createWindow = async (first = true) => {
if (isDevelopment) {
await installExtensions();
await installExtensions().catch(console.log);
}
const nativeFrame = store.get('window_window_bar_style') === 'linux';
@@ -194,8 +223,8 @@ const createWindow = async () => {
},
macOS: {
autoHideMenuBar: true,
frame: false,
titleBarStyle: 'hidden',
frame: true,
titleBarStyle: 'default',
trafficLightPosition: { x: 10, y: 10 },
},
windows: {
@@ -229,6 +258,26 @@ const createWindow = async () => {
...(nativeFrame && isWindows() && nativeFrameConfig.windows),
});
// From https://github.com/electron/electron/issues/526#issuecomment-1663959513
const bounds = store.get('bounds') as Rectangle | undefined;
if (bounds) {
const screenArea = screen.getDisplayMatching(bounds).workArea;
if (
bounds.x > screenArea.x + screenArea.width ||
bounds.x < screenArea.x ||
bounds.y < screenArea.y ||
bounds.y > screenArea.y + screenArea.height
) {
if (bounds.width < screenArea.width && bounds.height < screenArea.height) {
mainWindow.setBounds({ height: bounds.height, width: bounds.width });
} else {
mainWindow.setBounds({ height: 900, width: 1440 });
}
} else {
mainWindow.setBounds(bounds);
}
}
electronLocalShortcut.register(mainWindow, 'Ctrl+Shift+I', () => {
mainWindow?.webContents.openDevTools();
});
@@ -258,6 +307,10 @@ const createWindow = async () => {
app.exit();
});
ipcMain.handle('window-clear-cache', async () => {
return mainWindow?.webContents.session.clearCache();
});
ipcMain.on('app-restart', () => {
// Fix for .AppImage
if (process.env.APPIMAGE) {
@@ -304,40 +357,60 @@ const createWindow = async () => {
}
const queue = JSON.parse(data.toString());
getMainWindow()?.webContents.send('renderer-player-restore-queue', queue);
getMainWindow()?.webContents.send('renderer-restore-queue', queue);
});
});
});
}
});
const globalMediaKeysEnabled = store.get('global_media_hotkeys') as boolean;
ipcMain.on('download-url', (_event, url: string) => {
mainWindow?.webContents.downloadURL(url);
});
if (globalMediaKeysEnabled !== false) {
const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean;
if (globalMediaKeysEnabled) {
enableMediaKeys(mainWindow);
}
mainWindow.loadURL(resolveHtmlPath('index.html'));
const startWindowMinimized = store.get('window_start_minimized', false) as boolean;
mainWindow.on('ready-to-show', () => {
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
if (process.env.START_MINIMIZED) {
mainWindow.minimize();
} else {
if (!first || !startWindowMinimized) {
const maximized = store.get('maximized');
const fullScreen = store.get('fullscreen');
if (maximized) {
mainWindow.maximize();
}
if (fullScreen) {
mainWindow.setFullScreen(true);
}
mainWindow.show();
createWinThumbarButtons();
}
});
mainWindow.on('closed', () => {
ipcMain.removeHandler('window-clear-cache');
mainWindow = null;
});
let saved = false;
mainWindow.on('close', (event) => {
store.set('bounds', mainWindow?.getNormalBounds());
store.set('maximized', mainWindow?.isMaximized());
store.set('fullscreen', mainWindow?.isFullScreen());
if (!exitFromTray && store.get('window_exit_to_tray')) {
if (isMacOS() && !forceQuit) {
exitFromTray = true;
@@ -350,7 +423,7 @@ const createWindow = async () => {
event.preventDefault();
saved = true;
getMainWindow()?.webContents.send('renderer-player-save-queue');
getMainWindow()?.webContents.send('renderer-save-queue');
ipcMain.once('player-save-queue', async (_event, data: Record<string, any>) => {
const queueLocation = join(app.getPath('userData'), 'queue');
@@ -404,7 +477,7 @@ const createWindow = async () => {
const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu();
// Open urls in the user's browser
// Open URLs in the user's browser
mainWindow.webContents.setWindowOpenHandler((edata) => {
shell.openExternal(edata.url);
return { action: 'deny' };
@@ -414,139 +487,15 @@ const createWindow = async () => {
// eslint-disable-next-line
new AppUpdater();
}
const theme = store.get('theme') as TitleTheme | undefined;
nativeTheme.themeSource = theme || 'dark';
};
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined;
const prefetchPlaylistParams = [
'--prefetch-playlist=no',
'--prefetch-playlist=yes',
'--prefetch-playlist',
];
const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => {
const parameters = ['--idle=yes', '--no-config', '--load-scripts=no'];
if (!extraParameters?.some((param) => prefetchPlaylistParams.includes(param))) {
parameters.push('--prefetch-playlist=yes');
}
return parameters;
};
let mpvInstance: MpvAPI | null = null;
const createMpv = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
const { extraParameters, properties } = data;
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
console.log('Setting mpv params: ', params);
const extra = isDevelopment ? '-dev' : '';
const mpv = new MpvAPI(
{
audio_only: true,
auto_restart: false,
binary: MPV_BINARY_PATH || '',
socket: isWindows() ? `\\\\.\\pipe\\mpvserver${extra}` : `/tmp/node-mpv${extra}.sock`,
time_update: 1,
},
params,
);
// eslint-disable-next-line promise/catch-or-return
mpv.start()
.catch((error) => {
console.log('MPV failed to start', error);
})
.finally(() => {
console.log('Setting MPV properties: ', properties);
mpv.setMultipleProperties(properties || {});
});
mpv.on('status', (status, ...rest) => {
console.log('MPV Event: status', status.property, status.value, rest);
if (status.property === 'playlist-pos') {
if (status.value === -1) {
mpv?.stop();
}
if (status.value !== 0) {
getMainWindow()?.webContents.send('renderer-player-auto-next');
}
}
});
// Automatically updates the play button when the player is playing
mpv.on('resumed', () => {
console.log('MPV Event: resumed');
getMainWindow()?.webContents.send('renderer-player-play');
});
// Automatically updates the play button when the player is stopped
mpv.on('stopped', () => {
console.log('MPV Event: stopped');
getMainWindow()?.webContents.send('renderer-player-stop');
});
// Automatically updates the play button when the player is paused
mpv.on('paused', () => {
console.log('MPV Event: paused');
getMainWindow()?.webContents.send('renderer-player-pause');
});
// Event output every interval set by time_update, used to update the current time
mpv.on('timeposition', (time: number) => {
getMainWindow()?.webContents.send('renderer-player-current-time', time);
});
mpv.on('quit', () => {
console.log('MPV Event: quit');
});
return mpv;
};
export const getMpvInstance = () => {
return mpvInstance;
};
ipcMain.on('player-set-properties', async (_event, data: Record<string, any>) => {
if (data.length === 0) {
return;
}
if (data.length === 1) {
getMpvInstance()?.setProperty(Object.keys(data)[0], Object.values(data)[0]);
} else {
getMpvInstance()?.setMultipleProperties(data);
}
});
ipcMain.on(
'player-restart',
async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
mpvInstance?.quit();
mpvInstance = createMpv(data);
},
);
ipcMain.on(
'player-initialize',
async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
console.log('Initializing MPV with data: ', data);
mpvInstance = createMpv(data);
},
);
ipcMain.on('player-quit', async () => {
mpvInstance?.stop();
mpvInstance?.quit();
mpvInstance = null;
});
// https://github.com/electron/electron/issues/46538#issuecomment-2808806722
app.commandLine.appendSwitch('gtk-version', '3');
// Must duplicate with the one in renderer process settings.store.ts
enum BindingActions {
@@ -622,7 +571,7 @@ ipcMain.on(
}
}
const globalMediaKeysEnabled = store.get('global_media_hotkeys') as boolean;
const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean;
if (globalMediaKeysEnabled) {
enableMediaKeys(mainWindow);
@@ -630,17 +579,25 @@ ipcMain.on(
},
);
app.on('before-quit', () => {
getMpvInstance()?.stop();
getMpvInstance()?.quit();
});
ipcMain.on(
'logger',
(
_event,
data: {
message: string;
type: 'debug' | 'verbose' | 'success' | 'error' | 'warning' | 'info';
},
) => {
createLog(data);
},
);
app.on('window-all-closed', () => {
globalShortcut.unregisterAll();
getMpvInstance()?.quit();
// Respect the OSX convention of having the application in memory even
// after all windows have been closed
if (isMacOS()) {
ipcMain.removeHandler('window-clear-cache');
mainWindow = null;
} else {
app.quit();
@@ -665,6 +622,8 @@ if (!singleInstance) {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
} else if (!mainWindow.isVisible()) {
mainWindow.show();
}
mainWindow.focus();
@@ -691,12 +650,35 @@ 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.
if (mainWindow === null) createWindow();
if (mainWindow === null) createWindow(false);
else if (!mainWindow.isVisible()) {
mainWindow.show();
createWinThumbarButtons();
}
});
})
.catch(console.log);
}
// Register 'open-item' handler globally, ensuring it is only registered once
if (!ipcMain.eventNames().includes('open-item')) {
ipcMain.handle('open-item', async (_event, path: string) => {
return new Promise<void>((resolve, reject) => {
access(path, constants.F_OK, (error) => {
if (error) {
reject(error);
return;
}
shell.showItemInFolder(path);
resolve();
});
});
});
}
+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',
},
+7
View File
@@ -24,7 +24,12 @@ const devtools = () => {
ipcRenderer.send('window-dev-tools');
};
const clearCache = (): Promise<void> => {
return ipcRenderer.invoke('window-clear-cache');
};
export const browser = {
clearCache,
devtools,
exit,
maximize,
@@ -32,3 +37,5 @@ export const browser = {
quit,
unmaximize,
};
export type Browser = typeof browser;
+27 -1
View File
@@ -1,9 +1,18 @@
import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron';
import Store from 'electron-store';
import { toServerType, type TitleTheme } from '/@/renderer/types';
const store = new Store();
const set = (property: string, value: string | Record<string, unknown> | boolean | string[]) => {
const set = (
property: string,
value: string | Record<string, unknown> | boolean | string[] | undefined,
) => {
if (value === undefined) {
store.delete(property);
return;
}
store.set(`${property}`, value);
};
@@ -43,9 +52,25 @@ const fontError = (cb: (event: IpcRendererEvent, file: string) => void) => {
ipcRenderer.on('custom-font-error', cb);
};
const themeSet = (theme: TitleTheme): void => {
ipcRenderer.send('theme-set', theme);
};
const SERVER_TYPE = toServerType(process.env.SERVER_TYPE);
const env = {
SERVER_LOCK:
SERVER_TYPE !== null ? process.env.SERVER_LOCK?.toLocaleLowerCase() === 'true' : false,
SERVER_NAME: process.env.SERVER_NAME ?? '',
SERVER_TYPE,
SERVER_URL: process.env.SERVER_URL ?? 'http://',
START_MAXIMIZED: store.get('maximized'),
};
export const localSettings = {
disableMediaKeys,
enableMediaKeys,
env,
fontError,
get,
passwordGet,
@@ -54,6 +79,7 @@ export const localSettings = {
restart,
set,
setZoomFactor,
themeSet,
};
export type LocalSettings = typeof localSettings;
+19 -33
View File
@@ -1,12 +1,16 @@
import { ipcRenderer, IpcRendererEvent } from 'electron';
import { PlayerData, PlayerState } from '/@/renderer/store';
import { PlayerData } from '/@/renderer/store';
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
ipcRenderer.send('player-initialize', data);
return ipcRenderer.invoke('player-initialize', data);
};
const restart = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
ipcRenderer.send('player-restart', data);
const restart = (data: {
binaryPath?: string;
extraParameters?: string[];
properties?: Record<string, any>;
}) => {
return ipcRenderer.invoke('player-restart', data);
};
const isRunning = () => {
@@ -18,12 +22,11 @@ const cleanup = () => {
};
const setProperties = (data: Record<string, any>) => {
console.log('Setting property :>>', data);
ipcRenderer.send('player-set-properties', data);
};
const autoNext = (data: PlayerData) => {
ipcRenderer.send('player-auto-next', data);
const autoNext = (url?: string) => {
ipcRenderer.send('player-auto-next', url);
};
const currentTime = () => {
@@ -50,14 +53,6 @@ const previous = () => {
ipcRenderer.send('player-previous');
};
const restoreQueue = () => {
ipcRenderer.send('player-restore-queue');
};
const saveQueue = (data: Record<string, any>) => {
ipcRenderer.send('player-save-queue', data);
};
const seek = (seconds: number) => {
ipcRenderer.send('player-seek', seconds);
};
@@ -66,12 +61,12 @@ const seekTo = (seconds: number) => {
ipcRenderer.send('player-seek-to', seconds);
};
const setQueue = (data: PlayerData, pause?: boolean) => {
ipcRenderer.send('player-set-queue', data, pause);
const setQueue = (current?: string, next?: string, pause?: boolean) => {
ipcRenderer.send('player-set-queue', current, next, pause);
};
const setQueueNext = (data: PlayerData) => {
ipcRenderer.send('player-set-queue-next', data);
const setQueueNext = (url?: string) => {
ipcRenderer.send('player-set-queue-next', url);
};
const stop = () => {
@@ -154,20 +149,14 @@ const rendererQuit = (cb: (event: IpcRendererEvent) => void) => {
ipcRenderer.on('renderer-player-quit', cb);
};
const rendererSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
ipcRenderer.on('renderer-player-save-queue', cb);
};
const rendererRestoreQueue = (
cb: (event: IpcRendererEvent, data: Partial<PlayerState>) => void,
) => {
ipcRenderer.on('renderer-player-restore-queue', cb);
};
const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
ipcRenderer.on('renderer-player-error', cb);
};
const rendererPlayerFallback = (cb: (event: IpcRendererEvent, data: boolean) => void) => {
ipcRenderer.on('renderer-player-fallback', cb);
};
export const mpvPlayer = {
autoNext,
cleanup,
@@ -182,8 +171,6 @@ export const mpvPlayer = {
previous,
quit,
restart,
restoreQueue,
saveQueue,
seek,
seekTo,
setProperties,
@@ -201,10 +188,9 @@ export const mpvPlayerListener = {
rendererPause,
rendererPlay,
rendererPlayPause,
rendererPlayerFallback,
rendererPrevious,
rendererQuit,
rendererRestoreQueue,
rendererSaveQueue,
rendererSkipBackward,
rendererSkipForward,
rendererStop,
+13 -2
View File
@@ -1,5 +1,6 @@
import { IpcRendererEvent, ipcRenderer } from 'electron';
import { SongUpdate } from '/@/renderer/types';
import { QueueSong } from '/@/renderer/api/types';
import { PlayerStatus } from '/@/renderer/types';
const requestFavorite = (
cb: (
@@ -46,6 +47,10 @@ const updatePassword = (password: string) => {
ipcRenderer.send('remote-password', password);
};
const updatePlayback = (playback: PlayerStatus) => {
ipcRenderer.send('update-playback', playback);
};
const updateSetting = (
enabled: boolean,
port: number,
@@ -67,7 +72,7 @@ const updateShuffle = (shuffle: boolean) => {
ipcRenderer.send('update-shuffle', shuffle);
};
const updateSong = (args: SongUpdate) => {
const updateSong = (args: QueueSong | undefined) => {
ipcRenderer.send('update-song', args);
};
@@ -79,6 +84,10 @@ const updateVolume = (volume: number) => {
ipcRenderer.send('update-volume', volume);
};
const updatePosition = (timeSec: number) => {
ipcRenderer.send('update-position', timeSec);
};
export const remote = {
requestFavorite,
requestPosition,
@@ -89,6 +98,8 @@ export const remote = {
setRemotePort,
updateFavorite,
updatePassword,
updatePlayback,
updatePosition,
updateRating,
updateRepeat,
updateSetting,
+60
View File
@@ -1,9 +1,69 @@
import { IpcRendererEvent, ipcRenderer } from 'electron';
import { isMacOS, isWindows, isLinux } from '../utils';
import { PlayerState } from '/@/renderer/store';
const saveQueue = (data: Record<string, any>) => {
ipcRenderer.send('player-save-queue', data);
};
const restoreQueue = () => {
ipcRenderer.send('player-restore-queue');
};
const openItem = async (path: string) => {
return ipcRenderer.invoke('open-item', path);
};
const onSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
ipcRenderer.on('renderer-save-queue', cb);
};
const onRestoreQueue = (cb: (event: IpcRendererEvent, data: Partial<PlayerState>) => void) => {
ipcRenderer.on('renderer-restore-queue', cb);
};
const playerErrorListener = (cb: (event: IpcRendererEvent, data: { code: number }) => void) => {
ipcRenderer.on('player-error-listener', cb);
};
const mainMessageListener = (
cb: (
event: IpcRendererEvent,
data: { message: string; type: 'success' | 'error' | 'warning' | 'info' },
) => void,
) => {
ipcRenderer.on('toast-from-main', cb);
};
const logger = (
cb: (
event: IpcRendererEvent,
data: {
message: string;
type: 'debug' | 'verbose' | 'error' | 'warning' | 'info';
},
) => void,
) => {
ipcRenderer.send('logger', cb);
};
const download = (url: string) => {
ipcRenderer.send('download-url', url);
};
export const utils = {
download,
isLinux,
isMacOS,
isWindows,
logger,
mainMessageListener,
onRestoreQueue,
onSaveQueue,
openItem,
playerErrorListener,
restoreQueue,
saveQueue,
};
export type Utils = typeof utils;
+48
View File
@@ -2,6 +2,7 @@
import path from 'path';
import process from 'process';
import { URL } from 'url';
import log from 'electron-log/main';
export let resolveHtmlPath: (htmlFileName: string) => string;
@@ -34,6 +35,10 @@ export const hotkeyToElectronAccelerator = (hotkey: string) => {
let accelerator = hotkey;
const replacements = {
arrowdown: 'Down',
arrowleft: 'Left',
arrowright: 'Right',
arrowup: 'Up',
mod: 'CmdOrCtrl',
numpad: 'num',
numpadadd: 'numadd',
@@ -50,3 +55,46 @@ export const hotkeyToElectronAccelerator = (hotkey: string) => {
return accelerator;
};
const logMethod = {
debug: log.debug,
error: log.error,
info: log.info,
success: log.info,
verbose: log.verbose,
warning: log.warn,
};
const logColor = {
debug: 'blue',
error: 'red',
info: 'blue',
success: 'green',
verbose: 'blue',
warning: 'yellow',
};
export const createLog = (data: {
message: string;
type: 'debug' | 'verbose' | 'success' | 'error' | 'warning' | 'info';
}) => {
logMethod[data.type](`%c${data.message}`, `color: ${logColor[data.type]}`);
};
export const autoUpdaterLogInterface = {
debug: (message: string) => {
createLog({ message: `[SYSTEM] ${message}`, type: 'debug' });
},
error: (message: string) => {
createLog({ message: `[SYSTEM] ${message}`, type: 'error' });
},
info: (message: string) => {
createLog({ message: `[SYSTEM] ${message}`, type: 'info' });
},
warn: (message: string) => {
createLog({ message: `[SYSTEM] ${message}`, type: 'warning' });
},
};
+21 -8
View File
@@ -18,10 +18,10 @@ import {
import { PlayerRepeat, PlayerStatus } from '/@/renderer/types';
import { WrapperSlider } from '/@/remote/components/wrapped-slider';
import { Tooltip } from '/@/renderer/components/tooltip';
import { Rating } from '/@/renderer/components';
import { Rating } from '/@/renderer/components/rating';
export const RemoteContainer = () => {
const { repeat, shuffle, song, status, volume } = useInfo();
const { position, repeat, shuffle, song, status, volume } = useInfo();
const send = useSend();
const showImage = useShowImage();
@@ -38,7 +38,7 @@ export const RemoteContainer = () => {
return (
<>
{song && (
{id && (
<>
<Title order={1}>{song.name}</Title>
<Group align="flex-end">
@@ -61,6 +61,7 @@ export const RemoteContainer = () => {
spacing={0}
>
<RemoteButton
disabled={!id}
tooltip="Previous track"
variant="default"
onClick={() => send({ event: 'previous' })}
@@ -68,7 +69,8 @@ export const RemoteContainer = () => {
<RiSkipBackFill size={25} />
</RemoteButton>
<RemoteButton
tooltip={status === PlayerStatus.PLAYING ? 'Pause' : 'Play'}
disabled={!id}
tooltip={id && status === PlayerStatus.PLAYING ? 'Pause' : 'Play'}
variant="default"
onClick={() => {
if (status === PlayerStatus.PLAYING) {
@@ -78,13 +80,14 @@ export const RemoteContainer = () => {
}
}}
>
{status === PlayerStatus.PLAYING ? (
{id && status === PlayerStatus.PLAYING ? (
<RiPauseFill size={25} />
) : (
<RiPlayFill size={25} />
)}
</RemoteButton>
<RemoteButton
disabled={!id}
tooltip="Next track"
variant="default"
onClick={() => send({ event: 'next' })}
@@ -110,8 +113,8 @@ export const RemoteContainer = () => {
repeat === PlayerRepeat.ONE
? 'One'
: repeat === PlayerRepeat.ALL
? 'all'
: 'none'
? 'all'
: 'none'
}`}
variant="default"
onClick={() => send({ event: 'repeat' })}
@@ -124,7 +127,7 @@ export const RemoteContainer = () => {
</RemoteButton>
<RemoteButton
$active={song?.userFavorite}
disabled={!song}
disabled={!id}
tooltip={song?.userFavorite ? 'Unfavorite' : 'Favorite'}
variant="default"
onClick={() => {
@@ -151,6 +154,16 @@ export const RemoteContainer = () => {
</div>
)}
</Group>
{id && position !== undefined && (
<WrapperSlider
label={(value) => formatDuration(value * 1e3)}
leftLabel={formatDuration(position * 1e3)}
max={song.duration / 1e3}
rightLabel={formatDuration(song.duration)}
value={position}
onChangeEnd={(e) => send({ event: 'position', position: e })}
/>
)}
<WrapperSlider
leftLabel={<RiVolumeUpFill size={20} />}
max={100}
+1 -1
View File
@@ -44,7 +44,7 @@ export const WrapperSlider = ({ leftLabel, rightLabel, value, ...props }: Wrappe
{...props}
min={0}
size={6}
value={!isSeeking ? value ?? 0 : seek}
value={!isSeeking ? (value ?? 0) : seek}
w="100%"
onChange={(e) => {
setIsSeeking(true);
+41 -7
View File
@@ -4,7 +4,7 @@ import merge from 'lodash/merge';
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/remote/types';
import type { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/remote/types';
interface StatefulWebSocket extends WebSocket {
natural: boolean;
@@ -133,6 +133,18 @@ export const useRemoteStore = create<SettingsSlice>()(
});
break;
}
case 'playback': {
set((state) => {
state.info.status = data;
});
break;
}
case 'position': {
set((state) => {
state.info.position = data;
});
break;
}
case 'proxy': {
set((state) => {
if (state.info.song) {
@@ -149,9 +161,33 @@ export const useRemoteStore = create<SettingsSlice>()(
});
break;
}
case 'repeat': {
set((state) => {
state.info.repeat = data;
});
break;
}
case 'shuffle': {
set((state) => {
state.info.shuffle = data;
});
break;
}
case 'song': {
set((nested) => {
nested.info = { ...nested.info, ...data };
set((state) => {
state.info.song = data;
});
break;
}
case 'state': {
set((state) => {
state.info = data;
});
break;
}
case 'volume': {
set((state) => {
state.info.volume = data;
});
}
}
@@ -212,11 +248,9 @@ export const useRemoteStore = create<SettingsSlice>()(
{ name: 'store_settings' },
),
{
merge: (persistedState, currentState) => {
return merge(currentState, persistedState);
},
merge: (persistedState, currentState) => merge(currentState, persistedState),
name: 'store_settings',
version: 6,
version: 7,
},
),
);
+51 -4
View File
@@ -1,7 +1,8 @@
import type { QueueSong } from '/@/renderer/api/types';
import type { SongUpdate } from '/@/renderer/types';
import type { PlayerRepeat, PlayerStatus, SongState } from '/@/renderer/types';
export interface SongUpdateSocket extends Omit<SongUpdate, 'song'> {
export interface SongUpdateSocket extends Omit<SongState, 'song'> {
position?: number;
song?: QueueSong | null;
}
@@ -15,6 +16,15 @@ export interface ServerFavorite {
event: 'favorite';
}
export interface ServerPlayStatus {
data: PlayerStatus;
event: 'playback';
}
export interface ServerPosition {
data: number;
event: 'position';
}
export interface ServerProxy {
data: string;
event: 'proxy';
@@ -25,12 +35,43 @@ export interface ServerRating {
event: 'rating';
}
export interface ServerRepeat {
data: PlayerRepeat;
event: 'repeat';
}
export interface ServerShuffle {
data: boolean;
event: 'shuffle';
}
export interface ServerSong {
data: SongUpdateSocket;
data: QueueSong | null;
event: 'song';
}
export type ServerEvent = ServerError | ServerFavorite | ServerRating | ServerSong | ServerProxy;
export interface ServerState {
data: SongState;
event: 'state';
}
export interface ServerVolume {
data: number;
event: 'volume';
}
export type ServerEvent =
| ServerError
| ServerFavorite
| ServerPlayStatus
| ServerPosition
| ServerRating
| ServerRepeat
| ServerShuffle
| ServerSong
| ServerState
| ServerProxy
| ServerVolume;
export interface ClientSimpleEvent {
event: 'next' | 'pause' | 'play' | 'previous' | 'proxy' | 'repeat' | 'shuffle';
@@ -58,8 +99,14 @@ export interface ClientAuth {
header: string;
}
export interface ClientPosition {
event: 'position';
position: number;
}
export type ClientEvent =
| ClientAuth
| ClientPosition
| ClientSimpleEvent
| ClientFavorite
| ClientRating
+140 -466
View File
@@ -1,101 +1,11 @@
import { useAuthStore } from '/@/renderer/store';
import { toast } from '/@/renderer/components/toast/index';
import type {
AlbumDetailArgs,
AlbumListArgs,
SongListArgs,
SongDetailArgs,
AlbumArtistDetailArgs,
AlbumArtistListArgs,
SetRatingArgs,
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,
} from '/@/renderer/api/types';
import { ServerType } from '/@/renderer/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>;
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>;
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
search: (args: SearchArgs) => Promise<SearchResponse>;
setRating: (args: SetRatingArgs) => Promise<RatingResponse>;
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
}>;
type ApiController = {
jellyfin: ControllerEndpoint;
navidrome: ControllerEndpoint;
@@ -103,113 +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,
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,
getSongDetail: jfController.getSongDetail,
getSongList: jfController.getSongList,
getTopSongs: jfController.getTopSongList,
getUserList: undefined,
removeFromPlaylist: jfController.removeFromPlaylist,
scrobble: jfController.scrobble,
search: jfController.search,
setRating: 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,
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,
getSongDetail: ndController.getSongDetail,
getSongList: ndController.getSongList,
getTopSongs: ssController.getTopSongList,
getUserList: ndController.getUserList,
removeFromPlaylist: ndController.removeFromPlaylist,
scrobble: ssController.scrobble,
search: ssController.search3,
setRating: ssController.setRating,
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,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: undefined,
getLyrics: undefined,
getMusicFolderList: ssController.getMusicFolderList,
getPlaylistDetail: undefined,
getPlaylistList: undefined,
getSongDetail: undefined,
getSongList: undefined,
getTopSongs: ssController.getTopSongList,
getUserList: undefined,
scrobble: ssController.scrobble,
search: ssController.search3,
setRating: 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) {
@@ -239,274 +51,136 @@ 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 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);
};
export const controller = {
addToPlaylist,
authenticate,
createFavorite,
createPlaylist,
deleteFavorite,
deletePlaylist,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumDetail,
getAlbumList,
getArtistList,
getGenreList,
getLyrics,
getMusicFolderList,
getPlaylistDetail,
getPlaylistList,
getPlaylistSongList,
getRandomSongList,
getSongDetail,
getSongList,
getTopSongList,
getUserList,
removeFromPlaylist,
scrobble,
search,
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);
},
getArtistList(args) {
return apiController('getArtistList', args.apiClientProps.server?.type)?.(args);
},
getArtistListCount(args) {
return apiController('getArtistListCount', 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);
},
getRoles(args) {
return apiController('getRoles', 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);
},
};
+12
View File
@@ -0,0 +1,12 @@
// 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',
PUBLIC_PLAYLIST = 'publicPlaylist',
SHARING_ALBUM_SONG = 'sharingAlbumSong',
}
export type ServerFeatures = Partial<Record<ServerFeature, boolean>>;
+3 -3
View File
@@ -574,7 +574,7 @@ export enum JFSongListSort {
ARTIST = 'Artist,Album,SortName',
COMMUNITY_RATING = 'CommunityRating,SortName',
DURATION = 'Runtime,AlbumArtist,Album,SortName',
NAME = 'Name,SortName',
NAME = 'Name',
PLAY_COUNT = 'PlayCount,SortName',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
@@ -601,7 +601,7 @@ export type JFSongListParams = {
export enum JFAlbumArtistListSort {
ALBUM = 'Album,SortName',
DURATION = 'Runtime,AlbumArtist,Album,SortName',
NAME = 'Name,SortName',
NAME = 'SortName,Name',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
@@ -618,7 +618,7 @@ export type JFAlbumArtistListParams = {
export enum JFArtistListSort {
ALBUM = 'Album,SortName',
DURATION = 'Runtime,AlbumArtist,Album,SortName',
NAME = 'Name,SortName',
NAME = 'SortName,Name',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
+59 -9
View File
@@ -3,11 +3,12 @@ import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import { initClient, initContract } from '@ts-rest/core';
import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios';
import qs from 'qs';
import { ServerListItem } from '/@/renderer/types';
import { ServerListItem } from '/@/renderer/api/types';
import omitBy from 'lodash/omitBy';
import { z } from 'zod';
import { authenticationFailure } from '/@/renderer/api/utils';
import { authenticationFailure, getClientType } from '/@/renderer/api/utils';
import i18n from '/@/i18n/i18n';
import packageJson from '../../../../package.json';
const c = initContract();
@@ -24,9 +25,6 @@ export const contract = c.router({
},
authenticate: {
body: jfType._parameters.authenticate,
headers: z.object({
'X-Emby-Authorization': z.string(),
}),
method: 'POST',
path: 'users/authenticatebyname',
responses: {
@@ -115,6 +113,15 @@ export const contract = c.router({
400: jfType._response.error,
},
},
getInstantMix: {
method: 'GET',
path: 'songs/:itemId/InstantMix',
query: jfType._parameters.similarSongs,
responses: {
200: jfType._response.songList,
400: jfType._response.error,
},
},
getMusicFolderList: {
method: 'GET',
path: 'users/:userId/items',
@@ -150,6 +157,14 @@ export const contract = c.router({
400: jfType._response.error,
},
},
getServerInfo: {
method: 'GET',
path: 'system/info',
responses: {
200: jfType._response.serverInfo,
400: jfType._response.error,
},
},
getSimilarArtistList: {
method: 'GET',
path: 'artists/:id/similar',
@@ -159,6 +174,24 @@ export const contract = c.router({
400: jfType._response.error,
},
},
getSimilarSongs: {
method: 'GET',
path: 'items/:itemId/similar',
query: jfType._parameters.similarSongs,
responses: {
200: jfType._response.similarSongs,
400: jfType._response.error,
},
},
getSongData: {
method: 'GET',
path: 'users/:userId/items/:id',
query: jfType._parameters.songDetail,
responses: {
200: jfType._response.song,
400: jfType._response.error,
},
},
getSongDetail: {
method: 'GET',
path: 'users/:userId/items/:id',
@@ -178,7 +211,7 @@ export const contract = c.router({
},
getSongLyrics: {
method: 'GET',
path: 'users/:userId/Items/:id/Lyrics',
path: 'audio/:id/Lyrics',
responses: {
200: jfType._response.lyrics,
404: jfType._response.error,
@@ -193,6 +226,15 @@ export const contract = c.router({
400: jfType._response.error,
},
},
movePlaylistItem: {
body: null,
method: 'POST',
path: 'playlists/:playlistId/items/:itemId/move/:newIdx',
responses: {
200: jfType._response.moveItem,
400: jfType._response.error,
},
},
removeFavorite: {
body: jfType._parameters.favorite,
method: 'DELETE',
@@ -250,8 +292,8 @@ export const contract = c.router({
},
updatePlaylist: {
body: jfType._parameters.updatePlaylist,
method: 'PUT',
path: 'items/:id',
method: 'POST',
path: 'playlists/:id',
responses: {
200: jfType._response.updatePlaylist,
400: jfType._response.error,
@@ -298,6 +340,12 @@ const parsePath = (fullPath: string) => {
};
};
export const createAuthHeader = (): string => {
return `MediaBrowser Client="Feishin", Device="${getClientType()}", DeviceId="${
useAuthStore.getState().deviceId
}", Version="${packageJson.version}"`;
};
export const jfApiClient = (args: {
server: ServerListItem | null;
signal?: AbortSignal;
@@ -324,7 +372,9 @@ export const jfApiClient = (args: {
data: body,
headers: {
...headers,
...(token && { 'X-MediaBrowser-Token': token }),
...(token
? { Authorization: createAuthHeader().concat(`, Token="${token}"`) }
: { Authorization: createAuthHeader() }),
},
method: method as Method,
params,
File diff suppressed because it is too large Load Diff
+35 -19
View File
@@ -10,8 +10,9 @@ import {
Playlist,
MusicFolder,
Genre,
ServerListItem,
ServerType,
} from '/@/renderer/api/types';
import { ServerListItem, ServerType } from '/@/renderer/types';
const getStreamUrl = (args: {
container?: string;
@@ -29,11 +30,11 @@ const getStreamUrl = (args: {
`?userId=${server?.userId}` +
`&deviceId=${deviceId}` +
'&audioCodec=aac' +
`&api_key=${server?.credential}` +
`&apiKey=${server?.credential}` +
`&playSessionId=${deviceId}` +
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
'&transcodingContainer=ts' +
'&transcodingProtocol=hls'
'&transcodingProtocol=http'
);
};
@@ -52,7 +53,7 @@ const getAlbumArtistCoverArtUrl = (args: {
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
`?width=${size}` +
'&quality=96'
);
};
@@ -68,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'
);
};
@@ -85,7 +86,7 @@ const getSongCoverArtUrl = (args: {
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
`?width=${size}` +
'&quality=96'
);
}
@@ -96,7 +97,7 @@ const getSongCoverArtUrl = (args: {
`${args.baseUrl}/Items` +
`/${args.item?.AlbumId}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
`?width=${size}` +
'&quality=96'
);
}
@@ -115,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'
);
};
@@ -133,7 +134,7 @@ const normalizeSong = (
imageUrl: null,
name: entry.Name,
})),
albumId: item.AlbumId,
albumId: item.AlbumId || `dummy/${item.Id}`,
artistName: item?.ArtistItems?.[0]?.Name,
artists: item?.ArtistItems?.map((entry) => ({
id: entry.Id,
@@ -152,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,
@@ -170,12 +176,16 @@ 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,
playlistItemId: item.PlaylistItemId,
// releaseDate: (item.ProductionYear && new Date(item.ProductionYear, 0, 1).toISOString()) || null,
releaseDate: null,
releaseDate: item.PremiereDate
? new Date(item.PremiereDate).toISOString()
: item.ProductionYear
? new Date(item.ProductionYear, 0, 1).toISOString()
: null,
releaseYear: item.ProductionYear ? String(item.ProductionYear) : null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
@@ -202,6 +212,7 @@ const normalizeAlbum = (
imageSize?: number,
): Album => {
return {
albumArtist: item.AlbumArtist,
albumArtists:
item.AlbumArtists.map((entry) => ({
id: entry.Id,
@@ -214,6 +225,7 @@ const normalizeAlbum = (
name: entry.Name,
})),
backdropImageUrl: null,
comment: null,
createdAt: item.DateCreated,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({
@@ -232,7 +244,10 @@ const normalizeAlbum = (
isCompilation: null,
itemType: LibraryItem.ALBUM,
lastPlayedAt: null,
mbzId: item.ProviderIds?.MusicBrainzAlbum || null,
name: item.Name,
originalDate: null,
participants: null,
playCount: item.UserData?.PlayCount || 0,
releaseDate: item.PremiereDate?.split('T')[0] || null,
releaseYear: item.ProductionYear || null,
@@ -269,7 +284,7 @@ const normalizeAlbumArtist = (
) || [];
return {
albumCount: null,
albumCount: item.AlbumCount ?? null,
backgroundImageUrl: null,
biography: item.Overview || null,
duration: item.RunTimeTicks / 10000,
@@ -287,12 +302,13 @@ const normalizeAlbumArtist = (
}),
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: null,
mbz: item.ProviderIds?.MusicBrainzArtist || null,
name: item.Name,
playCount: item.UserData?.PlayCount || 0,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
similarArtists,
songCount: null,
songCount: item.SongCount ?? null,
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
};
@@ -379,7 +395,7 @@ const getGenreCoverArtUrl = (args: {
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
`?width=${size}` +
'&quality=96'
);
};
+45 -7
View File
@@ -387,11 +387,13 @@ const genericItem = z.object({
Name: z.string(),
});
const songDetailParameters = baseParameters;
const song = z.object({
Album: z.string(),
AlbumArtist: z.string(),
AlbumArtists: z.array(genericItem),
AlbumId: z.string(),
AlbumId: z.string().optional(),
AlbumPrimaryImageTag: z.string(),
ArtistItems: z.array(genericItem),
Artists: z.array(z.string()),
@@ -411,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(),
@@ -422,7 +425,13 @@ const song = z.object({
UserData: userData.optional(),
});
const providerIds = z.object({
MusicBrainzAlbum: z.string().optional(),
MusicBrainzArtist: z.string().optional(),
});
const albumArtist = z.object({
AlbumCount: z.number().optional(),
BackdropImageTags: z.array(z.string()),
ChannelId: z.null(),
DateCreated: z.string(),
@@ -435,8 +444,10 @@ const albumArtist = z.object({
LocationType: z.string(),
Name: z.string(),
Overview: z.string(),
ProviderIds: providerIds.optional(),
RunTimeTicks: z.number(),
ServerId: z.string(),
SongCount: z.number().optional(),
Type: z.string(),
UserData: userData.optional(),
});
@@ -466,6 +477,7 @@ const album = z.object({
ParentLogoItemId: z.string(),
PremiereDate: z.string().optional(),
ProductionYear: z.number(),
ProviderIds: providerIds.optional(),
RunTimeTicks: z.number(),
ServerId: z.string(),
Songs: z.array(song).optional(), // This is not a native Jellyfin property -- this is used for combined album detail
@@ -505,7 +517,7 @@ const albumList = pagination.extend({
const albumArtistListSort = {
ALBUM: 'Album,SortName',
DURATION: 'Runtime,AlbumArtist,Album,SortName',
NAME: 'Name,SortName',
NAME: 'SortName,Name',
RANDOM: 'Random,SortName',
RECENTLY_ADDED: 'DateCreated,SortName',
RELEASE_DATE: 'PremiereDate,AlbumArtist,Album,SortName',
@@ -535,7 +547,7 @@ const songListSort = {
ARTIST: 'Artist,Album,SortName',
COMMUNITY_RATING: 'CommunityRating,SortName',
DURATION: 'Runtime,AlbumArtist,Album,SortName',
NAME: 'Name,SortName',
NAME: 'Name',
PLAY_COUNT: 'PlayCount,SortName',
RANDOM: 'Random,SortName',
RECENTLY_ADDED: 'DateCreated,SortName',
@@ -552,6 +564,7 @@ const songListParameters = paginationParameters.merge(
GenreIds: z.string().optional(),
Genres: z.string().optional(),
IsFavorite: z.boolean().optional(),
IsPlayed: z.boolean().optional(),
SearchTerm: z.string().optional(),
SortBy: z.nativeEnum(songListSort).optional(),
Tags: z.string().optional(),
@@ -572,9 +585,9 @@ const playlistDetailParameters = baseParameters.extend({
});
const createPlaylistParameters = z.object({
IsPublic: z.boolean().optional(),
MediaType: z.literal('Audio'),
Name: z.string(),
Overview: z.string(),
UserId: z.string(),
});
@@ -586,9 +599,9 @@ const updatePlaylist = z.null();
const updatePlaylistParameters = z.object({
Genres: z.array(genreItem),
IsPublic: z.boolean().optional(),
MediaType: z.literal('Audio'),
Name: z.string(),
Overview: z.string(),
PremiereDate: z.null(),
ProviderIds: z.object({}),
Tags: z.array(genericItem),
@@ -600,14 +613,14 @@ const addToPlaylist = z.object({
});
const addToPlaylistParameters = z.object({
Ids: z.array(z.string()),
Ids: z.string(),
UserId: z.string(),
});
const removeFromPlaylist = z.null();
const removeFromPlaylistParameters = z.object({
EntryIds: z.array(z.string()),
EntryIds: z.string(),
});
const deletePlaylist = z.null();
@@ -654,6 +667,26 @@ const lyrics = z.object({
Lyrics: z.array(lyricText),
});
const serverInfo = z.object({
Version: z.string(),
});
const similarSongsParameters = z.object({
Fields: z.string().optional(),
Limit: z.number().optional(),
UserId: z.string().optional(),
});
const similarSongs = pagination.extend({
Items: z.array(song),
});
export enum JellyfinExtensions {
SONG_LYRICS = 'songLyrics',
}
const moveItem = z.null();
export const jfType = {
_enum: {
albumArtistList: albumArtistListSort,
@@ -683,6 +716,8 @@ export const jfType = {
scrobble: scrobbleParameters,
search: searchParameters,
similarArtistList: similarArtistListParameters,
similarSongs: similarSongsParameters,
songDetail: songDetailParameters,
songList: songListParameters,
updatePlaylist: updatePlaylistParameters,
},
@@ -700,6 +735,7 @@ export const jfType = {
genre,
genreList,
lyrics,
moveItem,
musicFolderList,
playlist,
playlistList,
@@ -707,6 +743,8 @@ export const jfType = {
removeFromPlaylist,
scrobble,
search,
serverInfo,
similarSongs,
song,
songList,
topSongsList,
+83 -16
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,24 +229,25 @@ 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',
PLAY_DATE = 'playDate',
RANDOM = 'random',
RATING = 'rating',
RECENTLY_ADDED = 'createdAt',
TITLE = 'title',
TRACK = 'track',
YEAR = 'year, album, discNumber, trackNumber',
YEAR = 'year',
}
export type NDSongListParams = {
@@ -260,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',
@@ -352,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',
@@ -380,38 +381,104 @@ 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: '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 = [
{ label: 'is in', value: 'inPlaylist' },
{ label: 'is not in', value: 'notInPlaylist' },
];
export const NDSongQueryDateOperators = [
+25 -5
View File
@@ -7,8 +7,8 @@ import qs from 'qs';
import { ndType } from './navidrome-types';
import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils';
import { useAuthStore } from '/@/renderer/store';
import { ServerListItem } from '/@/renderer/types';
import { toast } from '/@/renderer/components';
import { ServerListItem } from '/@/renderer/api/types';
import { toast } from '/@/renderer/components/toast';
import i18n from '/@/i18n/i18n';
const localSettings = isElectron() ? window.electron.localSettings : null;
@@ -147,6 +147,15 @@ export const contract = c.router({
500: resultWithHeaders(ndType._response.error),
},
},
movePlaylistItem: {
body: ndType._parameters.moveItem,
method: 'PUT',
path: 'playlist/:playlistId/tracks/:trackNumber',
responses: {
200: resultWithHeaders(ndType._response.moveItem),
400: resultWithHeaders(ndType._response.error),
},
},
removeFromPlaylist: {
body: null,
method: 'DELETE',
@@ -157,6 +166,16 @@ export const contract = c.router({
500: resultWithHeaders(ndType._response.error),
},
},
shareItem: {
body: ndType._parameters.shareItem,
method: 'POST',
path: 'share',
responses: {
200: resultWithHeaders(ndType._response.shareItem),
404: resultWithHeaders(ndType._response.error),
500: resultWithHeaders(ndType._response.error),
},
},
updatePlaylist: {
body: ndType._parameters.updatePlaylist,
method: 'PUT',
@@ -286,9 +305,10 @@ axiosClient.interceptors.response.use(
});
const serverId = currentServer.id;
useAuthStore
.getState()
.actions.updateServer(serverId, { ndCredential: undefined });
useAuthStore.getState().actions.updateServer(serverId, {
credential: undefined,
ndCredential: undefined,
});
useAuthStore.getState().actions.setCurrentServer(null);
// special error to prevent sending a second message, and stop other messages that could be enqueued
File diff suppressed because it is too large Load Diff
+127 -24
View File
@@ -7,8 +7,10 @@ import {
User,
AlbumArtist,
Genre,
ServerListItem,
ServerType,
RelatedArtist,
} from '/@/renderer/api/types';
import { ServerListItem, ServerType } from '/@/renderer/types';
import z from 'zod';
import { ndType } from './navidrome-types';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
@@ -40,15 +42,86 @@ const getCoverArtUrl = (args: {
`?id=${args.coverArtId}` +
`&${args.credential}` +
'&v=1.13.0' +
'&c=feishin' +
'&c=Feishin' +
`&size=${size}`
);
};
interface WithDate {
playDate?: string;
}
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;
@@ -72,10 +145,9 @@ const normalizeSong = (
const imagePlaceholderUrl = null;
return {
album: item.album,
albumArtists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
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,
@@ -90,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,
@@ -100,7 +172,7 @@ const normalizeSong = (
imagePlaceholderUrl,
imageUrl,
itemType: LibraryItem.SONG,
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
lastPlayedAt: normalizePlayDate(item),
lyrics: item.lyrics ? item.lyrics : null,
name: item.title,
path: item.path,
@@ -108,14 +180,17 @@ const normalizeSong = (
item.rgAlbumPeak || item.rgTrackPeak
? { album: item.rgAlbumPeak, track: item.rgTrackPeak }
: null,
playCount: item.playCount,
playCount: item.playCount || 0,
playlistItemId,
releaseDate: new Date(item.year, 0, 1).toISOString(),
releaseDate: (item.releaseDate
? new Date(item.releaseDate)
: 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,
@@ -143,12 +218,13 @@ const normalizeAlbum = (
const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || null;
return {
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
albumArtist: item.albumArtist,
...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,
@@ -159,16 +235,25 @@ const normalizeAlbum = (
imageUrl,
isCompilation: item.compilation,
itemType: LibraryItem.ALBUM,
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
lastPlayedAt: normalizePlayDate(item),
mbzId: item.mbzAlbumId || null,
name: item.name,
playCount: item.playCount,
releaseDate: new Date(item.minYear, 0, 1).toISOString(),
originalDate: item.originalDate
? new Date(item.originalDate).toISOString()
: item.originalYear
? new Date(item.originalYear, 0, 1).toISOString()
: null,
playCount: item.playCount || 0,
releaseDate: (item.releaseDate
? new Date(item.releaseDate)
: new Date(item.minYear, 0, 1)
).toISOString(),
releaseYear: item.minYear,
serverId: server?.id || 'unknown',
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,
@@ -189,16 +274,33 @@ const normalizeAlbumArtist = (
baseUrl: server?.url,
coverArtId: `ar-${item.id}`,
credential: server?.credential,
size: 100,
size: 300,
});
}
let albumCount: number;
let songCount: number;
if (item.stats) {
albumCount = Math.max(
item.stats.albumartist?.albumCount ?? 0,
item.stats.artist?.albumCount ?? 0,
);
songCount = Math.max(
item.stats.albumartist?.songCount ?? 0,
item.stats.artist?.songCount ?? 0,
);
} else {
albumCount = item.albumCount;
songCount = item.songCount;
}
return {
albumCount: item.albumCount,
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,
@@ -207,9 +309,10 @@ const normalizeAlbumArtist = (
id: item.id,
imageUrl: imageUrl || null,
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
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:
@@ -218,7 +321,7 @@ const normalizeAlbumArtist = (
imageUrl: artist?.artistImageUrl || null,
name: artist.name,
})) || null,
songCount: item.songCount,
songCount,
userFavorite: item.starred,
userRating: item.rating,
};
+72 -73
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,61 +70,70 @@ 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(),
playDate: z.string(),
playCount: z.number().optional(),
playDate: z.string().optional(),
rating: z.number(),
size: z.number(),
smallImageUrl: z.string().optional(),
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(),
allArtistIds: z.string(),
artist: z.string(),
artistId: z.string(),
comment: z.string().optional(),
compilation: z.boolean(),
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(),
@@ -127,9 +142,13 @@ const album = z.object({
name: z.string(),
orderAlbumArtistName: z.string(),
orderAlbumName: z.string(),
playCount: z.number(),
playDate: z.string(),
originalDate: z.string().optional(),
originalYear: z.number().optional(),
participants: z.optional(participants),
playCount: z.number().optional(),
playDate: z.string().optional(),
rating: z.number().optional(),
releaseDate: z.string().optional(),
size: z.number(),
songCount: z.number(),
sortAlbumArtistName: z.string(),
@@ -141,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(),
@@ -194,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(),
@@ -209,10 +213,12 @@ const song = z.object({
orderAlbumName: z.string(),
orderArtistName: z.string(),
orderTitle: z.string(),
participants: z.optional(participants),
path: z.string(),
playCount: z.number(),
playDate: z.string(),
playCount: z.number().optional(),
playDate: z.string().optional(),
rating: z.number().optional(),
releaseDate: z.string().optional(),
rgAlbumGain: z.number().optional(),
rgAlbumPeak: z.number().optional(),
rgTrackGain: z.number().optional(),
@@ -224,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(),
@@ -232,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(),
@@ -285,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(),
@@ -342,13 +319,31 @@ const removeFromPlaylistParameters = z.object({
id: z.array(z.string()),
});
const shareItem = z.object({
id: z.string(),
});
const shareItemParameters = z.object({
description: z.string(),
downloadable: z.boolean(),
expires: z.number(),
resourceIds: z.string(),
resourceType: z.string(),
});
const moveItemParameters = z.object({
insert_before: z.string(),
});
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: {
@@ -358,8 +353,10 @@ export const ndType = {
authenticate: authenticateParameters,
createPlaylist: createPlaylistParameters,
genreList: genreListParameters,
moveItem: moveItemParameters,
playlistList: playlistListParameters,
removeFromPlaylist: removeFromPlaylistParameters,
shareItem: shareItemParameters,
songList: songListParameters,
updatePlaylist: updatePlaylistParameters,
userList: userListParameters,
@@ -376,11 +373,13 @@ export const ndType = {
error,
genre,
genreList,
moveItem,
playlist,
playlistList,
playlistSong,
playlistSongList,
removeFromPlaylist,
shareItem,
song,
songList,
updatePlaylist,
+54
View File
@@ -18,6 +18,7 @@ import type {
LyricsQuery,
LyricSearchQuery,
GenreListQuery,
SimilarSongsQuery,
} from './types';
export const splitPaginatedQuery = (key: any) => {
@@ -49,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;
@@ -72,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) => {
@@ -196,6 +231,9 @@ export const queryKeys: Record<
return [serverId, 'playlists', 'songList'] as const;
},
},
roles: {
list: (serverId: string) => [serverId, 'roles'] as const,
},
search: {
list: (serverId: string, query?: SearchQuery) => {
if (query) return [serverId, 'search', 'list', query] as const;
@@ -207,6 +245,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;
@@ -239,6 +289,10 @@ export const queryKeys: Record<
return [serverId, 'songs', 'randomSongList'] as const;
},
root: (serverId: string) => [serverId, 'songs'] as const,
similar: (serverId: string, query?: SimilarSongsQuery) => {
if (query) return [serverId, 'song', 'similar', query] as const;
return [serverId, 'song', 'similar'] as const;
},
},
users: {
list: (serverId: string, query?: UserListQuery) => {
+153 -6
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',
@@ -50,6 +122,53 @@ export const contract = c.router({
200: ssType._response.randomSongList,
},
},
getServerInfo: {
method: 'GET',
path: 'getOpenSubsonicExtensions.view',
responses: {
200: ssType._response.serverInfo,
},
},
getSimilarSongs: {
method: 'GET',
path: 'getSimilarSongs',
query: ssType._parameters.similarSongs,
responses: {
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',
query: ssType._parameters.structuredLyrics,
responses: {
200: ssType._response.structuredLyrics,
},
},
getTopSongsList: {
method: 'GET',
path: 'getTopSongs.view',
@@ -58,6 +177,13 @@ export const contract = c.router({
200: ssType._response.topSongsList,
},
},
ping: {
method: 'GET',
path: 'ping.view',
responses: {
200: ssType._response.ping,
},
},
removeFavorite: {
method: 'GET',
path: 'unstar.view',
@@ -90,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({});
@@ -101,7 +235,6 @@ axiosClient.defaults.paramsSerializer = (params) => {
axiosClient.interceptors.response.use(
(response) => {
const data = response.data;
if (data['subsonic-response'].status !== 'ok') {
// Suppress code related to non-linked lastfm or spotify from Navidrome
if (data['subsonic-response'].error.code !== 0) {
@@ -122,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 {
@@ -131,12 +264,24 @@ const parsePath = (fullPath: string) => {
};
};
const silentlyTransformResponse = (data: any) => {
const jsonBody = JSON.parse(data);
const status = jsonBody ? jsonBody['subsonic-response']?.status : undefined;
if (status && status !== 'ok') {
jsonBody['subsonic-response'].error.code = 0;
}
return jsonBody;
};
export const ssApiClient = (args: {
server: ServerListItem | null;
signal?: AbortSignal;
silent?: boolean;
url?: string;
}) => {
const { server, url, signal } = args;
const { server, url, signal, silent } = args;
return initClient(contract, {
api: async ({ path, method, headers, body }) => {
@@ -150,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;
@@ -176,6 +321,8 @@ export const ssApiClient = (args: {
...params,
},
signal,
// In cases where we have a fallback, don't notify the error
transformResponse: silent ? silentlyTransformResponse : undefined,
url: `${baseUrl}/${api}`,
});
@@ -199,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
+189 -61
View File
@@ -1,8 +1,17 @@
import { nanoid } from 'nanoid';
import { z } from 'zod';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import { QueueSong, LibraryItem, AlbumArtist, Album } from '/@/renderer/api/types';
import { ServerListItem, ServerType } from '/@/renderer/types';
import {
QueueSong,
LibraryItem,
AlbumArtist,
Album,
ServerListItem,
ServerType,
Playlist,
Genre,
RelatedArtist,
} from '/@/renderer/api/types';
const getCoverArtUrl = (args: {
baseUrl: string | undefined;
@@ -21,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,
@@ -69,18 +151,15 @@ const normalizeSong = (
discNumber: item.discNumber || 1,
discSubtitle: null,
duration: item.duration ? item.duration * 1000 : 0,
gain: null,
genres: item.genre
? [
{
id: item.genre,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.genre,
},
]
: [],
id: item.id,
gain:
item.replayGain && (item.replayGain.albumGain || item.replayGain.trackGain)
? {
album: item.replayGain.albumGain,
track: item.replayGain.trackGain,
}
: null,
genres: getGenres(item),
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl,
itemType: LibraryItem.SONG,
@@ -88,7 +167,13 @@ const normalizeSong = (
lyrics: null,
name: item.title,
path: item.path,
peak: null,
peak:
item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak)
? {
album: item.replayGain.albumPeak,
track: item.replayGain.trackPeak,
}
: null,
playCount: item?.playCount || 0,
releaseDate: null,
releaseYear: item.year ? String(item.year) : null,
@@ -105,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 {
@@ -122,10 +210,11 @@ const normalizeAlbumArtist = (
biography: null,
duration: null,
genres: [],
id: item.id,
id: item.id.toString(),
imageUrl,
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: null,
mbz: null,
name: item.name,
playCount: null,
serverId: server?.id || 'unknown',
@@ -138,50 +227,46 @@ 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 {
albumArtists: item.artistId
? [{ id: item.artistId, imageUrl: null, name: item.artist }]
: [],
artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
albumArtist: 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,
itemType: LibraryItem.ALBUM,
lastPlayedAt: null,
mbzId: null,
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,
@@ -189,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,
};
+369 -17
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(),
});
@@ -53,25 +55,54 @@ const musicFolderList = z.object({
}),
});
const songGain = z.object({
albumGain: z.number().optional(),
albumPeak: z.number().optional(),
trackGain: z.number().optional(),
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(),
replayGain: songGain.optional(),
size: z.number(),
starred: z.boolean().optional(),
suffix: z.string(),
@@ -84,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(),
@@ -103,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(),
@@ -116,11 +156,14 @@ const albumListParameters = z.object({
const albumList = z.array(album.omit({ song: true }));
const albumArtist = z.object({
album: z.array(album).optional(),
albumCount: z.string(),
artistImageUrl: z.string().optional(),
coverArt: z.string().optional(),
id: z.string(),
id,
name: z.string(),
roles: z.array(z.string()).optional(),
starred: z.string().optional(),
});
const albumArtistList = z.object({
@@ -128,6 +171,15 @@ const albumArtistList = z.object({
name: z.string(),
});
const artistListEntry = albumArtist.pick({
albumCount: true,
coverArt: true,
id: true,
name: true,
roles: true,
starred: true,
});
const artistInfoParameters = z.object({
count: z.number().optional(),
id: z.string(),
@@ -160,9 +212,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({
@@ -174,11 +228,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({
@@ -201,8 +257,268 @@ 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({
openSubsonic: z.boolean().optional(),
serverVersion: z.string().optional(),
version: z.string(),
});
const extension = z.object({
name: z.string(),
versions: z.number().array(),
});
const serverInfo = z.object({
openSubsonicExtensions: z.array(extension).optional(),
});
const structuredLyricsParameters = z.object({
id: z.string(),
});
const lyricLine = z.object({
start: z.number().optional(),
value: z.string(),
});
const structuredLyric = z.object({
displayArtist: z.string().optional(),
displayTitle: z.string().optional(),
lang: z.string(),
line: z.array(lyricLine),
offset: z.number().optional(),
synced: z.boolean(),
});
const structuredLyrics = z.object({
lyricsList: z
.object({
structuredLyrics: z.array(structuredLyric).optional(),
})
.optional(),
});
const similarSongsParameters = z.object({
count: z.number().optional(),
id: z.string(),
});
const similarSongs = z.object({
similarSongs: z
.object({
song: z.array(song),
})
.optional(),
});
export enum SubsonicExtensions {
FORM_POST = 'formPost',
SONG_LYRICS = 'songLyrics',
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),
}),
});
@@ -212,29 +528,65 @@ export const ssType = {
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,
search3: search3Parameters,
setRating: setRatingParameters,
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,
search3,
serverInfo,
setRating,
similarSongs,
song,
structuredLyrics,
topSongsList,
},
};
+366 -52
View File
@@ -1,4 +1,8 @@
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';
import {
JFSortOrder,
@@ -57,13 +61,16 @@ export type User = {
export type ServerListItem = {
credential: string;
features?: ServerFeatures;
id: string;
name: string;
ndCredential?: string;
savePassword?: boolean;
type: ServerType;
url: string;
userId: string | null;
username: string;
version?: string;
};
export enum ServerType {
@@ -124,7 +131,7 @@ export interface BasePaginatedResponse<T> {
error?: string | any;
items: T;
startIndex: number;
totalRecordCount: number;
totalRecordCount: number | null;
}
export type AuthenticationResponse = {
@@ -144,9 +151,11 @@ export type Genre = {
};
export type Album = {
albumArtist: string;
albumArtists: RelatedArtist[];
artists: RelatedArtist[];
backdropImageUrl: string | null;
comment: string | null;
createdAt: string;
duration: number | null;
genres: Genre[];
@@ -156,7 +165,10 @@ export type Album = {
isCompilation: boolean | null;
itemType: LibraryItem.ALBUM;
lastPlayedAt: string | null;
mbzId: string | null;
name: string;
originalDate: string | null;
participants: Record<string, RelatedArtist[]> | null;
playCount: number | null;
releaseDate: string | null;
releaseYear: number | null;
@@ -201,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;
@@ -228,6 +241,7 @@ export type AlbumArtist = {
imageUrl: string | null;
itemType: LibraryItem.ALBUM_ARTIST;
lastPlayedAt: string | null;
mbz: string | null;
name: string;
playCount: number | null;
serverId: string;
@@ -300,6 +314,11 @@ type BaseEndpointArgs = {
};
};
export interface BaseQuery<T> {
sortBy: T;
sortOrder: SortOrder;
}
// Genre List
export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined;
@@ -309,7 +328,7 @@ export enum GenreListSort {
NAME = 'name',
}
export type GenreListQuery = {
export interface GenreListQuery extends BaseQuery<GenreListSort> {
_custom?: {
jellyfin?: null;
navidrome?: null;
@@ -317,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>;
@@ -361,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;
@@ -417,7 +434,8 @@ export const albumListSortMap: AlbumListSortMap = {
rating: NDAlbumListSort.RATING,
recentlyAdded: NDAlbumListSort.RECENTLY_ADDED,
recentlyPlayed: NDAlbumListSort.PLAY_DATE,
releaseDate: undefined,
// Recent versions of Navidrome support release date, but fallback to year for now
releaseDate: NDAlbumListSort.YEAR,
songCount: NDAlbumListSort.SONG_COUNT,
year: NDAlbumListSort.YEAR,
},
@@ -471,24 +489,25 @@ 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>>;
};
albumArtistIds?: string[];
albumIds?: string[];
artistIds?: string[];
favorite?: boolean;
genreIds?: string[];
imageSize?: number;
limit?: number;
maxYear?: number;
minYear?: number;
musicFolderId?: string;
role?: string;
searchTerm?: string;
sortBy: SongListSort;
sortOrder: SortOrder;
startIndex: number;
};
}
export type SongListArgs = { query: SongListQuery } & BaseEndpointArgs;
@@ -532,7 +551,7 @@ export const songListSortMap: SongListSortMap = {
id: NDSongListSort.ID,
name: NDSongListSort.TITLE,
playCount: NDSongListSort.PLAY_COUNT,
random: undefined,
random: NDSongListSort.RANDOM,
rating: NDSongListSort.RATING,
recentlyAdded: NDSongListSort.RECENTLY_ADDED,
recentlyPlayed: NDSongListSort.PLAY_DATE,
@@ -585,7 +604,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>>;
@@ -593,10 +612,8 @@ export type AlbumArtistListQuery = {
limit?: number;
musicFolderId?: string;
searchTerm?: string;
sortBy: AlbumArtistListSort;
sortOrder: SortOrder;
startIndex: number;
};
}
export type AlbumArtistListArgs = { query: AlbumArtistListQuery } & BaseEndpointArgs;
@@ -657,7 +674,7 @@ export type AlbumArtistDetailQuery = { id: string };
export type AlbumArtistDetailArgs = { query: AlbumArtistDetailQuery } & BaseEndpointArgs;
// Artist List
export type ArtistListResponse = BasePaginatedResponse<Artist[]> | null | undefined;
export type ArtistListResponse = BasePaginatedResponse<AlbumArtist[]> | null | undefined;
export enum ArtistListSort {
ALBUM = 'album',
@@ -673,17 +690,17 @@ 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;
role?: string;
searchTerm?: string;
startIndex: number;
};
}
export type ArtistListArgs = { query: ArtistListQuery } & BaseEndpointArgs;
@@ -757,6 +774,19 @@ export type RatingQuery = {
export type SetRatingArgs = { query: RatingQuery; serverId?: string } & BaseEndpointArgs;
// Sharing
export type ShareItemResponse = { id: string } | undefined;
export type ShareItemBody = {
description: string;
downloadable: boolean;
expires: number;
resourceIds: string;
resourceType: string;
};
export type ShareItemArgs = { body: ShareItemBody; serverId?: string } & BaseEndpointArgs;
// Add to playlist
export type AddToPlaylistResponse = null | undefined;
@@ -795,13 +825,13 @@ export type CreatePlaylistBody = {
navidrome?: {
owner?: string;
ownerId?: string;
public?: boolean;
rules?: Record<string, any>;
sync?: boolean;
};
};
comment?: string;
name: string;
public?: boolean;
};
export type CreatePlaylistArgs = { body: CreatePlaylistBody; serverId?: string } & BaseEndpointArgs;
@@ -818,7 +848,6 @@ export type UpdatePlaylistBody = {
navidrome?: {
owner?: string;
ownerId?: string;
public?: boolean;
rules?: Record<string, any>;
sync?: boolean;
};
@@ -826,6 +855,7 @@ export type UpdatePlaylistBody = {
comment?: string;
genres?: Genre[];
name: string;
public?: boolean;
};
export type UpdatePlaylistArgs = {
@@ -856,17 +886,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;
@@ -940,7 +968,7 @@ export enum UserListSort {
NAME = 'name',
}
export type UserListQuery = {
export interface UserListQuery extends BaseQuery<UserListSort> {
_custom?: {
navidrome?: {
owner_id?: string;
@@ -948,10 +976,8 @@ export type UserListQuery = {
};
limit?: number;
searchTerm?: string;
sortBy: UserListSort;
sortOrder: SortOrder;
startIndex: number;
};
}
export type UserListArgs = { query: UserListQuery } & BaseEndpointArgs;
@@ -1050,12 +1076,19 @@ export type SearchResponse = {
songs: Song[];
};
export enum Played {
All = 'all',
Never = 'never',
Played = 'played',
}
export type RandomSongListQuery = {
genre?: string;
limit?: number;
maxYear?: number;
minYear?: number;
musicFolderId?: string;
played: Played;
};
export type RandomSongListArgs = {
@@ -1092,17 +1125,11 @@ export type InternetProviderLyricSearchResponse = {
source: LyricSource;
};
export type SynchronizedLyricMetadata = {
lyrics: SynchronizedLyricsArray;
export type FullLyricsMetadata = {
lyrics: LyricsResponse;
remote: boolean;
} & Omit<InternetProviderLyricResponse, 'lyrics'>;
export type UnsynchronizedLyricMetadata = {
lyrics: string;
remote: boolean;
} & Omit<InternetProviderLyricResponse, 'lyrics'>;
export type FullLyricsMetadata = SynchronizedLyricMetadata | UnsynchronizedLyricMetadata;
source: string;
} & Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'>;
export type LyricOverride = Omit<InternetProviderLyricResponse, 'lyrics'>;
@@ -1139,3 +1166,290 @@ export type FontData = {
postscriptName: string;
style: string;
};
export type ServerInfoArgs = BaseEndpointArgs;
export type ServerInfo = {
features: ServerFeatures;
id?: string;
version: string;
};
export type StructuredLyricsArgs = {
query: LyricsQuery;
} & BaseEndpointArgs;
export type StructuredUnsyncedLyric = {
lyrics: string;
synced: false;
} & Omit<FullLyricsMetadata, 'lyrics'>;
export type StructuredSyncedLyric = {
lyrics: SynchronizedLyricsArray;
synced: true;
} & Omit<FullLyricsMetadata, 'lyrics'>;
export type StructuredLyric = {
lang: string;
} & (StructuredUnsyncedLyric | StructuredSyncedLyric);
export type SimilarSongsQuery = {
albumArtistIds: string[];
count?: number;
songId: string;
};
export type SimilarSongsArgs = {
query: SimilarSongsQuery;
} & BaseEndpointArgs;
export type MoveItemQuery = {
endingIndex: number;
playlistId: string;
startingIndex: number;
trackId: string;
};
export type MoveItemArgs = {
query: MoveItemQuery;
} & BaseEndpointArgs;
export type DownloadQuery = {
id: string;
};
export type DownloadArgs = {
query: DownloadQuery;
} & BaseEndpointArgs;
export type TranscodingQuery = {
base: string;
bitrate?: number;
format?: string;
};
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>;
getArtistListCount: (args: ArtistListArgs) => Promise<number>;
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>;
getRoles: (args: BaseEndpointArgs) => Promise<Array<string | { label: string; value: string }>>;
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 | ArtistListSort,
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;
};
+90 -2
View File
@@ -1,8 +1,12 @@
import { AxiosHeaders } from 'axios';
import isElectron from 'is-electron';
import semverCoerce from 'semver/functions/coerce';
import semverGte from 'semver/functions/gte';
import { z } from 'zod';
import { toast } from '/@/renderer/components';
import { toast } from '/@/renderer/components/toast';
import { useAuthStore } from '/@/renderer/store';
import { ServerListItem } from '/@/renderer/types';
import { ServerListItem } from '/@/renderer/api/types';
import { ServerFeature } from '/@/renderer/api/features-types';
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
@@ -38,3 +42,87 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => {
useAuthStore.getState().actions.setCurrentServer(null);
}
};
export const hasFeature = (server: ServerListItem | null, feature: ServerFeature): boolean => {
if (!server || !server.features) {
return false;
}
return server.features[feature] ?? false;
};
export type VersionInfo = ReadonlyArray<[string, Record<string, readonly number[]>]>;
/**
* Returns the available server features given the version string.
* @param versionInfo a list, in DECREASING VERSION order, of the features supported by the server.
* The first version match will automatically consider the rest matched.
* @example
* ```
* // The CORRECT way to order
* const VERSION_INFO: VersionInfo = [
* ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
* ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
* ];
* // INCORRECT way to order
* const VERSION_INFO: VersionInfo = [
* ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
* ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
* ];
* ```
* @param version the version string (SemVer)
* @returns a Record containing the matched features (if any) and their versions
*/
export const getFeatures = (
versionInfo: VersionInfo,
version: string,
): Record<string, number[]> => {
const cleanVersion = semverCoerce(version);
const features: Record<string, number[]> = {};
let matched = cleanVersion === null;
for (const [version, supportedFeatures] of versionInfo) {
if (!matched) {
matched = semverGte(cleanVersion!, version);
}
if (matched) {
for (const [feature, feat] of Object.entries(supportedFeatures)) {
if (feature in features) {
features[feature].push(...feat);
} else {
features[feature] = [...feat];
}
}
}
}
return features;
};
export const getClientType = (): string => {
if (isElectron()) {
return 'Desktop Client';
}
const agent = navigator.userAgent;
switch (true) {
case agent.toLowerCase().indexOf('edge') > -1:
return 'Microsoft Edge';
case agent.toLowerCase().indexOf('edg/') > -1:
return 'Edge Chromium'; // Match also / to avoid matching for the older Edge
case agent.toLowerCase().indexOf('opr') > -1:
return 'Opera';
case agent.toLowerCase().indexOf('chrome') > -1:
return 'Chrome';
case agent.toLowerCase().indexOf('trident') > -1:
return 'Internet Explorer';
case agent.toLowerCase().indexOf('firefox') > -1:
return 'Firefox';
case agent.toLowerCase().indexOf('safari') > -1:
return 'Safari';
default:
return 'PC';
}
};
export const SEPARATOR_STRING = ' · ';
+77 -49
View File
@@ -1,12 +1,11 @@
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';
import { MantineProvider } from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import isElectron from 'is-electron';
import { initSimpleImg } from 'react-simple-img';
import { BaseContextModal, toast } from './components';
import { toast } from './components';
import { useTheme } from './hooks';
import { IsUpdatedDialog } from './is-updated-dialog';
import { AppRouter } from './router/app-router';
@@ -20,35 +19,43 @@ import './styles/global.scss';
import { ContextMenuProvider } from '/@/renderer/features/context-menu';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types';
import { PlayerState, useCssSettings, usePlayerStore, useQueueControls } from '/@/renderer/store';
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';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
import { sanitizeCss } from '/@/renderer/utils/sanitize';
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
initSimpleImg({ threshold: 0.05 }, true);
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
const ipc = isElectron() ? window.electron.ipc : null;
const remote = isElectron() ? window.electron.remote : null;
const utils = isElectron() ? window.electron.utils : null;
export const App = () => {
const theme = useTheme();
const accent = useSettingsStore((store) => store.general.accent);
const language = useSettingsStore((store) => store.general.language);
const nativeImageAspect = useSettingsStore((store) => store.general.nativeAspectRatio);
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
const { enabled, content } = useCssSettings();
const { type: playbackType } = usePlaybackSettings();
const { bindings } = useHotkeySettings();
const handlePlayQueueAdd = useHandlePlayQueueAdd();
const { clearQueue, restoreQueue } = useQueueControls();
const remoteSettings = useRemoteSettings();
const textStyleRef = useRef<HTMLStyleElement>();
const cssRef = useRef<HTMLStyleElement>();
useDiscordRpc();
useServerVersion();
useEffect(() => {
if (type === FontType.SYSTEM && system) {
@@ -85,40 +92,74 @@ 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
// localStorage to bypass sanitizing.
const sanitized = sanitizeCss(content);
if (!cssRef.current) {
cssRef.current = document.createElement('style');
document.body.appendChild(cssRef.current);
}
cssRef.current.textContent = sanitized;
return () => {
cssRef.current!.textContent = '';
};
}
return () => {};
}, [content, enabled]);
useEffect(() => {
const root = document.documentElement;
root.style.setProperty('--primary-color', accent);
}, [accent]);
useEffect(() => {
const root = document.documentElement;
root.style.setProperty('--image-fit', nativeImageAspect ? 'contain' : 'cover');
}, [nativeImageAspect]);
const providerValue = useMemo(() => {
return { handlePlayQueueAdd };
}, [handlePlayQueueAdd]);
const webAudioProvider = useMemo(() => {
return { setWebAudio, webAudio };
}, [webAudio]);
// Start the mpv instance on startup
useEffect(() => {
const initializeMpv = async () => {
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
if (playbackType === PlaybackType.LOCAL) {
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
mpvPlayer?.stop();
mpvPlayer?.stop();
if (!isRunning) {
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
const properties: Record<string, any> = {
speed: usePlayerStore.getState().current.speed,
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
};
if (!isRunning) {
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
const properties: Record<string, any> = {
speed: usePlayerStore.getState().speed,
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
};
mpvPlayer?.initialize({
extraParameters,
properties,
});
await mpvPlayer?.initialize({
extraParameters,
properties,
});
mpvPlayer?.volume(properties.volume);
mpvPlayer?.volume(properties.volume);
}
}
mpvPlayer?.restoreQueue();
utils?.restoreQueue();
};
if (isElectron() && playbackType === PlaybackType.LOCAL) {
if (isElectron()) {
initializeMpv();
}
@@ -136,8 +177,8 @@ export const App = () => {
}, [bindings]);
useEffect(() => {
if (isElectron()) {
mpvPlayerListener!.rendererSaveQueue(() => {
if (utils) {
utils.onSaveQueue(() => {
const { current, queue } = usePlayerStore.getState();
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
current: {
@@ -146,20 +187,21 @@ export const App = () => {
},
queue,
};
mpvPlayer!.saveQueue(stateToSave);
utils.saveQueue(stateToSave);
});
mpvPlayerListener!.rendererRestoreQueue((_event: any, data) => {
utils.onRestoreQueue((_event: any, data) => {
const playerData = restoreQueue(data);
if (playbackType === PlaybackType.LOCAL) {
mpvPlayer!.setQueue(playerData, true);
setQueue(playerData, true);
}
updateSong(playerData.current.song);
});
}
return () => {
ipc?.removeAllListeners('renderer-player-restore-queue');
ipc?.removeAllListeners('renderer-player-save-queue');
ipc?.removeAllListeners('renderer-restore-queue');
ipc?.removeAllListeners('renderer-save-queue');
};
}, [playbackType, restoreQueue]);
@@ -241,27 +283,13 @@ export const App = () => {
},
}}
>
<ModalsProvider
modalProps={{
centered: true,
styles: {
body: { position: 'relative' },
content: { overflow: 'auto' },
},
transitionProps: {
duration: 300,
exitDuration: 300,
transition: 'fade',
},
}}
modals={{ addToPlaylist: AddToPlaylistContextModal, base: BaseContextModal }}
>
<PlayQueueHandlerContext.Provider value={providerValue}>
<ContextMenuProvider>
<PlayQueueHandlerContext.Provider value={providerValue}>
<ContextMenuProvider>
<WebAudioContext.Provider value={webAudioProvider}>
<AppRouter />
</ContextMenuProvider>
</PlayQueueHandlerContext.Provider>
</ModalsProvider>
</WebAudioContext.Provider>{' '}
</ContextMenuProvider>
</PlayQueueHandlerContext.Provider>
<IsUpdatedDialog />
</MantineProvider>
);
+169 -56
View File
@@ -1,4 +1,12 @@
import { useImperativeHandle, forwardRef, useRef, useState, useCallback, useEffect } from 'react';
import {
useImperativeHandle,
forwardRef,
useRef,
useState,
useCallback,
useEffect,
useMemo,
} from 'react';
import isElectron from 'is-electron';
import type { ReactPlayerProps } from 'react-player';
import ReactPlayer from 'react-player/lazy';
@@ -7,18 +15,21 @@ import {
crossfadeHandler,
gaplessHandler,
} from '/@/renderer/components/audio-player/utils/list-handlers';
import { useSettingsStore } from '/@/renderer/store/settings.store';
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import type { CrossfadeStyle } from '/@/renderer/types';
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
import { useSpeed } from '/@/renderer/store';
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';
interface AudioPlayerProps extends ReactPlayerProps {
crossfadeDuration: number;
crossfadeStyle: CrossfadeStyle;
currentPlayer: 1 | 2;
playbackStyle: PlaybackStyle;
player1: Song;
player2: Song;
player1?: Song;
player2?: Song;
status: PlayerStatus;
volume: number;
}
@@ -34,9 +45,50 @@ 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
// the first time. This workaround is important for Safari, which seems to require the
// source to be connected PRIOR to resuming audio context
const EMPTY_SOURCE =
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song): string | null => {
const prior = useRef(['', '']);
return useMemo(() => {
if (song?.serverId) {
// If we are the current track, we do not want a transcoding
// reconfiguration to force a restart.
if (current && prior.current[0] === song.uniqueId) {
return prior.current[1];
}
if (!transcode.enabled) {
// transcoding disabled; save the result
prior.current = [song.uniqueId, song.streamUrl];
return song.streamUrl;
}
const result = api.controller.getTranscodingUrl({
apiClientProps: {
server: getServerById(song.serverId),
},
query: {
base: song.streamUrl,
...transcode,
},
})!;
// transcoding enabled; save the updated result
prior.current = [song.uniqueId, result];
return result;
}
// no track; clear result
prior.current = ['', ''];
return null;
}, [current, song?.uniqueId, song?.serverId, song?.streamUrl, transcode]);
};
export const AudioPlayer = forwardRef(
@@ -60,15 +112,22 @@ export const AudioPlayer = forwardRef(
const [isTransitioning, setIsTransitioning] = useState(false);
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
const playback = useSettingsStore((state) => state.playback.mpvProperties);
const shouldUseWebAudio = useSettingsStore((state) => state.playback.webAudio);
const { resetSampleRate } = useSettingsStoreActions();
const playbackSpeed = useSpeed();
const { transcode } = usePlaybackSettings();
const [webAudio, setWebAudio] = useState<WebAudio | null>(null);
const stream1 = useSongUrl(transcode, currentPlayer === 1, player1);
const stream2 = useSongUrl(transcode, currentPlayer === 2, player2);
const { webAudio, setWebAudio } = useWebAudio();
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
null,
);
const [player2Source, setPlayer2Source] = useState<MediaElementAudioSourceNode | null>(
null,
);
const calculateReplayGain = useCallback(
(song: Song): number => {
if (playback.replayGainMode === 'no') {
@@ -118,15 +177,26 @@ export const AudioPlayer = forwardRef(
);
useEffect(() => {
if ('AudioContext' in window) {
const context = new AudioContext({
latencyHint: 'playback',
sampleRate: playback.audioSampleRateHz || undefined,
});
if (shouldUseWebAudio && 'AudioContext' in window) {
let context: AudioContext;
try {
context = new AudioContext({
latencyHint: 'playback',
sampleRate: playback.audioSampleRateHz || undefined,
});
} catch (error) {
// In practice, this should never be hit because the UI should validate
// the range. However, the actual supported range is not guaranteed
toast.error({ message: (error as Error).message });
context = new AudioContext({ latencyHint: 'playback' });
resetSampleRate();
}
const gain = context.createGain();
gain.connect(context.destination);
setWebAudio({ context, gain });
setWebAudio!({ context, gain });
return () => {
return context.close();
@@ -154,9 +224,18 @@ export const AudioPlayer = forwardRef(
useEffect(() => {
if (status === PlayerStatus.PLAYING) {
if (currentPlayer === 1) {
player1Ref.current?.getInternalPlayer()?.play();
// calling play() is not necessarily a safe option (https://developer.chrome.com/blog/play-request-was-interrupted)
// In practice, this failure is only likely to happen when using the 0-second wav:
// play() + play() in rapid succession will cause problems as the frist one ends the track.
player1Ref.current
?.getInternalPlayer()
?.play()
.catch(() => {});
} else {
player2Ref.current?.getInternalPlayer()?.play();
player2Ref.current
?.getInternalPlayer()
?.play()
.catch(() => {});
}
} else {
player1Ref.current?.getInternalPlayer()?.pause();
@@ -231,44 +310,71 @@ export const AudioPlayer = forwardRef(
);
useEffect(() => {
if (isElectron()) {
if (audioDeviceId) {
player1Ref.current?.getInternalPlayer()?.setSinkId(audioDeviceId);
player2Ref.current?.getInternalPlayer()?.setSinkId(audioDeviceId);
} else {
player1Ref.current?.getInternalPlayer()?.setSinkId('');
player2Ref.current?.getInternalPlayer()?.setSinkId('');
}
// Not standard, just used in chromium-based browsers. See
// https://developer.chrome.com/blog/audiocontext-setsinkid/.
// If the isElectron() check is every removed, fix this.
if (isElectron() && webAudio && 'setSinkId' in webAudio.context && audioDeviceId) {
const setSink = async () => {
try {
if (audioDeviceId !== 'default') {
// @ts-ignore
await webAudio.context.setSinkId(audioDeviceId);
} else {
// @ts-ignore
await webAudio.context.setSinkId('');
}
} catch (error) {
toast.error({ message: `Error setting sink: ${(error as Error).message}` });
}
};
setSink();
}
}, [audioDeviceId]);
}, [audioDeviceId, webAudio]);
useEffect(() => {
if (webAudio && player1Source) {
if (player1 === undefined) {
player1Source.disconnect();
setPlayer1Source(null);
} else if (currentPlayer === 1) {
webAudio.gain.gain.setValueAtTime(calculateReplayGain(player1), 0);
}
}
}, [calculateReplayGain, currentPlayer, player1, player1Source, webAudio]);
if (!webAudio) return;
useEffect(() => {
if (webAudio && player2Source) {
if (player2 === undefined) {
player2Source.disconnect();
setPlayer2Source(null);
} else if (currentPlayer === 2) {
webAudio.gain.gain.setValueAtTime(calculateReplayGain(player2), 0);
}
const sources = [player1Source ? player1 : null, player2Source ? player2 : null];
const current = sources[currentPlayer - 1];
// Set the current replaygain
if (current) {
const newVolume = calculateReplayGain(current) * volume;
webAudio.gain.gain.setValueAtTime(Math.max(0, newVolume), 0);
}
}, [calculateReplayGain, currentPlayer, player2, player2Source, webAudio]);
// Set the next track replaygain right before the end of this track
// Attempt to prevent pop-in for web audio.
const next = sources[3 - currentPlayer];
if (next && current) {
const newVolume = calculateReplayGain(next) * volume;
webAudio.gain.gain.setValueAtTime(
Math.max(0, newVolume),
Math.max(0, (current.duration - 1) / 1000),
);
}
}, [
calculateReplayGain,
currentPlayer,
player1,
player1Source,
player2,
player2Source,
volume,
webAudio,
]);
const handlePlayer1Start = useCallback(
async (player: ReactPlayer) => {
if (!webAudio || player1Source) return;
if (webAudio.context.state !== 'running') {
await webAudio.context.resume();
if (!webAudio) return;
if (player1Source) {
// This should fire once, only if the source is real (meaning we
// saw the dummy source) and the context is not ready
if (webAudio.context.state !== 'running') {
await webAudio.context.resume();
}
return;
}
const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
@@ -284,9 +390,12 @@ export const AudioPlayer = forwardRef(
const handlePlayer2Start = useCallback(
async (player: ReactPlayer) => {
if (!webAudio || player2Source) return;
if (webAudio.context.state !== 'running') {
await webAudio.context.resume();
if (!webAudio) return;
if (player2Source) {
if (webAudio.context.state !== 'running') {
await webAudio.context.resume();
}
return;
}
const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
@@ -300,6 +409,9 @@ export const AudioPlayer = forwardRef(
[player2Source, webAudio],
);
// Bugfix for Safari: rather than use the `<audio>` volume (which doesn't work),
// use the GainNode to scale the volume. In this case, for compatibility with
// other browsers, set the `<audio>` volume to 1
return (
<>
<ReactPlayer
@@ -312,10 +424,11 @@ export const AudioPlayer = forwardRef(
playbackRate={playbackSpeed}
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
url={player1?.streamUrl}
volume={volume}
url={stream1 || EMPTY_SOURCE}
volume={webAudio ? 1 : volume}
width={0}
onEnded={handleOnEnded}
// If there is no stream url, we do not need to handle when the audio finishes
onEnded={stream1 ? handleOnEnded : undefined}
onProgress={
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
}
@@ -331,10 +444,10 @@ export const AudioPlayer = forwardRef(
playbackRate={playbackSpeed}
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
url={player2?.streamUrl}
volume={volume}
url={stream2 || EMPTY_SOURCE}
volume={webAudio ? 1 : volume}
width={0}
onEnded={handleOnEnded}
onEnded={stream2 ? handleOnEnded : undefined}
onProgress={
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
}
@@ -23,7 +23,10 @@ export const gaplessHandler = (args: {
const durationPadding = isFlac ? 0.065 : 0.116;
if (currentTime + durationPadding >= duration) {
return nextPlayerRef.current.getInternalPlayer()?.play();
return nextPlayerRef.current
.getInternalPlayer()
?.play()
.catch(() => {});
}
return null;
@@ -57,11 +60,15 @@ export const crossfadeHandler = (args: {
} = args;
if (!isTransitioning || currentPlayer !== player) {
const shouldBeginTransition = currentTime >= duration - fadeDuration;
// check for a large-enough duration, as the default audio element has some dummy audio
const shouldBeginTransition = duration > 0.5 && currentTime >= duration - fadeDuration;
if (shouldBeginTransition) {
setIsTransitioning(true);
return nextPlayerRef.current.getInternalPlayer().play();
return nextPlayerRef.current
.getInternalPlayer()
?.play()
.catch(() => {});
}
return null;
}
@@ -94,10 +101,10 @@ export const crossfadeHandler = (args: {
fadeType === 'constantPower'
? 0
: fadeType === 'constantPowerSlowFade'
? 1
: fadeType === 'constantPowerSlowCut'
? 3
: 10;
? 1
: fadeType === 'constantPowerSlowCut'
? 3
: 10;
percentageOfFadeLeft = timeLeft / fadeDuration;
currentPlayerVolumeCalculation =
+4 -1
View File
@@ -24,7 +24,10 @@ const StyledButton = styled(MantineButton)<StyledButtonProps>`
background: ${(props) => `var(--btn-${props.variant}-bg)`};
border: ${(props) => `var(--btn-${props.variant}-border)`};
border-radius: ${(props) => `var(--btn-${props.variant}-radius)`};
transition: background 0.2s ease-in-out, color 0.2s ease-in-out, border 0.2s ease-in-out;
transition:
background 0.2s ease-in-out,
color 0.2s ease-in-out,
border 0.2s ease-in-out;
svg {
fill: ${(props) => `var(--btn-${props.variant}-fg)`};
+4 -2
View File
@@ -17,7 +17,9 @@ const CardWrapper = styled.div<{
cursor: ${({ link }) => link && 'pointer'};
background: var(--card-default-bg);
border-radius: var(--card-default-radius);
transition: border 0.2s ease-in-out, background 0.2s ease-in-out;
transition:
border 0.2s ease-in-out,
background 0.2s ease-in-out;
&:hover {
background: var(--card-default-bg-hover);
@@ -200,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}
+24 -8
View File
@@ -1,4 +1,5 @@
import React from 'react';
import formatDuration from 'format-duration';
import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
@@ -6,6 +7,7 @@ import { Album, AlbumArtist, Artist, Playlist, Song } from '/@/renderer/api/type
import { Text } from '/@/renderer/components/text';
import { AppRoute } from '/@/renderer/router/routes';
import { CardRow } from '/@/renderer/types';
import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
const Row = styled.div<{ $secondary?: boolean }>`
width: 100%;
@@ -69,7 +71,10 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
)}
onClick={(e) => e.stopPropagation()}
>
{row.arrayProperty && item[row.arrayProperty]}
{row.arrayProperty &&
(row.format
? row.format(item)
: item[row.arrayProperty])}
</Text>
</React.Fragment>
))}
@@ -88,7 +93,8 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
overflow="hidden"
size={index > 0 ? 'sm' : 'md'}
>
{row.arrayProperty && item[row.arrayProperty]}
{row.arrayProperty &&
(row.format ? row.format(item) : item[row.arrayProperty])}
</Text>
))}
</Row>
@@ -114,7 +120,7 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
)}
onClick={(e) => e.stopPropagation()}
>
{data && data[row.property]}
{data && (row.format ? row.format(data) : data[row.property])}
</Text>
) : (
<Text
@@ -123,7 +129,7 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
overflow="hidden"
size={index > 0 ? 'sm' : 'md'}
>
{data && data[row.property]}
{data && (row.format ? row.format(data) : data[row.property])}
</Text>
)}
</Row>
@@ -151,12 +157,15 @@ export const ALBUM_CARD_ROWS: { [key: string]: CardRow<Album> } = {
},
},
createdAt: {
format: (song) => formatDateAbsolute(song.createdAt),
property: 'createdAt',
},
duration: {
format: (album) => (album.duration === null ? null : formatDuration(album.duration)),
property: 'duration',
},
lastPlayedAt: {
format: (album) => formatDateRelative(album.lastPlayedAt),
property: 'lastPlayedAt',
},
name: {
@@ -170,6 +179,7 @@ export const ALBUM_CARD_ROWS: { [key: string]: CardRow<Album> } = {
property: 'playCount',
},
rating: {
format: (album) => formatRating(album),
property: 'userRating',
},
releaseDate: {
@@ -208,12 +218,15 @@ export const SONG_CARD_ROWS: { [key: string]: CardRow<Song> } = {
},
},
createdAt: {
format: (song) => formatDateAbsolute(song.createdAt),
property: 'createdAt',
},
duration: {
format: (song) => (song.duration === null ? null : formatDuration(song.duration)),
property: 'duration',
},
lastPlayedAt: {
format: (song) => formatDateRelative(song.lastPlayedAt),
property: 'lastPlayedAt',
},
name: {
@@ -227,6 +240,7 @@ export const SONG_CARD_ROWS: { [key: string]: CardRow<Song> } = {
property: 'playCount',
},
rating: {
format: (song) => formatRating(song),
property: 'userRating',
},
releaseDate: {
@@ -242,12 +256,14 @@ export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow<AlbumArtist> } = {
property: 'albumCount',
},
duration: {
format: (artist) => (artist.duration === null ? null : formatDuration(artist.duration)),
property: 'duration',
},
genres: {
property: 'genres',
},
lastPlayedAt: {
format: (artist) => formatDateRelative(artist.lastPlayedAt),
property: 'lastPlayedAt',
},
name: {
@@ -261,6 +277,7 @@ export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow<AlbumArtist> } = {
property: 'playCount',
},
rating: {
format: (artist) => formatRating(artist),
property: 'userRating',
},
songCount: {
@@ -270,12 +287,14 @@ export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow<AlbumArtist> } = {
export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow<Playlist> } = {
duration: {
format: (playlist) =>
playlist.duration === null ? null : formatDuration(playlist.duration),
property: 'duration',
},
name: {
property: 'name',
route: {
route: AppRoute.PLAYLISTS_DETAIL,
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
},
},
@@ -295,7 +314,4 @@ export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow<Playlist> } = {
songCount: {
property: 'songCount',
},
updatedAt: {
property: 'songCount',
},
};
+2 -2
View File
@@ -92,7 +92,7 @@ const Image = styled(SimpleImg)`
img {
height: 100%;
object-fit: cover;
object-fit: var(--image-fit);
}
`;
@@ -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
@@ -14,6 +14,7 @@ import { Badge } from '/@/renderer/components/badge';
import { AppRoute } from '/@/renderer/router/routes';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { Play } from '/@/renderer/types';
import { usePlayButtonBehavior } from '/@/renderer/store';
const Carousel = styled(motion.div)`
position: relative;
@@ -62,7 +63,7 @@ const BackgroundImage = styled.img`
height: 150%;
user-select: none;
filter: blur(24px);
object-fit: cover;
object-fit: var(--image-fit);
object-position: 0 30%;
`;
@@ -114,6 +115,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
const handlePlayQueueAdd = usePlayQueueAdd();
const [itemIndex, setItemIndex] = useState(0);
const [direction, setDirection] = useState(0);
const playType = usePlayButtonBehavior();
const currentItem = data?.[itemIndex];
@@ -205,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
@@ -222,11 +228,18 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
id: [currentItem.id],
type: LibraryItem.ALBUM,
},
playType: Play.NOW,
playType,
});
}}
>
{t('player.play', { postProcess: 'titleCase' })}
{t(
playType === Play.NOW
? 'player.play'
: playType === Play.NEXT
? 'player.addNext'
: 'player.addLast',
{ postProcess: 'titleCase' },
)}
</Button>
<Group spacing="sm">
<Button
+48 -21
View File
@@ -4,6 +4,7 @@ import {
ReactNode,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
@@ -114,9 +115,11 @@ export const SwiperGridCarousel = ({
isLoading,
uniqueId,
}: SwiperGridCarouselProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const swiperRef = useRef<SwiperCore | any>(null);
const playButtonBehavior = usePlayButtonBehavior();
const handlePlayQueueAdd = usePlayQueueAdd();
const [slideCount, setSlideCount] = useState(4);
useEffect(() => {
swiperRef.current?.slideTo(0, 0);
@@ -191,23 +194,24 @@ export const SwiperGridCarousel = ({
const handleNext = useCallback(() => {
const activeIndex = swiperRef?.current?.activeIndex || 0;
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4));
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || slideCount));
swiperRef?.current?.slideTo(activeIndex + slidesPerView);
}, [swiperProps?.slidesPerView]);
}, [slideCount, swiperProps?.slidesPerView]);
const handlePrev = useCallback(() => {
const activeIndex = swiperRef?.current?.activeIndex || 0;
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4));
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || slideCount));
swiperRef?.current?.slideTo(activeIndex - slidesPerView);
}, [swiperProps?.slidesPerView]);
}, [slideCount, swiperProps?.slidesPerView]);
const handleOnSlideChange = useCallback((e: SwiperCore) => {
const { slides, isEnd, isBeginning, params } = e;
if (isEnd || isBeginning) return;
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({
hasNextPage: (params?.slidesPerView || 4) < slides.length,
hasPreviousPage: (params?.slidesPerView || 4) < slides.length,
hasNextPage: slideCount < slides.length,
hasPreviousPage: slideCount < slides.length,
});
}, []);
@@ -215,42 +219,67 @@ export const SwiperGridCarousel = ({
const { slides, isEnd, isBeginning, params } = e;
if (isEnd || isBeginning) return;
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({
hasNextPage: (params.slidesPerView || 4) < slides.length,
hasPreviousPage: (params.slidesPerView || 4) < slides.length,
hasNextPage: slideCount < slides.length,
hasPreviousPage: slideCount < slides.length,
});
}, []);
const handleOnReachEnd = useCallback((e: SwiperCore) => {
const { slides, params } = e;
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({
hasNextPage: false,
hasPreviousPage: (params.slidesPerView || 4) < slides.length,
hasPreviousPage: slideCount < slides.length,
});
}, []);
const handleOnReachBeginning = useCallback((e: SwiperCore) => {
const { slides, params } = e;
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({
hasNextPage: (params.slidesPerView || 4) < slides.length,
hasNextPage: slideCount < slides.length,
hasPreviousPage: false,
});
}, []);
const handleOnResize = useCallback((e: SwiperCore) => {
if (!e) return;
const { width } = e;
const slidesPerView = getSlidesPerView(width);
if (!e.params) return;
e.params.slidesPerView = slidesPerView;
}, []);
useLayoutEffect(() => {
const handleResize = () => {
// Use the container div ref and not swiper width, as this value is more accurate
const width = containerRef.current?.clientWidth;
const { activeIndex, params, slides } =
(swiperRef.current as SwiperCore | undefined) ?? {};
const throttledOnResize = throttle(handleOnResize, 200);
if (width) {
const slidesPerView = getSlidesPerView(width);
setSlideCount(slidesPerView);
}
if (activeIndex !== undefined && slides && params?.slidesPerView) {
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({
hasNextPage: activeIndex + slideCount < slides.length,
hasPreviousPage: activeIndex > 0,
});
}
};
handleResize();
const throttledResize = throttle(handleResize, 200);
window.addEventListener('resize', throttledResize);
return () => {
window.removeEventListener('resize', throttledResize);
};
}, []);
return (
<CarouselContainer
ref={containerRef}
className="grid-carousel"
spacing="md"
>
@@ -266,16 +295,14 @@ export const SwiperGridCarousel = ({
ref={swiperRef}
resizeObserver
modules={[Virtual]}
slidesPerView={4}
slidesPerView={slideCount}
spaceBetween={20}
style={{ height: '100%', width: '100%' }}
onBeforeInit={(swiper) => {
swiperRef.current = swiper;
}}
onBeforeResize={handleOnResize}
onReachBeginning={handleOnReachBeginning}
onReachEnd={handleOnReachEnd}
onResize={throttledOnResize}
onSlideChange={handleOnSlideChange}
onZoomChange={handleOnZoomChange}
{...swiperProps}
+1
View File
@@ -27,6 +27,7 @@ export * from './select';
export * from './skeleton';
export * from './slider';
export * from './spinner';
export * from './spoiler';
export * from './switch';
export * from './tabs';
export * from './text';
@@ -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);
+3 -1
View File
@@ -9,7 +9,9 @@ const StyledPagination = styled(MantinePagination)<PaginationProps>`
color: var(--btn-default-fg);
background-color: var(--btn-default-bg);
border: none;
transition: background 0.2s ease-in-out, color 0.2s ease-in-out;
transition:
background 0.2s ease-in-out,
color 0.2s ease-in-out;
&[data-active] {
color: var(--btn-primary-fg);
@@ -54,8 +54,10 @@ interface QueryBuilderProps {
boolean: { label: string; value: string }[];
date: { label: string; value: string }[];
number: { label: string; value: string }[];
playlist: { label: string; value: string }[];
string: { label: string; value: string }[];
};
playlists?: { label: string; value: string }[];
uniqueId: string;
}
@@ -73,6 +75,7 @@ export const QueryBuilder = ({
onChangeValue,
onClearFilters,
onResetFilters,
playlists,
groupIndex,
uniqueId,
filters,
@@ -180,6 +183,7 @@ export const QueryBuilder = ({
level={level}
noRemove={data?.rules?.length === 1}
operators={operators}
selectData={playlists}
onChangeField={onChangeField}
onChangeOperator={onChangeOperator}
onChangeValue={onChangeValue}
@@ -204,6 +208,7 @@ export const QueryBuilder = ({
groupIndex={[...(groupIndex || []), index]}
level={level + 1}
operators={operators}
playlists={playlists}
uniqueId={group.uniqueId}
onAddRule={onAddRule}
onAddRuleGroup={onAddRuleGroup}

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