mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-13 20:26:35 +01:00
Compare commits
694 Commits
openapi-ca
...
renovate/b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00c0234f37 | ||
|
|
ec04313317 | ||
|
|
d87e9f9622 | ||
|
|
10de1ce8fe | ||
|
|
5d5ae271a5 | ||
|
|
d707a9dba1 | ||
|
|
d359d2f7a8 | ||
|
|
ba268cc3fb | ||
|
|
dcba6c3659 | ||
|
|
57821e4cde | ||
|
|
b7d0301099 | ||
|
|
fa65a392b0 | ||
|
|
ec990be12a | ||
|
|
0f6bab03eb | ||
|
|
d20c775daf | ||
|
|
622947e374 | ||
|
|
6e22075a63 | ||
|
|
d68d0fa962 | ||
|
|
00b08c0b32 | ||
|
|
0183127d2a | ||
|
|
d9ced0d639 | ||
|
|
f5f75ed2e1 | ||
|
|
df6f706c2f | ||
|
|
3aed429120 | ||
|
|
f9012b6411 | ||
|
|
6293e7a3c9 | ||
|
|
9404fa2b27 | ||
|
|
19b756a507 | ||
|
|
4e94c3e28b | ||
|
|
127d924c5b | ||
|
|
8150a51238 | ||
|
|
fd80a9d916 | ||
|
|
e75f7f1b28 | ||
|
|
f7bfad8673 | ||
|
|
3690d0bf86 | ||
|
|
4e257364b6 | ||
|
|
58e7ff7f9d | ||
|
|
105492ac28 | ||
|
|
bb12b122c3 | ||
|
|
6d15f693b5 | ||
|
|
e12fbe08a4 | ||
|
|
e9af1588f2 | ||
|
|
b733857da2 | ||
|
|
44a5c6b3dd | ||
|
|
035d8f06cc | ||
|
|
c59d3bb21a | ||
|
|
9962fbbe2e | ||
|
|
070e2b2d0c | ||
|
|
8b94722110 | ||
|
|
755ef1f942 | ||
|
|
a1f3da1819 | ||
|
|
d19449e6a5 | ||
|
|
fc866a64e0 | ||
|
|
48299ff694 | ||
|
|
a35787b82b | ||
|
|
a9117bcda4 | ||
|
|
9f5f18d2db | ||
|
|
8044156df5 | ||
|
|
f9a7cd7457 | ||
|
|
1dd8541ed5 | ||
|
|
d86a4d6815 | ||
|
|
857e730168 | ||
|
|
461662f1ff | ||
|
|
a183fce142 | ||
|
|
b1e2419c65 | ||
|
|
3c93465940 | ||
|
|
64c8c83fae | ||
|
|
1691940c3f | ||
|
|
c3707e3b87 | ||
|
|
d4dda995e1 | ||
|
|
5ed9fb11ac | ||
|
|
54d2a0f3bf | ||
|
|
4bb1e2543f | ||
|
|
3bb9235777 | ||
|
|
50768923fb | ||
|
|
44957300ab | ||
|
|
cec9b51b4f | ||
|
|
18e8a5375b | ||
|
|
b31520d603 | ||
|
|
ab8e3422dc | ||
|
|
2df0b9d821 | ||
|
|
0d6484b7ec | ||
|
|
bd70e0ca34 | ||
|
|
f806ae4018 | ||
|
|
8a1ad14faf | ||
|
|
e71bb7e904 | ||
|
|
96269ff177 | ||
|
|
365729d75f | ||
|
|
4f1ad3fee0 | ||
|
|
a2eff41d66 | ||
|
|
5aa093d299 | ||
|
|
1d44899606 | ||
|
|
48b0029180 | ||
|
|
7113a8c57c | ||
|
|
946ff12185 | ||
|
|
1e64e42842 | ||
|
|
d4a46bc629 | ||
|
|
5a1eec690f | ||
|
|
f9bad54b40 | ||
|
|
d8c55a29bb | ||
|
|
ff4d384cd6 | ||
|
|
8510cfe77d | ||
|
|
eb6c0e0935 | ||
|
|
6a85961816 | ||
|
|
8ba9319f27 | ||
|
|
b5885fe234 | ||
|
|
ba3497b9dd | ||
|
|
0fd80868f8 | ||
|
|
9239b12118 | ||
|
|
8a01f04de5 | ||
|
|
1864c17412 | ||
|
|
ac6edd40c8 | ||
|
|
03523be555 | ||
|
|
fa8073d9ff | ||
|
|
f9c7b18fdd | ||
|
|
1ac5f71bf3 | ||
|
|
5bad7b8ae3 | ||
|
|
fb33b725e0 | ||
|
|
ce3fa80a28 | ||
|
|
ec9c94bd7a | ||
|
|
bb265cd403 | ||
|
|
5b4882c102 | ||
|
|
22644075e7 | ||
|
|
6fc406f2c5 | ||
|
|
d8bbb4dfe8 | ||
|
|
046023b9dd | ||
|
|
193a15ea45 | ||
|
|
45700f6f7d | ||
|
|
99f2129d91 | ||
|
|
29d11f6ecb | ||
|
|
22f0507258 | ||
|
|
c300651d0d | ||
|
|
eacdc83fda | ||
|
|
e3b5cf4996 | ||
|
|
553f38a237 | ||
|
|
68f26e5a34 | ||
|
|
78e0a2b5c1 | ||
|
|
24a0df9a39 | ||
|
|
b28a5794ec | ||
|
|
179db631f7 | ||
|
|
e44821e8f6 | ||
|
|
740e9f8749 | ||
|
|
0bf7653e36 | ||
|
|
b846958f2c | ||
|
|
3c2833e3e8 | ||
|
|
3c9b71e124 | ||
|
|
8cecf53057 | ||
|
|
c008f28d31 | ||
|
|
142ba42883 | ||
|
|
cf9b8161ef | ||
|
|
83c9ab0079 | ||
|
|
f5e9c1de45 | ||
|
|
76c17856ba | ||
|
|
4b59d8f117 | ||
|
|
31720cef05 | ||
|
|
34e2f91d50 | ||
|
|
42a0892022 | ||
|
|
95562f9fee | ||
|
|
643116e1d0 | ||
|
|
9bf04df5d9 | ||
|
|
8482b3cfb9 | ||
|
|
7f3e27c007 | ||
|
|
3225023c1f | ||
|
|
cc7bfff412 | ||
|
|
70a48e1da4 | ||
|
|
80df5dc984 | ||
|
|
982d7432f9 | ||
|
|
97a1feb16d | ||
|
|
894768c4f9 | ||
|
|
134fe92f42 | ||
|
|
5d2a529fb3 | ||
|
|
94b3d41d7d | ||
|
|
a2dcaa9521 | ||
|
|
e4d6b8e1bd | ||
|
|
7256ac81c0 | ||
|
|
6e81226054 | ||
|
|
12af9f9a57 | ||
|
|
83ee080200 | ||
|
|
da4d06c5ab | ||
|
|
87c8349c6b | ||
|
|
c5726559fd | ||
|
|
9e489cd41f | ||
|
|
9b00854e68 | ||
|
|
397147d035 | ||
|
|
f788e8b741 | ||
|
|
99ad70fbc8 | ||
|
|
7c57b62ece | ||
|
|
c7ac591548 | ||
|
|
736a01f447 | ||
|
|
cef81ae2ed | ||
|
|
376769c732 | ||
|
|
6fdfc6a61b | ||
|
|
b6e4b3a4f5 | ||
|
|
d5d4309417 | ||
|
|
5b3537b3d7 | ||
|
|
2134ea3f7f | ||
|
|
42e8a780ca | ||
|
|
7825fa4e43 | ||
|
|
d5f4c624e3 | ||
|
|
c5ee639016 | ||
|
|
e4f01973b7 | ||
|
|
5f30a4538b | ||
|
|
a536ef88c4 | ||
|
|
a0834973ed | ||
|
|
acaeba11f3 | ||
|
|
a3960b30c0 | ||
|
|
a6da575785 | ||
|
|
ea206f43a2 | ||
|
|
5cfa466d8b | ||
|
|
921a364bb0 | ||
|
|
ad9ebe5baa | ||
|
|
6ea77f484d | ||
|
|
e5bbb1ea0c | ||
|
|
3fb1d94038 | ||
|
|
e807575dc7 | ||
|
|
ef83000b30 | ||
|
|
6a20f948f4 | ||
|
|
bd6acb1686 | ||
|
|
f33c039d1b | ||
|
|
40cadfca44 | ||
|
|
0ba41754d8 | ||
|
|
6e154de954 | ||
|
|
4fe3abdc0e | ||
|
|
1219c5ec3b | ||
|
|
2147f57df5 | ||
|
|
9c09e7113e | ||
|
|
5fa865f9e5 | ||
|
|
bcc748e664 | ||
|
|
6b443bb2ec | ||
|
|
fec78c8448 | ||
|
|
c22933260b | ||
|
|
8fc6f07d5a | ||
|
|
386c4cb723 | ||
|
|
965b602c68 | ||
|
|
8142bbd50e | ||
|
|
418beafebb | ||
|
|
434ebc8b11 | ||
|
|
300036c859 | ||
|
|
24ec04d89f | ||
|
|
b82a2ced75 | ||
|
|
568e26c534 | ||
|
|
f52005768a | ||
|
|
f6211a03dd | ||
|
|
995d56d5ff | ||
|
|
63a7c71e77 | ||
|
|
dc4d3639e0 | ||
|
|
d88dd1dd63 | ||
|
|
a2fba38954 | ||
|
|
54856dc026 | ||
|
|
dbd58dd666 | ||
|
|
dbc42bb8e2 | ||
|
|
f6a5b27efc | ||
|
|
ebc15d3e27 | ||
|
|
c0c4740215 | ||
|
|
98bbc26c5e | ||
|
|
4b48cad6f7 | ||
|
|
d65960fe5d | ||
|
|
27d54c5b1c | ||
|
|
31adb5dcd1 | ||
|
|
cf03e3118a | ||
|
|
3997e016fa | ||
|
|
4eead536a9 | ||
|
|
6c8a5dc104 | ||
|
|
8795ed4ebf | ||
|
|
7b7800435a | ||
|
|
136ec00f3e | ||
|
|
8a9d9dd977 | ||
|
|
37983c943a | ||
|
|
d218303b93 | ||
|
|
4952e65a03 | ||
|
|
b825829191 | ||
|
|
ff4c9263e4 | ||
|
|
946c6b9981 | ||
|
|
6880a2ce3e | ||
|
|
b308baaf41 | ||
|
|
8e29e5e419 | ||
|
|
a7b8a68b8d | ||
|
|
99dcda6197 | ||
|
|
bcd5c33ecd | ||
|
|
32270576c2 | ||
|
|
e4eba084dd | ||
|
|
352b6c91f8 | ||
|
|
a8a029de73 | ||
|
|
7ab1c6bb15 | ||
|
|
119b2e3d2f | ||
|
|
08f9ec5d37 | ||
|
|
d6e2fcb233 | ||
|
|
ba722b4517 | ||
|
|
4f08c7a097 | ||
|
|
3b29375179 | ||
|
|
eb3f8b93d0 | ||
|
|
ebb6949ea7 | ||
|
|
1d8bdcc411 | ||
|
|
e6e7f2a692 | ||
|
|
5882006ee7 | ||
|
|
ea1c1d0468 | ||
|
|
077fa89717 | ||
|
|
382db1da0d | ||
|
|
0ebf6a6db6 | ||
|
|
d2334a35c1 | ||
|
|
99e4e1fbd7 | ||
|
|
bc05ecd543 | ||
|
|
268f23f39a | ||
|
|
744c5539d8 | ||
|
|
f5b2e0b8f9 | ||
|
|
f1b4f805e5 | ||
|
|
59c360aea7 | ||
|
|
c843c71003 | ||
|
|
a7c95ab009 | ||
|
|
8824f07e1b | ||
|
|
7cd79c3121 | ||
|
|
b444d2c66a | ||
|
|
b83378d656 | ||
|
|
5807bf1d8f | ||
|
|
3da726463d | ||
|
|
8c0898738d | ||
|
|
e378f63b70 | ||
|
|
44f7d2b854 | ||
|
|
e4500303bb | ||
|
|
2f18e6e7f6 | ||
|
|
ca0b2aa7a6 | ||
|
|
f2ed024296 | ||
|
|
b6f4ffd251 | ||
|
|
8715cb5b9e | ||
|
|
d87fe973f3 | ||
|
|
f680495ca3 | ||
|
|
d2f733f9a4 | ||
|
|
d55f082579 | ||
|
|
bc316b3dc8 | ||
|
|
116a036d56 | ||
|
|
8ee4f951fe | ||
|
|
457c53da6f | ||
|
|
46ffe0af9c | ||
|
|
e6d73ae367 | ||
|
|
826e21ecc8 | ||
|
|
885b45838c | ||
|
|
bd6bf6ee3c | ||
|
|
2068be1221 | ||
|
|
bdfb6edfa3 | ||
|
|
25e8c6d591 | ||
|
|
c11c33e1a8 | ||
|
|
e8232d31ab | ||
|
|
2d0d497961 | ||
|
|
4bd9dbe910 | ||
|
|
b456afe00e | ||
|
|
9aa69eded9 | ||
|
|
b5f5b02787 | ||
|
|
01b3c6f902 | ||
|
|
61ff36d761 | ||
|
|
66c11231b2 | ||
|
|
34c1e45bc2 | ||
|
|
ea0641b659 | ||
|
|
6ce5f9dfd5 | ||
|
|
5541653f73 | ||
|
|
ae5420d4ae | ||
|
|
0f1a6fe4c2 | ||
|
|
ccf2d15d5d | ||
|
|
56a469d8c3 | ||
|
|
561e78efb4 | ||
|
|
ff0a64ecb9 | ||
|
|
2dfebb51be | ||
|
|
716f4c8198 | ||
|
|
9da046abc1 | ||
|
|
01eb56f047 | ||
|
|
94dcaf2ea2 | ||
|
|
37b50fe13c | ||
|
|
6829794aa0 | ||
|
|
3a090a5716 | ||
|
|
f96c399e62 | ||
|
|
06a6c6e16b | ||
|
|
daf88a5ca2 | ||
|
|
0f75518287 | ||
|
|
de32e2eb6f | ||
|
|
84962cbc94 | ||
|
|
ba356638e8 | ||
|
|
3439d3c017 | ||
|
|
837c7d4ed3 | ||
|
|
4ce03ffa21 | ||
|
|
50cabcd99d | ||
|
|
b346370dfc | ||
|
|
fc6419685c | ||
|
|
04ffbe5e9a | ||
|
|
d0809ce58b | ||
|
|
9730aaac57 | ||
|
|
340bcafd3d | ||
|
|
f5c9a4a476 | ||
|
|
a56aa2dd53 | ||
|
|
106f33227a | ||
|
|
1311f66f72 | ||
|
|
217068eeb7 | ||
|
|
44d55f7fa3 | ||
|
|
29582ed461 | ||
|
|
ca6d499680 | ||
|
|
3b69859867 | ||
|
|
c1c7de6a81 | ||
|
|
7bcbe20641 | ||
|
|
2edf23bb40 | ||
|
|
074aa7e639 | ||
|
|
a37e83d448 | ||
|
|
d8543351e2 | ||
|
|
dce91cf8c8 | ||
|
|
48e456903e | ||
|
|
2757c18312 | ||
|
|
a1117a1fbd | ||
|
|
f685a65241 | ||
|
|
e73ebc9741 | ||
|
|
18a1cd388a | ||
|
|
1c2f08bc17 | ||
|
|
6fff4a7bfa | ||
|
|
5aad260767 | ||
|
|
8b5914001d | ||
|
|
5eaaad660d | ||
|
|
de36952f53 | ||
|
|
edec464306 | ||
|
|
6b8400cc3d | ||
|
|
a0bf5199ba | ||
|
|
01264c10a6 | ||
|
|
558b31f386 | ||
|
|
5dcec831f3 | ||
|
|
737abe6f3a | ||
|
|
edc6caf255 | ||
|
|
0a99a78ddc | ||
|
|
71594b4a9a | ||
|
|
5656df4339 | ||
|
|
bb6c3b4eec | ||
|
|
fa4d51c5e6 | ||
|
|
2420ece5fe | ||
|
|
21bb702fd3 | ||
|
|
4a494271dd | ||
|
|
1f6768178a | ||
|
|
00dd84035e | ||
|
|
f5d966fcc3 | ||
|
|
98d7c8d59f | ||
|
|
268d88a5fb | ||
|
|
8ddc35a1ce | ||
|
|
fead4acae1 | ||
|
|
ccd042750d | ||
|
|
46ad25f47d | ||
|
|
0c46004cd9 | ||
|
|
e4619556ba | ||
|
|
a0346fe5b7 | ||
|
|
9f2dc178f5 | ||
|
|
aedd2b04a2 | ||
|
|
4c751e0a86 | ||
|
|
32d8086121 | ||
|
|
7c200899d7 | ||
|
|
613d72fa26 | ||
|
|
909e2142d6 | ||
|
|
8083ab78b5 | ||
|
|
cbc0138507 | ||
|
|
1dacb69d80 | ||
|
|
77ff451e60 | ||
|
|
4f695bc58a | ||
|
|
d1d4fe2e33 | ||
|
|
98b561d62c | ||
|
|
d6a8fa1485 | ||
|
|
fc0d268fe5 | ||
|
|
042385599f | ||
|
|
7b10888c95 | ||
|
|
09a729effe | ||
|
|
2789532aa8 | ||
|
|
694db80d4c | ||
|
|
3920ed4b92 | ||
|
|
a650148dfd | ||
|
|
0a1dd56af6 | ||
|
|
0413a8b6d2 | ||
|
|
841e4dabb5 | ||
|
|
815a153b94 | ||
|
|
0c274af72c | ||
|
|
6d34f605a3 | ||
|
|
dad6f650bc | ||
|
|
5045c2e448 | ||
|
|
ec4744709d | ||
|
|
91dd146c0f | ||
|
|
4344d75694 | ||
|
|
b97f5b809d | ||
|
|
b9e5cce383 | ||
|
|
565de2d377 | ||
|
|
6ed633ce42 | ||
|
|
cf7150cd9d | ||
|
|
b8d2f1f911 | ||
|
|
17e8759a52 | ||
|
|
360b6bcce1 | ||
|
|
874fd9ac0a | ||
|
|
92f3c8cf15 | ||
|
|
441a41b2eb | ||
|
|
bc6c3b1013 | ||
|
|
a676391af2 | ||
|
|
411a6cced1 | ||
|
|
62474af0c0 | ||
|
|
738d4753f2 | ||
|
|
152d4451ec | ||
|
|
fd71adfba9 | ||
|
|
327ace1d30 | ||
|
|
95a301dc43 | ||
|
|
7151c4ca21 | ||
|
|
05997987ad | ||
|
|
bab4e620e3 | ||
|
|
977d1c38b2 | ||
|
|
342846e4fc | ||
|
|
99440f8432 | ||
|
|
2086ac7dd2 | ||
|
|
4a1012fd22 | ||
|
|
89427af41c | ||
|
|
5996c4afce | ||
|
|
dfa78590c2 | ||
|
|
912a963a2b | ||
|
|
f260585917 | ||
|
|
22d8a00716 | ||
|
|
c350fd0f40 | ||
|
|
139d23ddc2 | ||
|
|
cc2ccd1bf3 | ||
|
|
1491494bcb | ||
|
|
0b77f97048 | ||
|
|
d3d4d37e82 | ||
|
|
1c1447362e | ||
|
|
d1c97b8e1d | ||
|
|
fb32709259 | ||
|
|
c9b7c5bb56 | ||
|
|
42ff253339 | ||
|
|
c4ffc357a3 | ||
|
|
afcaec0a89 | ||
|
|
09edca8b7a | ||
|
|
f9fd34b11e | ||
|
|
aa666565d1 | ||
|
|
f83e8ee806 | ||
|
|
84ebed1eb7 | ||
|
|
1d7c6af520 | ||
|
|
226da3b371 | ||
|
|
8d052a6cb1 | ||
|
|
ef0409d06c | ||
|
|
b9abf590c5 | ||
|
|
6a9bb060eb | ||
|
|
6a7600a8c6 | ||
|
|
e12131108e | ||
|
|
89b5b99873 | ||
|
|
50dcec1ff5 | ||
|
|
d089537bca | ||
|
|
e70355fbe1 | ||
|
|
140c459ac3 | ||
|
|
098e8c6fed | ||
|
|
1ba8e2c93c | ||
|
|
9e480f6efb | ||
|
|
0892847c2f | ||
|
|
c464ba83f2 | ||
|
|
62e51fd00a | ||
|
|
cf9051c277 | ||
|
|
d270957c82 | ||
|
|
0ee872999d | ||
|
|
c4f4dcc181 | ||
|
|
22ee5113d0 | ||
|
|
e233eee07b | ||
|
|
185849b68a | ||
|
|
e62b6f8339 | ||
|
|
9931537d87 | ||
|
|
82c4df5cde | ||
|
|
103f556c8d | ||
|
|
582a1d9866 | ||
|
|
244757c92c | ||
|
|
0ff869dfcd | ||
|
|
a1e0e4fd9d | ||
|
|
4138214ac3 | ||
|
|
9039077286 | ||
|
|
8a1129bbde | ||
|
|
706a8d2850 | ||
|
|
ba4dbcf5a1 | ||
|
|
bfae788a44 | ||
|
|
e02a2ae48f | ||
|
|
18dc32d735 | ||
|
|
85ff708597 | ||
|
|
23b48a0d0f | ||
|
|
d1055b0b36 | ||
|
|
3c77758b32 | ||
|
|
b564a43d9c | ||
|
|
f08657ab27 | ||
|
|
e5fb071708 | ||
|
|
72b4faa00b | ||
|
|
d28ee6d714 | ||
|
|
2f62a8bb39 | ||
|
|
75f1276119 | ||
|
|
6c8395ff87 | ||
|
|
82b2e7773f | ||
|
|
336958318d | ||
|
|
8a8f7956ef | ||
|
|
c728e97bda | ||
|
|
5c76dd26bc | ||
|
|
7af5ee1812 | ||
|
|
7f0e71578d | ||
|
|
45e881c93e | ||
|
|
b429306f05 | ||
|
|
88acd51ee2 | ||
|
|
3c802a7505 | ||
|
|
928a8458dd | ||
|
|
43797fee42 | ||
|
|
b9158c467a | ||
|
|
252ab45473 | ||
|
|
afc083e9fa | ||
|
|
f867ce3842 | ||
|
|
2a464c316d | ||
|
|
b9cf26db2f | ||
|
|
580585846b | ||
|
|
1af1c72e81 | ||
|
|
5557004375 | ||
|
|
5d50ff5f81 | ||
|
|
8e2ed40a8b | ||
|
|
8461268837 | ||
|
|
8a0b963d2c | ||
|
|
24acd94015 | ||
|
|
c30654c33c | ||
|
|
7bafd13564 | ||
|
|
0e73a56a45 | ||
|
|
f9fec33048 | ||
|
|
934a9c9e32 | ||
|
|
25115e95aa | ||
|
|
146681f0ba | ||
|
|
7f1a0ff6fc | ||
|
|
8d8d38600e | ||
|
|
172b054f48 | ||
|
|
2168847a45 | ||
|
|
e5a2acd6dd | ||
|
|
ddc613cd72 | ||
|
|
fa99b1d81c | ||
|
|
93902fc610 | ||
|
|
84f66dd54e | ||
|
|
d446fde009 | ||
|
|
ee676fd568 | ||
|
|
2cca942ce6 | ||
|
|
59d574edb7 | ||
|
|
c170b18155 | ||
|
|
45a49a4fb4 | ||
|
|
12a2f7c1a5 | ||
|
|
d0950c8f09 | ||
|
|
ebf8220f1d | ||
|
|
c4e8180b3c | ||
|
|
771b0a7eab | ||
|
|
4db0ab0f40 | ||
|
|
dd480f96cd | ||
|
|
6b6d54a07c | ||
|
|
428beda1c7 | ||
|
|
baa8d40940 | ||
|
|
c8bdee26b7 | ||
|
|
ef73ed6ef7 | ||
|
|
acb9da6f93 | ||
|
|
d1f242bc09 | ||
|
|
113bd9af05 | ||
|
|
d757e12e1a | ||
|
|
37d6101f02 | ||
|
|
d70e0fe9cf | ||
|
|
053cc9406d | ||
|
|
0f85120c5e | ||
|
|
25aef7fabf | ||
|
|
492ea66841 | ||
|
|
8b2a8b94b6 | ||
|
|
f24e80701c | ||
|
|
0b3d6676d1 | ||
|
|
c3a8734adf | ||
|
|
8fd59d6f33 | ||
|
|
da3bff3edf | ||
|
|
45cb5a0008 | ||
|
|
ea097fb1a3 | ||
|
|
a25b48b151 | ||
|
|
873f1d9e83 | ||
|
|
294439bf74 | ||
|
|
6e74be0d46 | ||
|
|
deb81eae10 | ||
|
|
70dcf3f7b3 | ||
|
|
ebcfed83c4 | ||
|
|
5d46278584 | ||
|
|
4f020a947a | ||
|
|
3460d1de3c | ||
|
|
7d2e4cd817 | ||
|
|
8cd6ef37c4 | ||
|
|
e4daaf0d83 | ||
|
|
69c98af9f9 | ||
|
|
7425a493ee | ||
|
|
691c194152 | ||
|
|
2f8896c375 | ||
|
|
d95bab41a1 | ||
|
|
820e208bdc | ||
|
|
73fd6721f6 | ||
|
|
41c1c5f7bf | ||
|
|
6963d95880 | ||
|
|
5182aec13f | ||
|
|
217ea488df | ||
|
|
43a055d7ea | ||
|
|
79061f4635 | ||
|
|
cd9154f110 | ||
|
|
2b6febc8da | ||
|
|
b13039f377 | ||
|
|
2904083053 | ||
|
|
47af1c4576 | ||
|
|
80365f277a | ||
|
|
c46dff16cc | ||
|
|
9ec657cf6e |
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "9.0.11",
|
"version": "10.0.7",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "Development Jellyfin Server",
|
"name": "Development Jellyfin Server",
|
||||||
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
|
"image": "mcr.microsoft.com/devcontainers/dotnet:10.0-noble",
|
||||||
"service": "app",
|
"service": "app",
|
||||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
|
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
|
||||||
"postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
|
"postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
|
||||||
// reads the extensions list and installs them
|
// The previous way of installing extensions via the vs command dont work on selfhosted devcontainers
|
||||||
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"ms-dotnettools.csharp",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"github.vscode-github-actions",
|
||||||
|
"ms-dotnettools.vscode-dotnet-runtime",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"alexcvzz.vscode-sqlite",
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
"eamodio.gitlens",
|
||||||
|
"redhat.vscode-xml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/dotnet:2": {
|
"ghcr.io/devcontainers/features/dotnet:2": {
|
||||||
"version": "none",
|
"version": "none",
|
||||||
"dotnetRuntimeVersions": "9.0",
|
"dotnetRuntimeVersions": "10.0",
|
||||||
"aspNetCoreRuntimeVersions": "9.0"
|
"aspNetCoreRuntimeVersions": "10.0"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
|
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
|
||||||
"preserve_apt_list": false,
|
"preserve_apt_list": false,
|
||||||
|
|||||||
@@ -379,6 +379,9 @@ dotnet_diagnostic.CA1720.severity = suggestion
|
|||||||
# disable warning CA1724: Type names should not match namespaces
|
# disable warning CA1724: Type names should not match namespaces
|
||||||
dotnet_diagnostic.CA1724.severity = suggestion
|
dotnet_diagnostic.CA1724.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1873: Avoid potentially expensive logging
|
||||||
|
dotnet_diagnostic.CA1873.severity = suggestion
|
||||||
|
|
||||||
# disable warning CA1805: Do not initialize unnecessarily
|
# disable warning CA1805: Do not initialize unnecessarily
|
||||||
dotnet_diagnostic.CA1805.severity = suggestion
|
dotnet_diagnostic.CA1805.severity = suggestion
|
||||||
|
|
||||||
@@ -400,6 +403,10 @@ dotnet_diagnostic.CA1861.severity = suggestion
|
|||||||
# disable warning CA2000: Dispose objects before losing scope
|
# disable warning CA2000: Dispose objects before losing scope
|
||||||
dotnet_diagnostic.CA2000.severity = suggestion
|
dotnet_diagnostic.CA2000.severity = suggestion
|
||||||
|
|
||||||
|
# TODO: Reevaluate when false positives are fixed: https://github.com/dotnet/roslyn-analyzers/issues/7699
|
||||||
|
# disable warning CA2025: Do not pass 'IDisposable' instances into unawaited tasks
|
||||||
|
dotnet_diagnostic.CA2025.severity = suggestion
|
||||||
|
|
||||||
# disable warning CA2253: Named placeholders should not be numeric values
|
# disable warning CA2253: Named placeholders should not be numeric values
|
||||||
dotnet_diagnostic.CA2253.severity = suggestion
|
dotnet_diagnostic.CA2253.severity = suggestion
|
||||||
|
|
||||||
|
|||||||
17
.github/ISSUE_TEMPLATE/issue report.yml
vendored
17
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -87,10 +87,9 @@ body:
|
|||||||
label: Jellyfin Server version
|
label: Jellyfin Server version
|
||||||
description: What version of Jellyfin are you using?
|
description: What version of Jellyfin are you using?
|
||||||
options:
|
options:
|
||||||
- 10.11.3
|
- 10.11.8
|
||||||
- 10.11.2
|
- 10.11.7
|
||||||
- 10.11.1
|
- 10.11.6
|
||||||
- 10.11.0
|
|
||||||
- Master
|
- Master
|
||||||
- Unstable
|
- Unstable
|
||||||
- Older*
|
- Older*
|
||||||
@@ -139,13 +138,14 @@ body:
|
|||||||
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
|
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
|
||||||
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
|
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
|
||||||
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
|
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
|
||||||
|
- **CPU Model**: [e.g. AMD Ryzen 5 9600X, Intel Core i7-8565U, etc.]
|
||||||
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
|
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
|
||||||
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
|
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
|
||||||
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
||||||
- **Base URL**: [e.g. none, yes: /example]
|
- **Base URL**: [e.g. none, yes: /example]
|
||||||
- **Networking**: [e.g. Host, Bridge/NAT]
|
- **Networking**: [e.g. Host, Bridge/NAT]
|
||||||
- **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD]
|
- **Jellyfin Data Storage & Filesystem**: [e.g. local SATA SSD - ext4, local HDD - NTFS]
|
||||||
- **Media Storage**: [e.g. Local HDD, SMB Share]
|
- **Media Storage & Filesystem**: [e.g. Local HDD - ext4, SMB Share]
|
||||||
- **External Integrations**: [e.g. Jellystat, Jellyseerr]
|
- **External Integrations**: [e.g. Jellystat, Jellyseerr]
|
||||||
value: |
|
value: |
|
||||||
- OS:
|
- OS:
|
||||||
@@ -156,13 +156,14 @@ body:
|
|||||||
- FFmpeg Version:
|
- FFmpeg Version:
|
||||||
- Playback Method:
|
- Playback Method:
|
||||||
- Hardware Acceleration:
|
- Hardware Acceleration:
|
||||||
|
- CPU Model:
|
||||||
- GPU Model:
|
- GPU Model:
|
||||||
- Plugins:
|
- Plugins:
|
||||||
- Reverse Proxy:
|
- Reverse Proxy:
|
||||||
- Base URL:
|
- Base URL:
|
||||||
- Networking:
|
- Networking:
|
||||||
- Jellyfin Data Storage:
|
- Jellyfin Data Storage & Filesystem:
|
||||||
- Media Storage:
|
- Media Storage & Filesystem:
|
||||||
- External Integrations:
|
- External Integrations:
|
||||||
render: markdown
|
render: markdown
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
19
.github/workflows/ci-codeql-analysis.yml
vendored
19
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -8,6 +8,10 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: '24 2 * * 4'
|
- cron: '24 2 * * 4'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
@@ -20,18 +24,21 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9.0.x'
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
|
|||||||
26
.github/workflows/ci-compat.yml
vendored
26
.github/workflows/ci-compat.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: ABI Compatibility
|
name: ABI Compatibility
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
@@ -11,22 +11,22 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9.0.x'
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
dotnet build Jellyfin.Server -o ./out
|
dotnet build Jellyfin.Server -o ./out
|
||||||
|
|
||||||
- name: Upload Head
|
- name: Upload Head
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: abi-head
|
name: abi-head
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@@ -40,16 +40,16 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9.0.x'
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
- name: Checkout common ancestor
|
- name: Checkout common ancestor
|
||||||
env:
|
env:
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
dotnet build Jellyfin.Server -o ./out
|
dotnet build Jellyfin.Server -o ./out
|
||||||
|
|
||||||
- name: Upload Head
|
- name: Upload Head
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: abi-base
|
name: abi-base
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
||||||
|
|
||||||
name: ABI - Difference
|
name: ABI - Difference
|
||||||
if: ${{ github.event_name == 'pull_request_target' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- abi-head
|
- abi-head
|
||||||
@@ -85,13 +85,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download abi-head
|
- name: Download abi-head
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: abi-head
|
name: abi-head
|
||||||
path: abi-head
|
path: abi-head
|
||||||
|
|
||||||
- name: Download abi-base
|
- name: Download abi-base
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: abi-base
|
name: abi-base
|
||||||
path: abi-base
|
path: abi-base
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
{
|
{
|
||||||
echo 'body<<EOF'
|
echo 'body<<EOF'
|
||||||
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
|
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
|
||||||
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
|
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 || true )"
|
||||||
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
|
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
|
||||||
printf "\n${file}\n${COMPAT_OUTPUT}\n"
|
printf "\n${file}\n${COMPAT_OUTPUT}\n"
|
||||||
fi
|
fi
|
||||||
|
|||||||
271
.github/workflows/ci-openapi.yml
vendored
271
.github/workflows/ci-openapi.yml
vendored
@@ -1,271 +0,0 @@
|
|||||||
name: OpenAPI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
pull_request_target:
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
openapi-head:
|
|
||||||
name: OpenAPI - HEAD
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions: read-all
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
|
||||||
- name: Setup .NET
|
|
||||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
|
||||||
with:
|
|
||||||
dotnet-version: '9.0.x'
|
|
||||||
- name: Generate openapi.json
|
|
||||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
|
||||||
- name: Upload openapi.json
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: openapi-head
|
|
||||||
retention-days: 14
|
|
||||||
if-no-files-found: error
|
|
||||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
|
|
||||||
|
|
||||||
openapi-base:
|
|
||||||
name: OpenAPI - BASE
|
|
||||||
if: ${{ github.base_ref != '' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions: read-all
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Checkout common ancestor
|
|
||||||
env:
|
|
||||||
HEAD_REF: ${{ github.head_ref }}
|
|
||||||
run: |
|
|
||||||
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
|
|
||||||
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
|
|
||||||
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
|
|
||||||
git checkout --progress --force $ANCESTOR_REF
|
|
||||||
- name: Setup .NET
|
|
||||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
|
||||||
with:
|
|
||||||
dotnet-version: '9.0.x'
|
|
||||||
- name: Generate openapi.json
|
|
||||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
|
||||||
- name: Upload openapi.json
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: openapi-base
|
|
||||||
retention-days: 14
|
|
||||||
if-no-files-found: error
|
|
||||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
|
|
||||||
|
|
||||||
openapi-diff:
|
|
||||||
permissions:
|
|
||||||
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
|
||||||
|
|
||||||
name: OpenAPI - Difference
|
|
||||||
if: ${{ github.event_name == 'pull_request_target' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- openapi-head
|
|
||||||
- openapi-base
|
|
||||||
steps:
|
|
||||||
- name: Download openapi-head
|
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
|
||||||
with:
|
|
||||||
name: openapi-head
|
|
||||||
path: openapi-head
|
|
||||||
- name: Download openapi-base
|
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
|
||||||
with:
|
|
||||||
name: openapi-base
|
|
||||||
path: openapi-base
|
|
||||||
- name: Workaround openapi-diff issue
|
|
||||||
run: |
|
|
||||||
sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
|
|
||||||
sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
|
|
||||||
- name: Calculate OpenAPI difference
|
|
||||||
uses: docker://openapitools/openapi-diff
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
|
|
||||||
- id: read-diff
|
|
||||||
name: Read openapi-diff output
|
|
||||||
run: |
|
|
||||||
# Read and fix markdown
|
|
||||||
body=$(cat openapi-changes.md)
|
|
||||||
# Write to workflow summary
|
|
||||||
echo "$body" >> $GITHUB_STEP_SUMMARY
|
|
||||||
# Set ApiChanged var
|
|
||||||
if [ "$body" != '' ]; then
|
|
||||||
echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
# Add header/footer for diff comment
|
|
||||||
echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md
|
|
||||||
echo "<details>" >> openapi-changes-reply.md
|
|
||||||
echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md
|
|
||||||
echo "" >> openapi-changes-reply.md
|
|
||||||
echo "$body" >> openapi-changes-reply.md
|
|
||||||
echo "" >> openapi-changes-reply.md
|
|
||||||
echo "</details>" >> openapi-changes-reply.md
|
|
||||||
- name: Find difference comment
|
|
||||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
|
||||||
id: find-comment
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
direction: last
|
|
||||||
body-includes: openapi-diff-workflow-comment
|
|
||||||
- name: Reply or edit difference comment (changed)
|
|
||||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
|
||||||
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
|
||||||
edit-mode: replace
|
|
||||||
body-path: openapi-changes-reply.md
|
|
||||||
- name: Edit difference comment (unchanged)
|
|
||||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
|
||||||
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
|
||||||
edit-mode: replace
|
|
||||||
body: |
|
|
||||||
<!--openapi-diff-workflow-comment-->
|
|
||||||
|
|
||||||
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
|
||||||
|
|
||||||
publish-unstable:
|
|
||||||
name: OpenAPI - Publish Unstable Spec
|
|
||||||
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- openapi-head
|
|
||||||
steps:
|
|
||||||
- name: Set unstable dated version
|
|
||||||
id: version
|
|
||||||
run: |-
|
|
||||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
|
||||||
- name: Download openapi-head
|
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
|
||||||
with:
|
|
||||||
name: openapi-head
|
|
||||||
path: openapi-head
|
|
||||||
- name: Upload openapi.json (unstable) to repository server
|
|
||||||
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
|
|
||||||
with:
|
|
||||||
host: "${{ secrets.REPO_HOST }}"
|
|
||||||
username: "${{ secrets.REPO_USER }}"
|
|
||||||
key: "${{ secrets.REPO_KEY }}"
|
|
||||||
source: openapi-head/openapi.json
|
|
||||||
strip_components: 1
|
|
||||||
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
|
||||||
- name: Move openapi.json (unstable) into place
|
|
||||||
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
|
|
||||||
with:
|
|
||||||
host: "${{ secrets.REPO_HOST }}"
|
|
||||||
username: "${{ secrets.REPO_USER }}"
|
|
||||||
key: "${{ secrets.REPO_KEY }}"
|
|
||||||
debug: false
|
|
||||||
script_stop: false
|
|
||||||
script: |
|
|
||||||
if ! test -d /run/workflows; then
|
|
||||||
sudo mkdir -p /run/workflows
|
|
||||||
sudo chown ${{ secrets.REPO_USER }} /run/workflows
|
|
||||||
fi
|
|
||||||
(
|
|
||||||
flock -x -w 300 200 || exit 1
|
|
||||||
TGT_DIR="/srv/repository/main/openapi"
|
|
||||||
LAST_SPEC="$( ls -lt ${TGT_DIR}/unstable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
|
|
||||||
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
|
|
||||||
if diff /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/${LAST_SPEC} &>/dev/null; then
|
|
||||||
rm -r /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# Move new spec into place
|
|
||||||
sudo mv /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
|
|
||||||
# Delete previous jellyfin-openapi-unstable_previous.json
|
|
||||||
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
|
||||||
# Move current jellyfin-openapi-unstable.json symlink to jellyfin-openapi-unstable_previous.json
|
|
||||||
sudo mv ${TGT_DIR}/jellyfin-openapi-unstable.json ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
|
||||||
# Create new jellyfin-openapi-unstable.json symlink
|
|
||||||
sudo ln -s unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-unstable.json
|
|
||||||
# Check that the previous openapi unstable spec link is correct
|
|
||||||
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-unstable_previous.json )" != "unstable/${LAST_SPEC}" ]]; then
|
|
||||||
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
|
||||||
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
|
||||||
fi
|
|
||||||
) 200>/run/workflows/openapi-unstable.lock
|
|
||||||
|
|
||||||
publish-stable:
|
|
||||||
name: OpenAPI - Publish Stable Spec
|
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- openapi-head
|
|
||||||
steps:
|
|
||||||
- name: Set version number
|
|
||||||
id: version
|
|
||||||
run: |-
|
|
||||||
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
|
||||||
- name: Download openapi-head
|
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
|
||||||
with:
|
|
||||||
name: openapi-head
|
|
||||||
path: openapi-head
|
|
||||||
- name: Upload openapi.json (stable) to repository server
|
|
||||||
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
|
|
||||||
with:
|
|
||||||
host: "${{ secrets.REPO_HOST }}"
|
|
||||||
username: "${{ secrets.REPO_USER }}"
|
|
||||||
key: "${{ secrets.REPO_KEY }}"
|
|
||||||
source: openapi-head/openapi.json
|
|
||||||
strip_components: 1
|
|
||||||
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
|
||||||
- name: Move openapi.json (stable) into place
|
|
||||||
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
|
|
||||||
with:
|
|
||||||
host: "${{ secrets.REPO_HOST }}"
|
|
||||||
username: "${{ secrets.REPO_USER }}"
|
|
||||||
key: "${{ secrets.REPO_KEY }}"
|
|
||||||
debug: false
|
|
||||||
script_stop: false
|
|
||||||
script: |
|
|
||||||
if ! test -d /run/workflows; then
|
|
||||||
sudo mkdir -p /run/workflows
|
|
||||||
sudo chown ${{ secrets.REPO_USER }} /run/workflows
|
|
||||||
fi
|
|
||||||
(
|
|
||||||
flock -x -w 300 200 || exit 1
|
|
||||||
TGT_DIR="/srv/repository/main/openapi"
|
|
||||||
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
|
|
||||||
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
|
|
||||||
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
|
|
||||||
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# Move new spec into place
|
|
||||||
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
|
|
||||||
# Delete previous jellyfin-openapi-stable_previous.json
|
|
||||||
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
|
||||||
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
|
|
||||||
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
|
||||||
# Create new jellyfin-openapi-stable.json symlink
|
|
||||||
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
|
|
||||||
# Check that the previous openapi stable spec link is correct
|
|
||||||
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
|
|
||||||
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
|
||||||
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
|
||||||
fi
|
|
||||||
) 200>/run/workflows/openapi-stable.lock
|
|
||||||
8
.github/workflows/ci-tests.yml
vendored
8
.github/workflows/ci-tests.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SDK_VERSION: "9.0.x"
|
SDK_VERSION: "10.0.x"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
run-tests:
|
||||||
@@ -20,9 +20,9 @@ jobs:
|
|||||||
|
|
||||||
runs-on: "${{ matrix.os }}"
|
runs-on: "${{ matrix.os }}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ env.SDK_VERSION }}
|
dotnet-version: ${{ env.SDK_VERSION }}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
--verbosity minimal
|
--verbosity minimal
|
||||||
|
|
||||||
- name: Merge code coverage results
|
- name: Merge code coverage results
|
||||||
uses: danielpalme/ReportGenerator-GitHub-Action@dcdfb6e704e87df6b2ed0cf123a6c9f69e364869 # v5.5.0
|
uses: danielpalme/ReportGenerator-GitHub-Action@c31aa4ed4f12f147061186cf2a029f307b5c3636 # v5.5.9
|
||||||
with:
|
with:
|
||||||
reports: "**/coverage.cobertura.xml"
|
reports: "**/coverage.cobertura.xml"
|
||||||
targetdir: "merged/"
|
targetdir: "merged/"
|
||||||
|
|||||||
13
.github/workflows/commands.yml
vendored
13
.github/workflows/commands.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
types:
|
types:
|
||||||
- created
|
- created
|
||||||
- edited
|
- edited
|
||||||
pull_request_target:
|
pull_request:
|
||||||
types:
|
types:
|
||||||
- labeled
|
- labeled
|
||||||
- synchronize
|
- synchronize
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
reactions: '+1'
|
reactions: '+1'
|
||||||
|
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -36,20 +36,23 @@ jobs:
|
|||||||
|
|
||||||
rename:
|
rename:
|
||||||
name: Rename
|
name: Rename
|
||||||
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'
|
if: contains(github.event.comment.body, '@jellyfin-bot rename')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: pull in script
|
- name: pull in script
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
repository: jellyfin/jellyfin-triage-script
|
repository: jellyfin/jellyfin-triage-script
|
||||||
|
|
||||||
- name: install python
|
- name: install python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
|
||||||
- name: install python packages
|
- name: install python packages
|
||||||
run: pip install -r rename/requirements.txt
|
run: pip install -r rename/requirements.txt
|
||||||
|
|
||||||
- name: run rename script
|
- name: run rename script
|
||||||
run: python3 rename.py
|
run: python3 rename.py
|
||||||
working-directory: ./rename
|
working-directory: ./rename
|
||||||
|
|||||||
2
.github/workflows/issue-stale.yml
vendored
2
.github/workflows/issue-stale.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
ascending: true
|
ascending: true
|
||||||
|
|||||||
7
.github/workflows/issue-template-check.yml
vendored
7
.github/workflows/issue-template-check.yml
vendored
@@ -10,16 +10,19 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: pull in script
|
- name: pull in script
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
repository: jellyfin/jellyfin-triage-script
|
repository: jellyfin/jellyfin-triage-script
|
||||||
|
|
||||||
- name: install python
|
- name: install python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
|
||||||
- name: install python packages
|
- name: install python packages
|
||||||
run: pip install -r main-repo-triage/requirements.txt
|
run: pip install -r main-repo-triage/requirements.txt
|
||||||
|
|
||||||
- name: check and comment issue
|
- name: check and comment issue
|
||||||
working-directory: ./main-repo-triage
|
working-directory: ./main-repo-triage
|
||||||
run: python3 single_issue_gha.py
|
run: python3 single_issue_gha.py
|
||||||
|
|||||||
44
.github/workflows/openapi-generate.yml
vendored
Normal file
44
.github/workflows/openapi-generate.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: OpenAPI Generate
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
repository:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
artifact:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
name: Main
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref }}
|
||||||
|
repository: ${{ inputs.repository }}
|
||||||
|
|
||||||
|
- name: Configure .NET
|
||||||
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
|
- name: Create File
|
||||||
|
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter Jellyfin.Server.Integration.Tests.OpenApiSpecTests
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact }}
|
||||||
|
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
|
||||||
|
retention-days: 14
|
||||||
|
if-no-files-found: error
|
||||||
140
.github/workflows/openapi-merge.yml
vendored
Normal file
140
.github/workflows/openapi-merge.yml
vendored
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
name: OpenAPI Publish
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-openapi:
|
||||||
|
name: OpenAPI - Publish Artifact
|
||||||
|
uses: ./.github/workflows/openapi-generate.yml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
with:
|
||||||
|
ref: ${{ github.sha }}
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
artifact: openapi-head
|
||||||
|
|
||||||
|
publish-unstable:
|
||||||
|
name: OpenAPI - Publish Unstable Spec
|
||||||
|
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- publish-openapi
|
||||||
|
steps:
|
||||||
|
- name: Set unstable dated version
|
||||||
|
id: version
|
||||||
|
run: |-
|
||||||
|
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||||
|
- name: Download openapi-head
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: openapi-head
|
||||||
|
path: openapi-head
|
||||||
|
- name: Upload openapi.json (unstable) to repository server
|
||||||
|
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
|
||||||
|
with:
|
||||||
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
|
username: "${{ secrets.REPO_USER }}"
|
||||||
|
key: "${{ secrets.REPO_KEY }}"
|
||||||
|
source: openapi-head/openapi.json
|
||||||
|
strip_components: 1
|
||||||
|
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||||
|
- name: Move openapi.json (unstable) into place
|
||||||
|
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
||||||
|
with:
|
||||||
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
|
username: "${{ secrets.REPO_USER }}"
|
||||||
|
key: "${{ secrets.REPO_KEY }}"
|
||||||
|
debug: false
|
||||||
|
script: |
|
||||||
|
if ! test -d /run/workflows; then
|
||||||
|
sudo mkdir -p /run/workflows
|
||||||
|
sudo chown ${{ secrets.REPO_USER }} /run/workflows
|
||||||
|
fi
|
||||||
|
(
|
||||||
|
flock -x -w 300 200 || exit 1
|
||||||
|
TGT_DIR="/srv/repository/main/openapi"
|
||||||
|
LAST_SPEC="$( ls -lt ${TGT_DIR}/unstable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
|
||||||
|
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
|
||||||
|
if diff /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/${LAST_SPEC} &>/dev/null; then
|
||||||
|
rm -r /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Move new spec into place
|
||||||
|
sudo mv /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
|
||||||
|
# Delete previous jellyfin-openapi-unstable_previous.json
|
||||||
|
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||||
|
# Move current jellyfin-openapi-unstable.json symlink to jellyfin-openapi-unstable_previous.json
|
||||||
|
sudo mv ${TGT_DIR}/jellyfin-openapi-unstable.json ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||||
|
# Create new jellyfin-openapi-unstable.json symlink
|
||||||
|
sudo ln -s unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-unstable.json
|
||||||
|
# Check that the previous openapi unstable spec link is correct
|
||||||
|
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-unstable_previous.json )" != "unstable/${LAST_SPEC}" ]]; then
|
||||||
|
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||||
|
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||||
|
fi
|
||||||
|
) 200>/run/workflows/openapi-unstable.lock
|
||||||
|
|
||||||
|
publish-stable:
|
||||||
|
name: OpenAPI - Publish Stable Spec
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- publish-openapi
|
||||||
|
steps:
|
||||||
|
- name: Set version number
|
||||||
|
id: version
|
||||||
|
run: |-
|
||||||
|
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
- name: Download openapi-head
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: openapi-head
|
||||||
|
path: openapi-head
|
||||||
|
- name: Upload openapi.json (stable) to repository server
|
||||||
|
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
|
||||||
|
with:
|
||||||
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
|
username: "${{ secrets.REPO_USER }}"
|
||||||
|
key: "${{ secrets.REPO_KEY }}"
|
||||||
|
source: openapi-head/openapi.json
|
||||||
|
strip_components: 1
|
||||||
|
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||||
|
- name: Move openapi.json (stable) into place
|
||||||
|
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
||||||
|
with:
|
||||||
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
|
username: "${{ secrets.REPO_USER }}"
|
||||||
|
key: "${{ secrets.REPO_KEY }}"
|
||||||
|
debug: false
|
||||||
|
script: |
|
||||||
|
if ! test -d /run/workflows; then
|
||||||
|
sudo mkdir -p /run/workflows
|
||||||
|
sudo chown ${{ secrets.REPO_USER }} /run/workflows
|
||||||
|
fi
|
||||||
|
(
|
||||||
|
flock -x -w 300 200 || exit 1
|
||||||
|
TGT_DIR="/srv/repository/main/openapi"
|
||||||
|
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
|
||||||
|
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
|
||||||
|
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
|
||||||
|
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Move new spec into place
|
||||||
|
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
|
||||||
|
# Delete previous jellyfin-openapi-stable_previous.json
|
||||||
|
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
|
||||||
|
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
# Create new jellyfin-openapi-stable.json symlink
|
||||||
|
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
|
||||||
|
# Check that the previous openapi stable spec link is correct
|
||||||
|
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
|
||||||
|
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
fi
|
||||||
|
) 200>/run/workflows/openapi-stable.lock
|
||||||
80
.github/workflows/openapi-pull-request.yml
vendored
Normal file
80
.github/workflows/openapi-pull-request.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: OpenAPI Check
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ancestor:
|
||||||
|
name: Common Ancestor
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
base_ref: ${{ steps.ancestor.outputs.base_ref }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Search History
|
||||||
|
id: ancestor
|
||||||
|
run: |
|
||||||
|
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
|
||||||
|
git fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
|
||||||
|
|
||||||
|
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} HEAD)
|
||||||
|
|
||||||
|
echo "ref: ${ANCESTOR_REF}"
|
||||||
|
|
||||||
|
echo "base_ref=${ANCESTOR_REF}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
head:
|
||||||
|
name: Head Artifact
|
||||||
|
uses: ./.github/workflows/openapi-generate.yml
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
artifact: openapi-head
|
||||||
|
|
||||||
|
base:
|
||||||
|
name: Base Artifact
|
||||||
|
uses: ./.github/workflows/openapi-generate.yml
|
||||||
|
needs:
|
||||||
|
- ancestor
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.ancestor.outputs.base_ref }}
|
||||||
|
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||||
|
artifact: openapi-base
|
||||||
|
|
||||||
|
diff:
|
||||||
|
name: Generate Report
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- head
|
||||||
|
- base
|
||||||
|
steps:
|
||||||
|
- name: Download Head
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: openapi-head
|
||||||
|
path: openapi-head
|
||||||
|
- name: Download Base
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: openapi-base
|
||||||
|
path: openapi-base
|
||||||
|
- name: Detect Changes
|
||||||
|
id: openapi-diff
|
||||||
|
run: |
|
||||||
|
sed -i 's:allOf:oneOf:g' openapi-head/openapi.json
|
||||||
|
sed -i 's:allOf:oneOf:g' openapi-base/openapi.json
|
||||||
|
|
||||||
|
mkdir -p /tmp/openapi-report
|
||||||
|
mv openapi-head/openapi.json /tmp/openapi-report/head.json
|
||||||
|
mv openapi-base/openapi.json /tmp/openapi-report/base.json
|
||||||
|
|
||||||
|
docker run -v /tmp/openapi-report:/data openapitools/openapi-diff:2.1.6 /data/base.json /data/head.json --state -l ERROR --markdown /data/openapi-report.md
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: openapi-report
|
||||||
|
path: /tmp/openapi-report/openapi-report.md
|
||||||
59
.github/workflows/openapi-workflow-run.yml
vendored
Normal file
59
.github/workflows/openapi-workflow-run.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: OpenAPI Report
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- OpenAPI Check
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
metadata:
|
||||||
|
name: Generate Metadata
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
outputs:
|
||||||
|
pr_number: ${{ steps.pr_number.outputs.pr_number }}
|
||||||
|
steps:
|
||||||
|
- name: Get Pull Request Number
|
||||||
|
id: pr_number
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
run: |
|
||||||
|
API_RESPONSE=$(gh pr list --repo "${GITHUB_REPOSITORY}" --search "${HEAD_SHA}" --state open --json number)
|
||||||
|
PR_NUMBER=$(echo "${API_RESPONSE}" | jq '.[0].number')
|
||||||
|
|
||||||
|
echo "repository: ${GITHUB_REPOSITORY}"
|
||||||
|
echo "sha: ${HEAD_SHA}"
|
||||||
|
echo "response: ${API_RESPONSE}"
|
||||||
|
echo "pr: ${PR_NUMBER}"
|
||||||
|
|
||||||
|
echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
comment:
|
||||||
|
name: Pull Request Comment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
needs:
|
||||||
|
- metadata
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Download OpenAPI Report
|
||||||
|
id: download_report
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: openapi-report
|
||||||
|
path: openapi-report
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Push Comment
|
||||||
|
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
file-path: ${{ steps.download_report.outputs.download-path }}/openapi-report.md
|
||||||
|
pr-number: ${{ needs.metadata.outputs.pr_number }}
|
||||||
|
comment-tag: openapi-report
|
||||||
3
.github/workflows/project-automation.yml
vendored
3
.github/workflows/project-automation.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request_target:
|
pull_request:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -21,6 +21,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
project: Current Release
|
project: Current Release
|
||||||
action: delete
|
action: delete
|
||||||
|
column: In progress
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add to 'Release Next' project
|
- name: Add to 'Release Next' project
|
||||||
|
|||||||
4
.github/workflows/pull-request-conflict.yml
vendored
4
.github/workflows/pull-request-conflict.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request_target:
|
pull_request:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Apply label
|
- name: Apply label
|
||||||
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request'}}
|
||||||
with:
|
with:
|
||||||
dirtyLabel: 'merge conflict'
|
dirtyLabel: 'merge conflict'
|
||||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||||
|
|||||||
2
.github/workflows/pull-request-stale.yaml
vendored
2
.github/workflows/pull-request-stale.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
ascending: true
|
ascending: true
|
||||||
|
|||||||
6
.github/workflows/release-bump-version.yaml
vendored
6
.github/workflows/release-bump-version.yaml
vendored
@@ -28,12 +28,12 @@ jobs:
|
|||||||
timeoutSeconds: 3600
|
timeoutSeconds: 3600
|
||||||
|
|
||||||
- name: Setup YQ
|
- name: Setup YQ
|
||||||
uses: chrisdickinson/setup-yq@latest
|
uses: chrisdickinson/setup-yq@fa3192edd79d6eb0e4e12de8dde3a0c26f2b853b # latest
|
||||||
with:
|
with:
|
||||||
yq-version: v4.9.8
|
yq-version: v4.9.8
|
||||||
|
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.TAG_BRANCH }}
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.TAG_BRANCH }}
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
|
|||||||
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll",
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll",
|
||||||
"args": ["--nowebclient"],
|
"args": ["--nowebclient"],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll",
|
||||||
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
|
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Jellyfin Contributors
|
# Jellyfin Contributors
|
||||||
|
|
||||||
|
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
||||||
- [1337joe](https://github.com/1337joe)
|
- [1337joe](https://github.com/1337joe)
|
||||||
- [97carmine](https://github.com/97carmine)
|
- [97carmine](https://github.com/97carmine)
|
||||||
- [Abbe98](https://github.com/Abbe98)
|
- [Abbe98](https://github.com/Abbe98)
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
- [bilde2910](https://github.com/bilde2910)
|
- [bilde2910](https://github.com/bilde2910)
|
||||||
- [bfayers](https://github.com/bfayers)
|
- [bfayers](https://github.com/bfayers)
|
||||||
- [BnMcG](https://github.com/BnMcG)
|
- [BnMcG](https://github.com/BnMcG)
|
||||||
- [Bond-009](https://github.com/Bond-009)
|
- [Bond_009](https://github.com/Bond-009)
|
||||||
- [brianjmurrell](https://github.com/brianjmurrell)
|
- [brianjmurrell](https://github.com/brianjmurrell)
|
||||||
- [bugfixin](https://github.com/bugfixin)
|
- [bugfixin](https://github.com/bugfixin)
|
||||||
- [chaosinnovator](https://github.com/chaosinnovator)
|
- [chaosinnovator](https://github.com/chaosinnovator)
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
- [DaveChild](https://github.com/DaveChild)
|
- [DaveChild](https://github.com/DaveChild)
|
||||||
- [DavidFair](https://github.com/DavidFair)
|
- [DavidFair](https://github.com/DavidFair)
|
||||||
- [Delgan](https://github.com/Delgan)
|
- [Delgan](https://github.com/Delgan)
|
||||||
|
- [DerMaddis](https://github.com/dermaddis)
|
||||||
- [Derpipose](https://github.com/Derpipose)
|
- [Derpipose](https://github.com/Derpipose)
|
||||||
- [dcrdev](https://github.com/dcrdev)
|
- [dcrdev](https://github.com/dcrdev)
|
||||||
- [dhartung](https://github.com/dhartung)
|
- [dhartung](https://github.com/dhartung)
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
- [geilername](https://github.com/geilername)
|
- [geilername](https://github.com/geilername)
|
||||||
- [GermanCoding](https://github.com/GermanCoding)
|
- [GermanCoding](https://github.com/GermanCoding)
|
||||||
- [gnattu](https://github.com/gnattu)
|
- [gnattu](https://github.com/gnattu)
|
||||||
|
- [gnuyent](https://github.com/gnuyent)
|
||||||
- [GodTamIt](https://github.com/GodTamIt)
|
- [GodTamIt](https://github.com/GodTamIt)
|
||||||
- [grafixeyehero](https://github.com/grafixeyehero)
|
- [grafixeyehero](https://github.com/grafixeyehero)
|
||||||
- [h1nk](https://github.com/h1nk)
|
- [h1nk](https://github.com/h1nk)
|
||||||
@@ -61,6 +64,7 @@
|
|||||||
- [HelloWorld017](https://github.com/HelloWorld017)
|
- [HelloWorld017](https://github.com/HelloWorld017)
|
||||||
- [ikomhoog](https://github.com/ikomhoog)
|
- [ikomhoog](https://github.com/ikomhoog)
|
||||||
- [iwalton3](https://github.com/iwalton3)
|
- [iwalton3](https://github.com/iwalton3)
|
||||||
|
- [Jakob Kukla](https://github.com/jakobkukla)
|
||||||
- [jftuga](https://github.com/jftuga)
|
- [jftuga](https://github.com/jftuga)
|
||||||
- [jkhsjdhjs](https://github.com/jkhsjdhjs)
|
- [jkhsjdhjs](https://github.com/jkhsjdhjs)
|
||||||
- [jmshrv](https://github.com/jmshrv)
|
- [jmshrv](https://github.com/jmshrv)
|
||||||
@@ -69,8 +73,10 @@
|
|||||||
- [JustAMan](https://github.com/JustAMan)
|
- [JustAMan](https://github.com/JustAMan)
|
||||||
- [justinfenn](https://github.com/justinfenn)
|
- [justinfenn](https://github.com/justinfenn)
|
||||||
- [JPVenson](https://github.com/JPVenson)
|
- [JPVenson](https://github.com/JPVenson)
|
||||||
|
- [JPUC1143](https://github.com/Jpuc1143/)
|
||||||
- [KerryRJ](https://github.com/KerryRJ)
|
- [KerryRJ](https://github.com/KerryRJ)
|
||||||
- [Larvitar](https://github.com/Larvitar)
|
- [Larvitar](https://github.com/Larvitar)
|
||||||
|
- [lbenini](https://github.com/lbenini)
|
||||||
- [LeoVerto](https://github.com/LeoVerto)
|
- [LeoVerto](https://github.com/LeoVerto)
|
||||||
- [Liggy](https://github.com/Liggy)
|
- [Liggy](https://github.com/Liggy)
|
||||||
- [lmaonator](https://github.com/lmaonator)
|
- [lmaonator](https://github.com/lmaonator)
|
||||||
@@ -83,15 +89,19 @@
|
|||||||
- [marius-luca-87](https://github.com/marius-luca-87)
|
- [marius-luca-87](https://github.com/marius-luca-87)
|
||||||
- [mark-monteiro](https://github.com/mark-monteiro)
|
- [mark-monteiro](https://github.com/mark-monteiro)
|
||||||
- [MarkCiliaVincenti](https://github.com/MarkCiliaVincenti)
|
- [MarkCiliaVincenti](https://github.com/MarkCiliaVincenti)
|
||||||
|
- [Martin Reuter](https://github.com/reuterma24)
|
||||||
- [Matt07211](https://github.com/Matt07211)
|
- [Matt07211](https://github.com/Matt07211)
|
||||||
|
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||||
- [Maxr1998](https://github.com/Maxr1998)
|
- [Maxr1998](https://github.com/Maxr1998)
|
||||||
- [mcarlton00](https://github.com/mcarlton00)
|
- [mcarlton00](https://github.com/mcarlton00)
|
||||||
|
- [Michael McElroy](https://github.com/mcmcelro)
|
||||||
- [mitchfizz05](https://github.com/mitchfizz05)
|
- [mitchfizz05](https://github.com/mitchfizz05)
|
||||||
- [mohd-akram](https://github.com/mohd-akram)
|
- [mohd-akram](https://github.com/mohd-akram)
|
||||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||||
- [n8225](https://github.com/n8225)
|
- [n8225](https://github.com/n8225)
|
||||||
- [Nalsai](https://github.com/Nalsai)
|
- [Nalsai](https://github.com/Nalsai)
|
||||||
- [Narfinger](https://github.com/Narfinger)
|
- [Narfinger](https://github.com/Narfinger)
|
||||||
|
- [Nathan McCrina](https://github.com/nfmccrina)
|
||||||
- [NathanPickard](https://github.com/NathanPickard)
|
- [NathanPickard](https://github.com/NathanPickard)
|
||||||
- [neilsb](https://github.com/neilsb)
|
- [neilsb](https://github.com/neilsb)
|
||||||
- [nevado](https://github.com/nevado)
|
- [nevado](https://github.com/nevado)
|
||||||
@@ -102,6 +112,7 @@
|
|||||||
- [OancaAndrei](https://github.com/OancaAndrei)
|
- [OancaAndrei](https://github.com/OancaAndrei)
|
||||||
- [obradovichv](https://github.com/obradovichv)
|
- [obradovichv](https://github.com/obradovichv)
|
||||||
- [oddstr13](https://github.com/oddstr13)
|
- [oddstr13](https://github.com/oddstr13)
|
||||||
|
- [olsh](https://github.com/olsh)
|
||||||
- [orryverducci](https://github.com/orryverducci)
|
- [orryverducci](https://github.com/orryverducci)
|
||||||
- [petermcneil](https://github.com/petermcneil)
|
- [petermcneil](https://github.com/petermcneil)
|
||||||
- [Phlogi](https://github.com/Phlogi)
|
- [Phlogi](https://github.com/Phlogi)
|
||||||
@@ -112,11 +123,13 @@
|
|||||||
- [RazeLighter777](https://github.com/RazeLighter777)
|
- [RazeLighter777](https://github.com/RazeLighter777)
|
||||||
- [redSpoutnik](https://github.com/redSpoutnik)
|
- [redSpoutnik](https://github.com/redSpoutnik)
|
||||||
- [ringmatter](https://github.com/ringmatter)
|
- [ringmatter](https://github.com/ringmatter)
|
||||||
|
- [Robert Lützner](https://github.com/rluetzner)
|
||||||
- [ryan-hartzell](https://github.com/ryan-hartzell)
|
- [ryan-hartzell](https://github.com/ryan-hartzell)
|
||||||
- [s0urcelab](https://github.com/s0urcelab)
|
- [s0urcelab](https://github.com/s0urcelab)
|
||||||
- [sachk](https://github.com/sachk)
|
- [sachk](https://github.com/sachk)
|
||||||
- [sammyrc34](https://github.com/sammyrc34)
|
- [sammyrc34](https://github.com/sammyrc34)
|
||||||
- [samuel9554](https://github.com/samuel9554)
|
- [samuel9554](https://github.com/samuel9554)
|
||||||
|
- [SapientGuardian](https://github.com/SapientGuardian)
|
||||||
- [scheidleon](https://github.com/scheidleon)
|
- [scheidleon](https://github.com/scheidleon)
|
||||||
- [sebPomme](https://github.com/sebPomme)
|
- [sebPomme](https://github.com/sebPomme)
|
||||||
- [SegiH](https://github.com/SegiH)
|
- [SegiH](https://github.com/SegiH)
|
||||||
@@ -126,6 +139,7 @@
|
|||||||
- [sl1288](https://github.com/sl1288)
|
- [sl1288](https://github.com/sl1288)
|
||||||
- [Smith00101010](https://github.com/Smith00101010)
|
- [Smith00101010](https://github.com/Smith00101010)
|
||||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||||
|
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
|
||||||
- [sparky8251](https://github.com/sparky8251)
|
- [sparky8251](https://github.com/sparky8251)
|
||||||
- [spookbits](https://github.com/spookbits)
|
- [spookbits](https://github.com/spookbits)
|
||||||
- [ssenart](https://github.com/ssenart)
|
- [ssenart](https://github.com/ssenart)
|
||||||
@@ -148,6 +162,7 @@
|
|||||||
- [twinkybot](https://github.com/twinkybot)
|
- [twinkybot](https://github.com/twinkybot)
|
||||||
- [Ullmie02](https://github.com/Ullmie02)
|
- [Ullmie02](https://github.com/Ullmie02)
|
||||||
- [Unhelpful](https://github.com/Unhelpful)
|
- [Unhelpful](https://github.com/Unhelpful)
|
||||||
|
- [Utku Özdemir](https://github.com/utkuozdemir)
|
||||||
- [viaregio](https://github.com/viaregio)
|
- [viaregio](https://github.com/viaregio)
|
||||||
- [vitorsemeano](https://github.com/vitorsemeano)
|
- [vitorsemeano](https://github.com/vitorsemeano)
|
||||||
- [voodoos](https://github.com/voodoos)
|
- [voodoos](https://github.com/voodoos)
|
||||||
@@ -163,6 +178,7 @@
|
|||||||
- [XVicarious](https://github.com/XVicarious)
|
- [XVicarious](https://github.com/XVicarious)
|
||||||
- [YouKnowBlom](https://github.com/YouKnowBlom)
|
- [YouKnowBlom](https://github.com/YouKnowBlom)
|
||||||
- [ZachPhelan](https://github.com/ZachPhelan)
|
- [ZachPhelan](https://github.com/ZachPhelan)
|
||||||
|
- [ZeusCraft10](https://github.com/ZeusCraft10)
|
||||||
- [KristupasSavickas](https://github.com/KristupasSavickas)
|
- [KristupasSavickas](https://github.com/KristupasSavickas)
|
||||||
- [Pusta](https://github.com/pusta)
|
- [Pusta](https://github.com/pusta)
|
||||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||||
@@ -205,6 +221,14 @@
|
|||||||
- [theshoeshiner](https://github.com/theshoeshiner)
|
- [theshoeshiner](https://github.com/theshoeshiner)
|
||||||
- [TokerX](https://github.com/TokerX)
|
- [TokerX](https://github.com/TokerX)
|
||||||
- [GeneMarks](https://github.com/GeneMarks)
|
- [GeneMarks](https://github.com/GeneMarks)
|
||||||
|
- [Kirill Nikiforov](https://github.com/allmazz)
|
||||||
|
- [bjorntp](https://github.com/bjorntp)
|
||||||
|
- [martenumberto](https://github.com/martenumberto)
|
||||||
|
- [ZeusCraft10](https://github.com/ZeusCraft10)
|
||||||
|
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
|
||||||
|
- [LiHRaM](https://github.com/LiHRaM)
|
||||||
|
- [MSalman5230](https://github.com/MSalman5230)
|
||||||
|
- [dwandw](https://github.com/dwandw)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
@@ -268,16 +292,3 @@
|
|||||||
- [tikuf](https://github.com/tikuf/)
|
- [tikuf](https://github.com/tikuf/)
|
||||||
- [Tim Hobbs](https://github.com/timhobbs)
|
- [Tim Hobbs](https://github.com/timhobbs)
|
||||||
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
||||||
- [olsh](https://github.com/olsh)
|
|
||||||
- [lbenini](https://github.com/lbenini)
|
|
||||||
- [gnuyent](https://github.com/gnuyent)
|
|
||||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
|
||||||
- [Jakob Kukla](https://github.com/jakobkukla)
|
|
||||||
- [Utku Özdemir](https://github.com/utkuozdemir)
|
|
||||||
- [JPUC1143](https://github.com/Jpuc1143/)
|
|
||||||
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
|
||||||
- [Robert Lützner](https://github.com/rluetzner)
|
|
||||||
- [Nathan McCrina](https://github.com/nfmccrina)
|
|
||||||
- [Martin Reuter](https://github.com/reuterma24)
|
|
||||||
- [Michael McElroy](https://github.com/mcmcelro)
|
|
||||||
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
|
|
||||||
|
|||||||
@@ -4,57 +4,52 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||||
<ItemGroup Label="Package Dependencies">
|
<ItemGroup Label="Package Dependencies">
|
||||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.7" />
|
<PackageVersion Include="AsyncKeyedLock" Version="8.0.2" />
|
||||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture.Xunit3" Version="4.19.0" />
|
||||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||||
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
||||||
<PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
|
<PackageVersion Include="BitFaster.Caching" Version="2.6.0" />
|
||||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
|
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
|
||||||
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
|
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
|
||||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
<PackageVersion Include="coverlet.collector" Version="10.0.0" />
|
||||||
<PackageVersion Include="Diacritics" Version="4.0.17" />
|
<PackageVersion Include="Diacritics" Version="4.1.8" />
|
||||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||||
<PackageVersion Include="FsCheck.Xunit" Version="3.3.2" />
|
<PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.3" />
|
||||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
|
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
|
||||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||||
<PackageVersion Include="Ignore" Version="0.2.1" />
|
<PackageVersion Include="Ignore" Version="0.2.1" />
|
||||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.12.0-pre1" />
|
||||||
<PackageVersion Include="libse" Version="4.0.12" />
|
<PackageVersion Include="libse" Version="4.0.12" />
|
||||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
|
||||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
|
||||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
|
||||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||||
<PackageVersion Include="NEbml" Version="1.1.0.5" />
|
<PackageVersion Include="NEbml" Version="1.1.0.5" />
|
||||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
@@ -62,11 +57,11 @@
|
|||||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||||
<PackageVersion Include="Polly" Version="8.6.5" />
|
<PackageVersion Include="Polly" Version="8.6.6" />
|
||||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
|
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
@@ -74,26 +69,21 @@
|
|||||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
||||||
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
||||||
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
|
<PackageVersion Include="SkiaSharp" Version="[3.116.1]" />
|
||||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
|
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="[3.116.1]" />
|
||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" />
|
||||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||||
<PackageVersion Include="Svg.Skia" Version="3.2.1" />
|
<PackageVersion Include="Svg.Skia" Version="3.4.1" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
<PackageVersion Include="System.Text.Json" Version="10.0.7" />
|
||||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="System.Text.Json" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||||
<PackageVersion Include="z440.atl.core" Version="7.9.0" />
|
<PackageVersion Include="z440.atl.core" Version="7.13.0" />
|
||||||
<PackageVersion Include="TMDbLib" Version="2.3.0" />
|
<PackageVersion Include="TMDbLib" Version="3.0.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
<PackageVersion Include="Xunit.v3.Priority" Version="1.1.18" />
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
75
Emby.Naming/Book/BookFileNameParser.cs
Normal file
75
Emby.Naming/Book/BookFileNameParser.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Emby.Naming.Book
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class to retrieve basic metadata from a book filename.
|
||||||
|
/// </summary>
|
||||||
|
public static class BookFileNameParser
|
||||||
|
{
|
||||||
|
private const string NameMatchGroup = "name";
|
||||||
|
private const string IndexMatchGroup = "index";
|
||||||
|
private const string YearMatchGroup = "year";
|
||||||
|
private const string SeriesNameMatchGroup = "seriesName";
|
||||||
|
|
||||||
|
private static readonly Regex[] _nameMatches =
|
||||||
|
[
|
||||||
|
// seriesName (seriesYear) #index (of count) (year) where only seriesName and index are required
|
||||||
|
new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>[0-9]{4})\))?)\s#(?<index>[0-9]+)((\s\(of\s(?<count>[0-9]+)\))?)((\s\((?<year>[0-9]{4})\))?)$"),
|
||||||
|
new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>[0-9]+)\)((\s\((?<year>[0-9]{4})\))?)$"),
|
||||||
|
new Regex(@"^(?<index>[0-9]+)\s\-\s(?<name>.+?)((\s\((?<year>[0-9]{4})\))?)$"),
|
||||||
|
new Regex(@"(?<name>.*)\((?<year>[0-9]{4})\)"),
|
||||||
|
// last resort matches the whole string as the name
|
||||||
|
new Regex(@"(?<name>.*)")
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a filename name to retrieve the book name, series name, index, and year.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">Book filename to parse for information.</param>
|
||||||
|
/// <returns>Returns <see cref="BookFileNameParserResult"/> object.</returns>
|
||||||
|
public static BookFileNameParserResult Parse(string? name)
|
||||||
|
{
|
||||||
|
var result = new BookFileNameParserResult();
|
||||||
|
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var regex in _nameMatches)
|
||||||
|
{
|
||||||
|
var match = regex.Match(name);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.Groups.TryGetValue(NameMatchGroup, out Group? nameGroup) && nameGroup.Success)
|
||||||
|
{
|
||||||
|
result.Name = nameGroup.Value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.Groups.TryGetValue(IndexMatchGroup, out Group? indexGroup) && indexGroup.Success && int.TryParse(indexGroup.Value, out var index))
|
||||||
|
{
|
||||||
|
result.Index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.Groups.TryGetValue(YearMatchGroup, out Group? yearGroup) && yearGroup.Success && int.TryParse(yearGroup.Value, out var year))
|
||||||
|
{
|
||||||
|
result.Year = year;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.Groups.TryGetValue(SeriesNameMatchGroup, out Group? seriesGroup) && seriesGroup.Success)
|
||||||
|
{
|
||||||
|
result.SeriesName = seriesGroup.Value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Emby.Naming/Book/BookFileNameParserResult.cs
Normal file
41
Emby.Naming/Book/BookFileNameParserResult.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Emby.Naming.Book
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Data object used to pass metadata parsed from a book filename.
|
||||||
|
/// </summary>
|
||||||
|
public class BookFileNameParserResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="BookFileNameParserResult"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public BookFileNameParserResult()
|
||||||
|
{
|
||||||
|
Name = null;
|
||||||
|
Index = null;
|
||||||
|
Year = null;
|
||||||
|
SeriesName = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the book.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the book index.
|
||||||
|
/// </summary>
|
||||||
|
public int? Index { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the publication year.
|
||||||
|
/// </summary>
|
||||||
|
public int? Year { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the series name.
|
||||||
|
/// </summary>
|
||||||
|
public string? SeriesName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -152,8 +152,8 @@ namespace Emby.Naming.Common
|
|||||||
|
|
||||||
CleanStrings =
|
CleanStrings =
|
||||||
[
|
[
|
||||||
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS)(?=[ _\,\.\(\)\[\]\-]|$)",
|
||||||
@"^(?<cleaned>.+?)(\[.*\])",
|
@"^\s*(?<cleaned>.+?)((\s*\[[^\]]+\]\s*)+)(\.[^\s]+)?$",
|
||||||
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
||||||
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
||||||
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
|
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
|
||||||
@@ -225,6 +225,7 @@ namespace Emby.Naming.Common
|
|||||||
".afc",
|
".afc",
|
||||||
".amf",
|
".amf",
|
||||||
".aif",
|
".aif",
|
||||||
|
".aifc",
|
||||||
".aiff",
|
".aiff",
|
||||||
".alac",
|
".alac",
|
||||||
".amr",
|
".amr",
|
||||||
@@ -378,6 +379,14 @@ namespace Emby.Naming.Common
|
|||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// "Name - 101.mkv", "Name - 101 [720p].mkv", "Name - 101 (2020).mkv"
|
||||||
|
// Handles absolute episode numbers with hyphen delimiter (common in anime)
|
||||||
|
// Without brackets (bracketed version handled above)
|
||||||
|
new EpisodeExpression(@".*[\\\/](?<seriesname>[^\\\/]+?)[\s_]+-[\s_]+(?<epnumber>[0-9]+)[\s_]*(?:\[.*?\]|\(.*?\))*[\s_]*(?:\.\w+)?$")
|
||||||
|
{
|
||||||
|
IsNamed = true
|
||||||
|
},
|
||||||
|
|
||||||
// /server/anything_102.mp4
|
// /server/anything_102.mp4
|
||||||
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
|
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
|
||||||
// /server/anything_1996.11.14.mp4
|
// /server/anything_1996.11.14.mp4
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public static class TvParserHelpers
|
|||||||
/// <param name="status">The status string.</param>
|
/// <param name="status">The status string.</param>
|
||||||
/// <param name="enumValue">The <see cref="SeriesStatus"/>.</param>
|
/// <param name="enumValue">The <see cref="SeriesStatus"/>.</param>
|
||||||
/// <returns>Returns true if parsing was successful.</returns>
|
/// <returns>Returns true if parsing was successful.</returns>
|
||||||
public static bool TryParseSeriesStatus(string status, out SeriesStatus? enumValue)
|
public static bool TryParseSeriesStatus(string? status, out SeriesStatus? enumValue)
|
||||||
{
|
{
|
||||||
if (Enum.TryParse(status, true, out SeriesStatus seriesStatus))
|
if (Enum.TryParse(status, true, out SeriesStatus seriesStatus))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace Emby.Naming.Video
|
|||||||
var match = expression.Match(name);
|
var match = expression.Match(name);
|
||||||
if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
|
if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
|
||||||
{
|
{
|
||||||
newName = cleaned.Value;
|
newName = cleaned.Value.Trim();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -136,19 +137,27 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
if (videos.Count > 1)
|
if (videos.Count > 1)
|
||||||
{
|
{
|
||||||
var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
|
var groups = videos
|
||||||
|
.Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x))
|
||||||
|
.Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value))
|
||||||
|
.GroupBy(x => x.resolutionMatch.Success)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
videos.Clear();
|
videos.Clear();
|
||||||
|
|
||||||
|
StringComparer comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
|
||||||
foreach (var group in groups)
|
foreach (var group in groups)
|
||||||
{
|
{
|
||||||
if (group.Key)
|
if (group.Key)
|
||||||
{
|
{
|
||||||
videos.InsertRange(0, group
|
videos.InsertRange(0, group
|
||||||
.OrderByDescending(x => ResolutionRegex().Match(x.Files[0].FileNameWithoutExtension.ToString()).Value, new AlphanumericComparator())
|
.OrderByDescending(x => x.resolutionMatch.Value, comparer)
|
||||||
.ThenBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
|
.ThenBy(x => x.filename, comparer)
|
||||||
|
.Select(x => x.value));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
|
videos.AddRange(group.OrderBy(x => x.filename, comparer).Select(x => x.value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,6 +217,8 @@ namespace Emby.Naming.Video
|
|||||||
// The CleanStringParser should have removed common keywords etc.
|
// The CleanStringParser should have removed common keywords etc.
|
||||||
return testFilename.IsEmpty
|
return testFilename.IsEmpty
|
||||||
|| testFilename[0] == '-'
|
|| testFilename[0] == '-'
|
||||||
|
|| testFilename[0] == '_'
|
||||||
|
|| testFilename[0] == '.'
|
||||||
|| CheckMultiVersionRegex().IsMatch(testFilename);
|
|| CheckMultiVersionRegex().IsMatch(testFilename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
CreateAndCheckMarker(ProgramDataPath, "data");
|
CreateAndCheckMarker(ProgramDataPath, "data");
|
||||||
CreateAndCheckMarker(CachePath, "cache");
|
CreateAndCheckMarker(CachePath, "cache");
|
||||||
CreateAndCheckMarker(DataPath, "data");
|
CreateAndCheckMarker(DataPath, "data");
|
||||||
|
CreateCacheDirTag(CachePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -100,6 +101,26 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
|
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a CACHEDIR.TAG file in the specified directory per the Cache Directory Tagging specification.
|
||||||
|
/// This signals to backup tools (e.g. Restic, Borg) that the directory contains cached data
|
||||||
|
/// and can be excluded from backups.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The cache directory path.</param>
|
||||||
|
internal static void CreateCacheDirTag(string path)
|
||||||
|
{
|
||||||
|
var tagPath = Path.Combine(path, "CACHEDIR.TAG");
|
||||||
|
if (!File.Exists(tagPath))
|
||||||
|
{
|
||||||
|
File.WriteAllText(
|
||||||
|
tagPath,
|
||||||
|
"Signature: 8a477f597d28d172789f06886806bc55\n"
|
||||||
|
+ "# This file is a cache directory tag created by Jellyfin.\n"
|
||||||
|
+ "# For information about cache directory tags, see:\n"
|
||||||
|
+ "#\thttps://bford.info/cachedir/\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<string> GetMarkers(string path, bool recursive = false)
|
private IEnumerable<string> GetMarkers(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
Logger.LogInformation("Setting cache path: {Path}", cachePath);
|
Logger.LogInformation("Setting cache path: {Path}", cachePath);
|
||||||
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
|
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
|
||||||
CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
|
CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
|
||||||
|
BaseApplicationPaths.CreateCacheDirTag(cachePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -507,7 +507,13 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
|
serviceCollection.AddSingleton<BaseItemRepository>();
|
||||||
|
serviceCollection.AddSingleton<IItemRepository>(sp => sp.GetRequiredService<BaseItemRepository>());
|
||||||
|
serviceCollection.AddSingleton<IItemQueryHelpers>(sp => sp.GetRequiredService<BaseItemRepository>());
|
||||||
|
serviceCollection.AddSingleton<IItemPersistenceService, ItemPersistenceService>();
|
||||||
|
serviceCollection.AddSingleton<INextUpService, NextUpService>();
|
||||||
|
serviceCollection.AddSingleton<IItemCountService, ItemCountService>();
|
||||||
|
serviceCollection.AddSingleton<ILinkedChildrenService, LinkedChildrenService>();
|
||||||
serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
|
serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
|
||||||
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
|
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
|
||||||
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
|
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
|
||||||
@@ -530,6 +536,7 @@ namespace Emby.Server.Implementations
|
|||||||
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
|
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
|
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
|
||||||
|
serviceCollection.AddSingleton<DotIgnoreIgnoreRule>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
|
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
|
||||||
|
|
||||||
@@ -641,6 +648,7 @@ namespace Emby.Server.Implementations
|
|||||||
BaseItem.ConfigurationManager = ConfigurationManager;
|
BaseItem.ConfigurationManager = ConfigurationManager;
|
||||||
BaseItem.FileSystem = Resolve<IFileSystem>();
|
BaseItem.FileSystem = Resolve<IFileSystem>();
|
||||||
BaseItem.ItemRepository = Resolve<IItemRepository>();
|
BaseItem.ItemRepository = Resolve<IItemRepository>();
|
||||||
|
BaseItem.ItemCountService = Resolve<IItemCountService>();
|
||||||
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
||||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||||
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Chapters;
|
using MediaBrowser.Controller.Chapters;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.IO;
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
@@ -128,7 +129,7 @@ public class ChapterManager : IChapterManager
|
|||||||
|
|
||||||
var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
|
var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
|
||||||
var threshold = TimeSpan.FromSeconds(1).Ticks;
|
var threshold = TimeSpan.FromSeconds(1).Ticks;
|
||||||
if (averageChapterDuration < threshold)
|
if (chapters.Count >= 2 && averageChapterDuration < threshold)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold);
|
_logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold);
|
||||||
extractImages = false;
|
extractImages = false;
|
||||||
@@ -232,12 +233,22 @@ public class ChapterManager : IChapterManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
|
public bool Supports(BaseItem item)
|
||||||
|
=> item is Video or Audio;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SaveChapters(BaseItem item, IReadOnlyList<ChapterInfo> chapters)
|
||||||
{
|
{
|
||||||
// Remove any chapters that are outside of the runtime of the video
|
if (!Supports(item))
|
||||||
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
|
{
|
||||||
_chapterRepository.SaveChapters(video.Id, validChapters);
|
_logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any chapters that are outside of the runtime of the item
|
||||||
|
var validChapters = chapters.Where(c => c.StartPositionTicks < item.RunTimeTicks).ToList();
|
||||||
|
_chapterRepository.SaveChapters(item.Id, validChapters);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ChapterInfo? GetChapter(Guid baseItemId, int index)
|
public ChapterInfo? GetChapter(Guid baseItemId, int index)
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
{
|
{
|
||||||
var childItem = _libraryManager.GetItemById(guidId);
|
var childItem = _libraryManager.GetItemById(guidId);
|
||||||
|
|
||||||
var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem is not null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase)));
|
var child = collection.LinkedChildren.FirstOrDefault(i => i.ItemId.HasValue && i.ItemId.Value.Equals(guidId));
|
||||||
|
|
||||||
if (child is null)
|
if (child is null)
|
||||||
{
|
{
|
||||||
@@ -342,7 +342,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
|
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
|
||||||
if (item is Video video)
|
if (item is Video video)
|
||||||
{
|
{
|
||||||
foreach (var childId in video.GetLocalAlternateVersionIds())
|
foreach (var childId in _libraryManager.GetLocalAlternateVersionIds(video))
|
||||||
{
|
{
|
||||||
if (!results.ContainsKey(childId))
|
if (!results.ContainsKey(childId))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,22 +39,24 @@ namespace Emby.Server.Implementations.Cryptography
|
|||||||
{
|
{
|
||||||
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
|
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
var iterations = GetIterationsParameter(hash);
|
||||||
return hash.Hash.SequenceEqual(
|
return hash.Hash.SequenceEqual(
|
||||||
Rfc2898DeriveBytes.Pbkdf2(
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
password,
|
password,
|
||||||
hash.Salt,
|
hash.Salt,
|
||||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
iterations,
|
||||||
HashAlgorithmName.SHA1,
|
HashAlgorithmName.SHA1,
|
||||||
32));
|
32));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
|
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
var iterations = GetIterationsParameter(hash);
|
||||||
return hash.Hash.SequenceEqual(
|
return hash.Hash.SequenceEqual(
|
||||||
Rfc2898DeriveBytes.Pbkdf2(
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
password,
|
password,
|
||||||
hash.Salt,
|
hash.Salt,
|
||||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
iterations,
|
||||||
HashAlgorithmName.SHA512,
|
HashAlgorithmName.SHA512,
|
||||||
DefaultOutputLength));
|
DefaultOutputLength));
|
||||||
}
|
}
|
||||||
@@ -62,6 +64,27 @@ namespace Emby.Server.Implementations.Cryptography
|
|||||||
throw new NotSupportedException($"Can't verify hash with id: {hash.Id}");
|
throw new NotSupportedException($"Can't verify hash with id: {hash.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts and validates the iterations parameter from a password hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash">The password hash containing parameters.</param>
|
||||||
|
/// <returns>The number of iterations.</returns>
|
||||||
|
/// <exception cref="FormatException">Thrown when iterations parameter is missing or invalid.</exception>
|
||||||
|
private static int GetIterationsParameter(PasswordHash hash)
|
||||||
|
{
|
||||||
|
if (!hash.Parameters.TryGetValue("iterations", out var iterationsStr))
|
||||||
|
{
|
||||||
|
throw new FormatException($"Password hash with id '{hash.Id}' is missing required 'iterations' parameter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(iterationsStr, CultureInfo.InvariantCulture, out var iterations))
|
||||||
|
{
|
||||||
|
throw new FormatException($"Password hash with id '{hash.Id}' has invalid 'iterations' parameter: '{iterationsStr}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return iterations;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public byte[] GenerateSalt()
|
public byte[] GenerateSalt()
|
||||||
=> GenerateSalt(DefaultSaltLength);
|
=> GenerateSalt(DefaultSaltLength);
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Database.Implementations;
|
using Jellyfin.Database.Implementations;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.IO;
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Playlists;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -35,7 +37,11 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
|||||||
|
|
||||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
|
var deadItemsProgress = new Progress<double>(val => progress.Report(val * 0.8));
|
||||||
|
await CleanDeadItems(cancellationToken, deadItemsProgress).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var playlistProgress = new Progress<double>(val => progress.Report(80 + (val * 0.2)));
|
||||||
|
await CleanOrphanedFilePlaylistsAsync(cancellationToken, playlistProgress).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
|
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
|
||||||
@@ -116,4 +122,32 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
|||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CleanOrphanedFilePlaylistsAsync(CancellationToken cancellationToken, IProgress<double> progress)
|
||||||
|
{
|
||||||
|
var playlists = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Playlist],
|
||||||
|
Recursive = true
|
||||||
|
}).OfType<Playlist>().ToList();
|
||||||
|
|
||||||
|
var numComplete = 0;
|
||||||
|
var numItems = Math.Max(playlists.Count, 1);
|
||||||
|
|
||||||
|
foreach (var playlist in playlists)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (playlist.IsFile && !File.Exists(playlist.Path))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Removing file-based playlist {Name} because source file {Path} no longer exists", playlist.Name, playlist.Path);
|
||||||
|
_libraryManager.DeleteItem(playlist, new DeleteOptions { DeleteFileLocation = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
numComplete++;
|
||||||
|
progress.Report((double)numComplete / numItems * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report(100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,17 +153,68 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null)
|
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(
|
||||||
|
IReadOnlyList<BaseItem> items,
|
||||||
|
DtoOptions options,
|
||||||
|
User? user = null,
|
||||||
|
BaseItem? owner = null,
|
||||||
|
bool skipVisibilityCheck = false)
|
||||||
{
|
{
|
||||||
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
|
var accessibleItems = skipVisibilityCheck || user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
|
||||||
var returnItems = new BaseItemDto[accessibleItems.Count];
|
var returnItems = new BaseItemDto[accessibleItems.Count];
|
||||||
List<(BaseItem, BaseItemDto)>? programTuples = null;
|
List<(BaseItem, BaseItemDto)>? programTuples = null;
|
||||||
List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
|
List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
|
||||||
|
|
||||||
|
// Batch-fetch user data for all items
|
||||||
|
Dictionary<Guid, UserItemData>? userDataBatch = null;
|
||||||
|
if (user is not null && options.EnableUserData)
|
||||||
|
{
|
||||||
|
userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-compute collection folders once to avoid N+1 queries in CanDelete
|
||||||
|
List<Folder>? allCollectionFolders = null;
|
||||||
|
if (user is not null && options.ContainsField(ItemFields.CanDelete))
|
||||||
|
{
|
||||||
|
allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch-fetch child counts for all folders to avoid N+1 queries
|
||||||
|
Dictionary<Guid, int>? childCountBatch = null;
|
||||||
|
if (options.ContainsField(ItemFields.ChildCount))
|
||||||
|
{
|
||||||
|
var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
|
||||||
|
if (folderIds.Count > 0)
|
||||||
|
{
|
||||||
|
childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch-fetch played/total counts for all folders to avoid N+1 queries
|
||||||
|
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null;
|
||||||
|
if (user is not null && options.EnableUserData)
|
||||||
|
{
|
||||||
|
var folderIds = accessibleItems.OfType<Folder>()
|
||||||
|
.Where(f => f.SupportsUserDataFromChildren && (f.SupportsPlayedStatus || options.ContainsField(ItemFields.RecursiveItemCount)))
|
||||||
|
.Select(f => f.Id).ToList();
|
||||||
|
if (folderIds.Count > 0)
|
||||||
|
{
|
||||||
|
playedCountBatch = _libraryManager.GetPlayedAndTotalCountBatch(folderIds, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int index = 0; index < accessibleItems.Count; index++)
|
for (int index = 0; index < accessibleItems.Count; index++)
|
||||||
{
|
{
|
||||||
var item = accessibleItems[index];
|
var item = accessibleItems[index];
|
||||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
var dto = GetBaseItemDtoInternal(
|
||||||
|
item,
|
||||||
|
options,
|
||||||
|
user,
|
||||||
|
owner,
|
||||||
|
userDataBatch?.GetValueOrDefault(item.Id),
|
||||||
|
allCollectionFolders,
|
||||||
|
childCountBatch,
|
||||||
|
playedCountBatch);
|
||||||
|
|
||||||
if (item is LiveTvChannel tvChannel)
|
if (item is LiveTvChannel tvChannel)
|
||||||
{
|
{
|
||||||
@@ -197,7 +248,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||||
{
|
{
|
||||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
var dto = GetBaseItemDtoInternal(item, options, user, owner, null);
|
||||||
if (item is LiveTvChannel tvChannel)
|
if (item is LiveTvChannel tvChannel)
|
||||||
{
|
{
|
||||||
LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
|
LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
|
||||||
@@ -215,7 +266,15 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
private BaseItemDto GetBaseItemDtoInternal(
|
||||||
|
BaseItem item,
|
||||||
|
DtoOptions options,
|
||||||
|
User? user = null,
|
||||||
|
BaseItem? owner = null,
|
||||||
|
UserItemData? userData = null,
|
||||||
|
List<Folder>? allCollectionFolders = null,
|
||||||
|
Dictionary<Guid, int>? childCountBatch = null,
|
||||||
|
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
|
||||||
{
|
{
|
||||||
var dto = new BaseItemDto
|
var dto = new BaseItemDto
|
||||||
{
|
{
|
||||||
@@ -252,7 +311,14 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (user is not null)
|
if (user is not null)
|
||||||
{
|
{
|
||||||
AttachUserSpecificInfo(dto, item, user, options);
|
AttachUserSpecificInfo(
|
||||||
|
dto,
|
||||||
|
item,
|
||||||
|
user,
|
||||||
|
options,
|
||||||
|
userData,
|
||||||
|
childCountBatch,
|
||||||
|
playedCountBatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is IHasMediaSources
|
if (item is IHasMediaSources
|
||||||
@@ -274,7 +340,9 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
{
|
{
|
||||||
dto.CanDelete = user is null
|
dto.CanDelete = user is null
|
||||||
? item.CanDelete()
|
? item.CanDelete()
|
||||||
: item.CanDelete(user);
|
: allCollectionFolders is not null
|
||||||
|
? item.CanDelete(user, allCollectionFolders)
|
||||||
|
: item.CanDelete(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.CanDownload))
|
if (options.ContainsField(ItemFields.CanDownload))
|
||||||
@@ -378,37 +446,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
|
||||||
{
|
|
||||||
Recursive = true,
|
|
||||||
DtoOptions = new DtoOptions(false) { EnableImages = false },
|
|
||||||
IncludeItemTypes = relatedItemKinds
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (dto.Type)
|
|
||||||
{
|
|
||||||
case BaseItemKind.Genre:
|
|
||||||
case BaseItemKind.MusicGenre:
|
|
||||||
query.GenreIds = [dto.Id];
|
|
||||||
break;
|
|
||||||
case BaseItemKind.MusicArtist:
|
|
||||||
query.ArtistIds = [dto.Id];
|
|
||||||
break;
|
|
||||||
case BaseItemKind.Person:
|
|
||||||
query.PersonIds = [dto.Id];
|
|
||||||
break;
|
|
||||||
case BaseItemKind.Studio:
|
|
||||||
query.StudioIds = [dto.Id];
|
|
||||||
break;
|
|
||||||
case BaseItemKind.Year
|
|
||||||
when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year):
|
|
||||||
query.Years = [year];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var counts = _libraryManager.GetItemCounts(query);
|
|
||||||
|
|
||||||
dto.AlbumCount = counts.AlbumCount;
|
dto.AlbumCount = counts.AlbumCount;
|
||||||
dto.ArtistCount = counts.ArtistCount;
|
dto.ArtistCount = counts.ArtistCount;
|
||||||
@@ -458,7 +496,14 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attaches the user specific info.
|
/// Attaches the user specific info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options)
|
private void AttachUserSpecificInfo(
|
||||||
|
BaseItemDto dto,
|
||||||
|
BaseItem item,
|
||||||
|
User user,
|
||||||
|
DtoOptions options,
|
||||||
|
UserItemData? userData = null,
|
||||||
|
Dictionary<Guid, int>? childCountBatch = null,
|
||||||
|
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
|
||||||
{
|
{
|
||||||
if (item.IsFolder)
|
if (item.IsFolder)
|
||||||
{
|
{
|
||||||
@@ -466,7 +511,19 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (options.EnableUserData)
|
if (options.EnableUserData)
|
||||||
{
|
{
|
||||||
dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
|
if (userData is not null)
|
||||||
|
{
|
||||||
|
// Use pre-fetched user data
|
||||||
|
dto.UserData = GetUserItemDataDto(userData, item.Id);
|
||||||
|
(int Played, int Total)? precomputed = playedCountBatch is not null
|
||||||
|
&& playedCountBatch.TryGetValue(item.Id, out var counts) ? counts : null;
|
||||||
|
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options, precomputed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fall back to individual fetch
|
||||||
|
dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
|
if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
|
||||||
@@ -485,7 +542,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (options.ContainsField(ItemFields.ChildCount))
|
if (options.ContainsField(ItemFields.ChildCount))
|
||||||
{
|
{
|
||||||
dto.ChildCount ??= GetChildCount(folder, user);
|
dto.ChildCount ??= GetChildCount(folder, user, childCountBatch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,7 +560,17 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
{
|
{
|
||||||
if (options.EnableUserData)
|
if (options.EnableUserData)
|
||||||
{
|
{
|
||||||
dto.UserData = _userDataRepository.GetUserDataDto(item, user);
|
if (userData is not null)
|
||||||
|
{
|
||||||
|
// Use pre-fetched user data
|
||||||
|
dto.UserData = GetUserItemDataDto(userData, item.Id);
|
||||||
|
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fall back to individual fetch
|
||||||
|
dto.UserData = _userDataRepository.GetUserDataDto(item, user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,7 +580,25 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetChildCount(Folder folder, User user)
|
private static UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(data);
|
||||||
|
|
||||||
|
return new UserItemDataDto
|
||||||
|
{
|
||||||
|
IsFavorite = data.IsFavorite,
|
||||||
|
Likes = data.Likes,
|
||||||
|
PlaybackPositionTicks = data.PlaybackPositionTicks,
|
||||||
|
PlayCount = data.PlayCount,
|
||||||
|
Rating = data.Rating,
|
||||||
|
Played = data.Played,
|
||||||
|
LastPlayedDate = data.LastPlayedDate,
|
||||||
|
ItemId = itemId,
|
||||||
|
Key = data.Key
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetChildCount(Folder folder, User user, Dictionary<Guid, int>? childCountBatch)
|
||||||
{
|
{
|
||||||
// Right now this is too slow to calculate for top level folders on a per-user basis
|
// Right now this is too slow to calculate for top level folders on a per-user basis
|
||||||
// Just return something so that apps that are expecting a value won't think the folders are empty
|
// Just return something so that apps that are expecting a value won't think the folders are empty
|
||||||
@@ -522,6 +607,13 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return Random.Shared.Next(1, 10);
|
return Random.Shared.Next(1, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use pre-fetched batch data if available
|
||||||
|
if (childCountBatch is not null && childCountBatch.TryGetValue(folder.Id, out var count))
|
||||||
|
{
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to individual query for special cases (Series, Season, etc.)
|
||||||
return folder.GetChildCount(user);
|
return folder.GetChildCount(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1019,6 +1111,15 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
{
|
{
|
||||||
dto.AlbumId = albumParent.Id;
|
dto.AlbumId = albumParent.Id;
|
||||||
dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
|
dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
|
||||||
|
if (albumParent.LUFS.HasValue)
|
||||||
|
{
|
||||||
|
// -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
|
||||||
|
dto.AlbumNormalizationGain = -18f - albumParent.LUFS;
|
||||||
|
}
|
||||||
|
else if (albumParent.NormalizationGain.HasValue)
|
||||||
|
{
|
||||||
|
dto.AlbumNormalizationGain = albumParent.NormalizationGain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (options.ContainsField(ItemFields.MediaSourceCount))
|
// if (options.ContainsField(ItemFields.MediaSourceCount))
|
||||||
@@ -1051,16 +1152,16 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
||||||
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
||||||
dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))])
|
var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
||||||
.Where(e => e.Value.Length > 0)
|
|
||||||
.Select(i =>
|
dto.ArtistItems = hasArtist.Artists
|
||||||
{
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
return new NameGuidPair
|
.Distinct()
|
||||||
{
|
.Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0
|
||||||
Name = i.Key,
|
? new NameGuidPair { Name = name, Id = artists[0].Id }
|
||||||
Id = i.Value.First().Id
|
: null)
|
||||||
};
|
.Where(item => item is not null)
|
||||||
}).Where(i => i is not null).ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is IHasAlbumArtist hasAlbumArtist)
|
if (item is IHasAlbumArtist hasAlbumArtist)
|
||||||
@@ -1085,31 +1186,16 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
// })
|
// })
|
||||||
// .ToList();
|
// .ToList();
|
||||||
|
|
||||||
|
var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
||||||
|
|
||||||
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
||||||
// .Except(foundArtists, new DistinctNameComparer())
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
.Select(i =>
|
.Distinct()
|
||||||
{
|
.Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0
|
||||||
// This should not be necessary but we're seeing some cases of it
|
? new NameGuidPair { Name = name, Id = albumArtists[0].Id }
|
||||||
if (string.IsNullOrEmpty(i))
|
: null)
|
||||||
{
|
.Where(item => item is not null)
|
||||||
return null;
|
.ToArray();
|
||||||
}
|
|
||||||
|
|
||||||
var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
|
|
||||||
{
|
|
||||||
EnableImages = false
|
|
||||||
});
|
|
||||||
if (artist is not null)
|
|
||||||
{
|
|
||||||
return new NameGuidPair
|
|
||||||
{
|
|
||||||
Name = artist.Name,
|
|
||||||
Id = artist.Id
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}).Where(i => i is not null).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add video info
|
// Add video info
|
||||||
@@ -1138,11 +1224,6 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.Chapters))
|
|
||||||
{
|
|
||||||
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.Trickplay))
|
if (options.ContainsField(ItemFields.Trickplay))
|
||||||
{
|
{
|
||||||
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
||||||
@@ -1156,6 +1237,11 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
dto.ExtraType = video.ExtraType;
|
dto.ExtraType = video.ExtraType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.ContainsField(ItemFields.Chapters))
|
||||||
|
{
|
||||||
|
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.MediaStreams))
|
if (options.ContainsField(ItemFields.MediaStreams))
|
||||||
{
|
{
|
||||||
// Add VideoInfo
|
// Add VideoInfo
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
<PackageReference Include="Microsoft.Data.Sqlite" />
|
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" />
|
||||||
@@ -39,7 +38,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The file system watchers.
|
/// The file system watchers.
|
||||||
@@ -47,19 +48,23 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <param name="configurationManager">The configuration manager.</param>
|
/// <param name="configurationManager">The configuration manager.</param>
|
||||||
/// <param name="fileSystem">The filesystem.</param>
|
/// <param name="fileSystem">The filesystem.</param>
|
||||||
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
|
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
|
||||||
|
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
|
||||||
public LibraryMonitor(
|
public LibraryMonitor(
|
||||||
ILogger<LibraryMonitor> logger,
|
ILogger<LibraryMonitor> logger,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IServerConfigurationManager configurationManager,
|
IServerConfigurationManager configurationManager,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IHostApplicationLifetime appLifetime)
|
IHostApplicationLifetime appLifetime,
|
||||||
|
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configurationManager = configurationManager;
|
_configurationManager = configurationManager;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
|
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
|
||||||
|
|
||||||
appLifetime.ApplicationStarted.Register(Start);
|
appLifetime.ApplicationStarted.Register(Start);
|
||||||
|
appLifetime.ApplicationStopping.Register(Stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -352,6 +357,12 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||||
|
if (_dotIgnoreIgnoreRule.ShouldIgnore(fileInfo, null))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
||||||
foreach (var i in _tempIgnoredPaths.Keys)
|
foreach (var i in _tempIgnoredPaths.Keys)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -497,8 +497,17 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual bool AreEqual(string path1, string path2)
|
public virtual bool AreEqual(string path1, string path2)
|
||||||
{
|
{
|
||||||
return Path.TrimEndingDirectorySeparator(path1).Equals(
|
if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2))
|
||||||
Path.TrimEndingDirectorySeparator(path2),
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized1 = Path.TrimEndingDirectorySeparator(path1);
|
||||||
|
var normalized2 = Path.TrimEndingDirectorySeparator(path2);
|
||||||
|
|
||||||
|
return string.Equals(
|
||||||
|
normalized1,
|
||||||
|
normalized2,
|
||||||
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,6 +586,12 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||||
{
|
{
|
||||||
|
if (!Directory.Exists(path))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Directory does not exist: {Path}", path);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
// On linux and macOS the search pattern is case-sensitive
|
// On linux and macOS the search pattern is case-sensitive
|
||||||
|
|||||||
@@ -267,22 +267,24 @@ namespace Emby.Server.Implementations.Images
|
|||||||
{
|
{
|
||||||
var image = item.GetImageInfo(type, 0);
|
var image = item.GetImageInfo(type, 0);
|
||||||
|
|
||||||
if (image is not null)
|
if (image is null)
|
||||||
{
|
{
|
||||||
if (!image.IsLocalFile)
|
return GetItemsWithImages(item).Count is not 0;
|
||||||
{
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
|
if (!image.IsLocalFile)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!HasChangedByDate(item, image))
|
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!HasChangedByDate(item, image))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Images
|
|||||||
includeItemTypes = new[] { BaseItemKind.Series };
|
includeItemTypes = new[] { BaseItemKind.Series };
|
||||||
break;
|
break;
|
||||||
case CollectionType.music:
|
case CollectionType.music:
|
||||||
includeItemTypes = new[] { BaseItemKind.MusicAlbum };
|
includeItemTypes = new[] { BaseItemKind.MusicArtist }; // Music albums usually don't have dedicated backdrops, so use artist instead
|
||||||
break;
|
break;
|
||||||
case CollectionType.musicvideos:
|
case CollectionType.musicvideos:
|
||||||
includeItemTypes = new[] { BaseItemKind.MusicVideo };
|
includeItemTypes = new[] { BaseItemKind.MusicVideo };
|
||||||
@@ -98,5 +98,11 @@ namespace Emby.Server.Implementations.Images
|
|||||||
|
|
||||||
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
|
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
|
||||||
|
{
|
||||||
|
var age = DateTime.UtcNow - image.DateModified;
|
||||||
|
return age.TotalDays > 7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using BitFaster.Caching.Lru;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.IO;
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
@@ -14,22 +17,36 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
|||||||
{
|
{
|
||||||
private static readonly bool IsWindows = OperatingSystem.IsWindows();
|
private static readonly bool IsWindows = OperatingSystem.IsWindows();
|
||||||
|
|
||||||
private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
|
private readonly FastConcurrentLru<string, IgnoreFileCacheEntry> _directoryCache;
|
||||||
{
|
private readonly FastConcurrentLru<string, ParsedIgnoreCacheEntry> _rulesCache;
|
||||||
for (var current = directory; current is not null; current = current.Parent)
|
|
||||||
{
|
|
||||||
var ignorePath = Path.Join(current.FullName, ".ignore");
|
|
||||||
if (File.Exists(ignorePath))
|
|
||||||
{
|
|
||||||
return new FileInfo(ignorePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DotIgnoreIgnoreRule"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public DotIgnoreIgnoreRule()
|
||||||
|
{
|
||||||
|
var cacheSize = Math.Max(100, Environment.ProcessorCount * 100);
|
||||||
|
_directoryCache = new FastConcurrentLru<string, IgnoreFileCacheEntry>(
|
||||||
|
Environment.ProcessorCount,
|
||||||
|
cacheSize,
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
_rulesCache = new FastConcurrentLru<string, ParsedIgnoreCacheEntry>(
|
||||||
|
Environment.ProcessorCount,
|
||||||
|
Math.Max(32, cacheSize / 4),
|
||||||
|
StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
|
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnoredInternal(fileInfo, parent);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the directory lookup cache. The parsed rules cache is not cleared
|
||||||
|
/// as it validates file modification time on each access.
|
||||||
|
/// </summary>
|
||||||
|
public void ClearDirectoryCache()
|
||||||
|
{
|
||||||
|
_directoryCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether or not the file is ignored.
|
/// Checks whether or not the file is ignored.
|
||||||
@@ -37,45 +54,86 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
|||||||
/// <param name="fileInfo">The file information.</param>
|
/// <param name="fileInfo">The file information.</param>
|
||||||
/// <param name="parent">The parent BaseItem.</param>
|
/// <param name="parent">The parent BaseItem.</param>
|
||||||
/// <returns>True if the file should be ignored.</returns>
|
/// <returns>True if the file should be ignored.</returns>
|
||||||
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
|
public bool IsIgnoredInternal(FileSystemMetadata fileInfo, BaseItem? parent)
|
||||||
{
|
{
|
||||||
var searchDirectory = fileInfo.IsDirectory
|
var searchDirectory = fileInfo.IsDirectory
|
||||||
? new DirectoryInfo(fileInfo.FullName)
|
? fileInfo.FullName
|
||||||
: new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
|
: Path.GetDirectoryName(fileInfo.FullName);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(searchDirectory.FullName))
|
if (string.IsNullOrEmpty(searchDirectory))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ignoreFile = FindIgnoreFile(searchDirectory);
|
var ignoreFile = FindIgnoreFileCached(searchDirectory);
|
||||||
if (ignoreFile is null)
|
if (ignoreFile is null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path in case the ignore files isn't a symlink and is empty
|
var parsedEntry = GetParsedRules(ignoreFile);
|
||||||
if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
|
if (parsedEntry is null)
|
||||||
|
{
|
||||||
|
// File was deleted after we cached the path - clear the directory cache entry and return false
|
||||||
|
_directoryCache.TryRemove(searchDirectory, out _);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty file means ignore everything
|
||||||
|
if (parsedEntry.IsEmpty)
|
||||||
{
|
{
|
||||||
// Ignore directory if we just have the file
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = GetFileContent(ignoreFile);
|
return parsedEntry.Rules.IsIgnored(GetPathToCheck(fileInfo.FullName, fileInfo.IsDirectory));
|
||||||
return string.IsNullOrWhiteSpace(content)
|
|
||||||
|| CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
|
/// <summary>
|
||||||
{
|
/// Checks whether a path should be ignored based on an array of ignore rules.
|
||||||
// If file has content, base ignoring off the content .gitignore-style rules
|
/// </summary>
|
||||||
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
/// <param name="path">The path to check.</param>
|
||||||
var ignore = new Ignore.Ignore();
|
/// <param name="rules">The array of ignore rules.</param>
|
||||||
ignore.Add(rules);
|
/// <param name="isDirectory">Whether the path is a directory.</param>
|
||||||
|
/// <returns>True if the path should be ignored.</returns>
|
||||||
|
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory)
|
||||||
|
=> CheckIgnoreRules(path, rules, isDirectory, IsWindows);
|
||||||
|
|
||||||
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
|
/// <summary>
|
||||||
// See https://github.com/jellyfin/jellyfin/issues/15484
|
/// Checks whether a path should be ignored based on an array of ignore rules.
|
||||||
var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
|
/// </summary>
|
||||||
|
/// <param name="path">The path to check.</param>
|
||||||
|
/// <param name="rules">The array of ignore rules.</param>
|
||||||
|
/// <param name="isDirectory">Whether the path is a directory.</param>
|
||||||
|
/// <param name="normalizePath">Whether to normalize backslashes to forward slashes (for Windows paths).</param>
|
||||||
|
/// <returns>True if the path should be ignored.</returns>
|
||||||
|
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath)
|
||||||
|
{
|
||||||
|
var ignore = new Ignore.Ignore();
|
||||||
|
|
||||||
|
// Add each rule individually to catch and skip invalid patterns
|
||||||
|
var validRulesAdded = 0;
|
||||||
|
foreach (var rule in rules)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ignore.Add(rule);
|
||||||
|
validRulesAdded++;
|
||||||
|
}
|
||||||
|
catch (RegexParseException)
|
||||||
|
{
|
||||||
|
// Ignore invalid patterns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no valid rules were added, fall back to ignoring everything (like an empty .ignore file)
|
||||||
|
if (validRulesAdded == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
|
||||||
|
// See https://github.com/jellyfin/jellyfin/issues/15484
|
||||||
|
var pathToCheck = normalizePath ? path.NormalizePath('/') : path;
|
||||||
|
|
||||||
// Add trailing slash for directories to match "folder/"
|
// Add trailing slash for directories to match "folder/"
|
||||||
if (isDirectory)
|
if (isDirectory)
|
||||||
@@ -86,11 +144,196 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
|||||||
return ignore.IsIgnored(pathToCheck);
|
return ignore.IsIgnored(pathToCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetFileContent(FileInfo ignoreFile)
|
private FileInfo? FindIgnoreFileCached(string directory)
|
||||||
{
|
{
|
||||||
ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
|
// Check if we have a cached result for this directory
|
||||||
return ignoreFile.Exists
|
if (_directoryCache.TryGet(directory, out var cached))
|
||||||
? File.ReadAllText(ignoreFile.FullName)
|
{
|
||||||
: string.Empty;
|
return cached.IgnoreFileDirectory is null
|
||||||
|
? null
|
||||||
|
: new FileInfo(Path.Join(cached.IgnoreFileDirectory, ".ignore"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectoryInfo startDir;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
startDir = new DirectoryInfo(directory);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk up the directory tree to find .ignore file using DirectoryInfo.Parent
|
||||||
|
var checkedDirs = new List<string> { directory };
|
||||||
|
|
||||||
|
for (var current = startDir; current is not null; current = current.Parent)
|
||||||
|
{
|
||||||
|
var currentPath = current.FullName;
|
||||||
|
|
||||||
|
// Check if this intermediate directory is cached
|
||||||
|
if (current != startDir && _directoryCache.TryGet(currentPath, out var parentCached))
|
||||||
|
{
|
||||||
|
// Cache the result for all directories we checked
|
||||||
|
var entry = new IgnoreFileCacheEntry(parentCached.IgnoreFileDirectory);
|
||||||
|
foreach (var dir in checkedDirs)
|
||||||
|
{
|
||||||
|
_directoryCache.AddOrUpdate(dir, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentCached.IgnoreFileDirectory is null
|
||||||
|
? null
|
||||||
|
: new FileInfo(Path.Join(parentCached.IgnoreFileDirectory, ".ignore"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var ignoreFile = new FileInfo(Path.Join(currentPath, ".ignore"));
|
||||||
|
if (ignoreFile.Exists)
|
||||||
|
{
|
||||||
|
// Cache for all directories we checked
|
||||||
|
var entry = new IgnoreFileCacheEntry(currentPath);
|
||||||
|
foreach (var dir in checkedDirs)
|
||||||
|
{
|
||||||
|
_directoryCache.AddOrUpdate(dir, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ignoreFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current != startDir)
|
||||||
|
{
|
||||||
|
checkedDirs.Add(currentPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No .ignore file found - cache null result for all directories
|
||||||
|
var nullEntry = new IgnoreFileCacheEntry((string?)null);
|
||||||
|
foreach (var dir in checkedDirs)
|
||||||
|
{
|
||||||
|
_directoryCache.AddOrUpdate(dir, nullEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ParsedIgnoreCacheEntry? GetParsedRules(FileInfo ignoreFile)
|
||||||
|
{
|
||||||
|
if (!ignoreFile.Exists)
|
||||||
|
{
|
||||||
|
_rulesCache.TryRemove(ignoreFile.FullName, out _);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastModified = ignoreFile.LastWriteTimeUtc;
|
||||||
|
var fileLength = ignoreFile.Length;
|
||||||
|
var key = ignoreFile.FullName;
|
||||||
|
|
||||||
|
// Check cache
|
||||||
|
if (_rulesCache.TryGet(key, out var cached))
|
||||||
|
{
|
||||||
|
if (cached.FileLastModified == lastModified && cached.FileLength == fileLength)
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stale - need to reparse
|
||||||
|
_rulesCache.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the file
|
||||||
|
var parsedEntry = ParseIgnoreFile(ignoreFile, lastModified, fileLength);
|
||||||
|
_rulesCache.AddOrUpdate(key, parsedEntry);
|
||||||
|
return parsedEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ParsedIgnoreCacheEntry ParseIgnoreFile(FileInfo ignoreFile, DateTime lastModified, long fileLength)
|
||||||
|
{
|
||||||
|
if (ignoreFile.LinkTarget is null && fileLength == 0)
|
||||||
|
{
|
||||||
|
return new ParsedIgnoreCacheEntry
|
||||||
|
{
|
||||||
|
Rules = new Ignore.Ignore(),
|
||||||
|
FileLastModified = lastModified,
|
||||||
|
FileLength = fileLength,
|
||||||
|
IsEmpty = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve symlinks
|
||||||
|
var resolvedFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
|
||||||
|
if (!resolvedFile.Exists)
|
||||||
|
{
|
||||||
|
return new ParsedIgnoreCacheEntry
|
||||||
|
{
|
||||||
|
Rules = new Ignore.Ignore(),
|
||||||
|
FileLastModified = lastModified,
|
||||||
|
FileLength = fileLength,
|
||||||
|
IsEmpty = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(resolvedFile.FullName);
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
{
|
||||||
|
return new ParsedIgnoreCacheEntry
|
||||||
|
{
|
||||||
|
Rules = new Ignore.Ignore(),
|
||||||
|
FileLastModified = lastModified,
|
||||||
|
FileLength = fileLength,
|
||||||
|
IsEmpty = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules = content.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
var ignore = new Ignore.Ignore();
|
||||||
|
var validRulesAdded = 0;
|
||||||
|
|
||||||
|
foreach (var rule in rules)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ignore.Add(rule);
|
||||||
|
validRulesAdded++;
|
||||||
|
}
|
||||||
|
catch (RegexParseException)
|
||||||
|
{
|
||||||
|
// Ignore invalid patterns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No valid rules means treat as empty (ignore all)
|
||||||
|
return new ParsedIgnoreCacheEntry
|
||||||
|
{
|
||||||
|
Rules = ignore,
|
||||||
|
FileLastModified = lastModified,
|
||||||
|
FileLength = fileLength,
|
||||||
|
IsEmpty = validRulesAdded == 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPathToCheck(string path, bool isDirectory)
|
||||||
|
{
|
||||||
|
// Normalize Windows paths
|
||||||
|
var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
|
||||||
|
|
||||||
|
// Add trailing slash for directories to match "folder/"
|
||||||
|
if (isDirectory)
|
||||||
|
{
|
||||||
|
pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathToCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct IgnoreFileCacheEntry(string? IgnoreFileDirectory);
|
||||||
|
|
||||||
|
private sealed class ParsedIgnoreCacheEntry
|
||||||
|
{
|
||||||
|
public required Ignore.Ignore Rules { get; init; }
|
||||||
|
|
||||||
|
public required DateTime FileLastModified { get; init; }
|
||||||
|
|
||||||
|
public required long FileLength { get; init; }
|
||||||
|
|
||||||
|
public required bool IsEmpty { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ namespace Emby.Server.Implementations.Library
|
|||||||
"**/*.sample.?????",
|
"**/*.sample.?????",
|
||||||
"**/sample/*",
|
"**/sample/*",
|
||||||
|
|
||||||
|
// Avoid adding Hungarian sample files
|
||||||
|
// https://github.com/jellyfin/jellyfin/issues/16237
|
||||||
|
"**/minta.?",
|
||||||
|
"**/minta.??",
|
||||||
|
"**/minta.???", // Matches minta.mkv
|
||||||
|
"**/minta.????", // Matches minta.webm
|
||||||
|
"**/minta.?????",
|
||||||
|
"**/*.minta.?",
|
||||||
|
"**/*.minta.??",
|
||||||
|
"**/*.minta.???",
|
||||||
|
"**/*.minta.????",
|
||||||
|
"**/*.minta.?????",
|
||||||
|
"**/minta/*",
|
||||||
|
|
||||||
// Directories
|
// Directories
|
||||||
"**/metadata/**",
|
"**/metadata/**",
|
||||||
"**/metadata",
|
"**/metadata",
|
||||||
@@ -50,6 +64,10 @@ namespace Emby.Server.Implementations.Library
|
|||||||
"**/lost+found",
|
"**/lost+found",
|
||||||
"**/subs/**",
|
"**/subs/**",
|
||||||
"**/subs",
|
"**/subs",
|
||||||
|
"**/.snapshots/**",
|
||||||
|
"**/.snapshots",
|
||||||
|
"**/.snapshot/**",
|
||||||
|
"**/.snapshot",
|
||||||
|
|
||||||
// Trickplay files
|
// Trickplay files
|
||||||
"**/*.trickplay",
|
"**/*.trickplay",
|
||||||
|
|||||||
@@ -30,18 +30,17 @@ using MediaBrowser.Controller.Drawing;
|
|||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.IO;
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.MediaSegments;
|
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
using MediaBrowser.Controller.Sorting;
|
using MediaBrowser.Controller.Sorting;
|
||||||
using MediaBrowser.Controller.Trickplay;
|
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
using MediaBrowser.Model.Drawing;
|
using MediaBrowser.Model.Drawing;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@@ -77,12 +76,17 @@ namespace Emby.Server.Implementations.Library
|
|||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly IItemRepository _itemRepository;
|
private readonly IItemRepository _itemRepository;
|
||||||
|
private readonly IItemPersistenceService _persistenceService;
|
||||||
|
private readonly INextUpService _nextUpService;
|
||||||
|
private readonly IItemCountService _countService;
|
||||||
|
private readonly ILinkedChildrenService _linkedChildrenService;
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
private readonly NamingOptions _namingOptions;
|
private readonly NamingOptions _namingOptions;
|
||||||
private readonly IPeopleRepository _peopleRepository;
|
private readonly IPeopleRepository _peopleRepository;
|
||||||
private readonly ExtraResolver _extraResolver;
|
private readonly ExtraResolver _extraResolver;
|
||||||
private readonly IPathManager _pathManager;
|
private readonly IPathManager _pathManager;
|
||||||
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
|
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
|
||||||
|
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _root folder sync lock.
|
/// The _root folder sync lock.
|
||||||
@@ -115,11 +119,16 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// <param name="userViewManagerFactory">The user view manager.</param>
|
/// <param name="userViewManagerFactory">The user view manager.</param>
|
||||||
/// <param name="mediaEncoder">The media encoder.</param>
|
/// <param name="mediaEncoder">The media encoder.</param>
|
||||||
/// <param name="itemRepository">The item repository.</param>
|
/// <param name="itemRepository">The item repository.</param>
|
||||||
|
/// <param name="persistenceService">The item persistence service.</param>
|
||||||
|
/// <param name="nextUpService">The next up service.</param>
|
||||||
|
/// <param name="countService">The item count service.</param>
|
||||||
|
/// <param name="linkedChildrenService">The linked children service.</param>
|
||||||
/// <param name="imageProcessor">The image processor.</param>
|
/// <param name="imageProcessor">The image processor.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="directoryService">The directory service.</param>
|
/// <param name="directoryService">The directory service.</param>
|
||||||
/// <param name="peopleRepository">The people repository.</param>
|
/// <param name="peopleRepository">The people repository.</param>
|
||||||
/// <param name="pathManager">The path manager.</param>
|
/// <param name="pathManager">The path manager.</param>
|
||||||
|
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
|
||||||
public LibraryManager(
|
public LibraryManager(
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -133,11 +142,16 @@ namespace Emby.Server.Implementations.Library
|
|||||||
Lazy<IUserViewManager> userViewManagerFactory,
|
Lazy<IUserViewManager> userViewManagerFactory,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IItemRepository itemRepository,
|
IItemRepository itemRepository,
|
||||||
|
IItemPersistenceService persistenceService,
|
||||||
|
INextUpService nextUpService,
|
||||||
|
IItemCountService countService,
|
||||||
|
ILinkedChildrenService linkedChildrenService,
|
||||||
IImageProcessor imageProcessor,
|
IImageProcessor imageProcessor,
|
||||||
NamingOptions namingOptions,
|
NamingOptions namingOptions,
|
||||||
IDirectoryService directoryService,
|
IDirectoryService directoryService,
|
||||||
IPeopleRepository peopleRepository,
|
IPeopleRepository peopleRepository,
|
||||||
IPathManager pathManager)
|
IPathManager pathManager,
|
||||||
|
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
|
||||||
{
|
{
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_logger = loggerFactory.CreateLogger<LibraryManager>();
|
_logger = loggerFactory.CreateLogger<LibraryManager>();
|
||||||
@@ -151,6 +165,10 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_userViewManagerFactory = userViewManagerFactory;
|
_userViewManagerFactory = userViewManagerFactory;
|
||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_itemRepository = itemRepository;
|
_itemRepository = itemRepository;
|
||||||
|
_persistenceService = persistenceService;
|
||||||
|
_nextUpService = nextUpService;
|
||||||
|
_countService = countService;
|
||||||
|
_linkedChildrenService = linkedChildrenService;
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
|
|
||||||
_cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
|
_cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
|
||||||
@@ -158,6 +176,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_namingOptions = namingOptions;
|
_namingOptions = namingOptions;
|
||||||
_peopleRepository = peopleRepository;
|
_peopleRepository = peopleRepository;
|
||||||
_pathManager = pathManager;
|
_pathManager = pathManager;
|
||||||
|
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
|
||||||
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
|
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
|
||||||
|
|
||||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||||
@@ -327,9 +346,17 @@ namespace Emby.Server.Implementations.Library
|
|||||||
DeleteItem(item, options, parent, notifyParentItem);
|
DeleteItem(item, options, parent, notifyParentItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items)
|
public void DeleteItemsUnsafeFast(IReadOnlyCollection<BaseItem> items, bool deleteSourceFiles = false)
|
||||||
{
|
{
|
||||||
var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray();
|
if (items.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathMaps = items.Select(e =>
|
||||||
|
(Item: e,
|
||||||
|
InternalPath: GetInternalMetadataPaths(e),
|
||||||
|
DeletePaths: deleteSourceFiles ? e.GetDeletePaths() : [])).ToArray();
|
||||||
|
|
||||||
foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
|
foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
|
||||||
{
|
{
|
||||||
@@ -363,7 +390,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
|
_persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
|
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
|
||||||
@@ -406,6 +433,99 @@ namespace Emby.Server.Implementations.Library
|
|||||||
item.Id);
|
item.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If deleting a primary version video, clear PrimaryVersionId from alternate versions
|
||||||
|
// OwnerId check: items with OwnerId set are alternate versions or extras, not primaries
|
||||||
|
if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty())
|
||||||
|
{
|
||||||
|
var localAlternateIds = GetLocalAlternateVersionIds(video).ToHashSet();
|
||||||
|
var allAlternateVersions = localAlternateIds
|
||||||
|
.Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
|
||||||
|
.Distinct()
|
||||||
|
.Select(id => GetItemById(id))
|
||||||
|
.OfType<Video>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Partition alternates by whether their files still exist on disk
|
||||||
|
var alternateVersions = new List<Video>();
|
||||||
|
var missingAlternates = new List<Video>();
|
||||||
|
foreach (var alt in allAlternateVersions)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(alt.Path) && !_fileSystem.FileExists(alt.Path))
|
||||||
|
{
|
||||||
|
missingAlternates.Add(alt);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
alternateVersions.Add(alt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete alternates whose files no longer exist to avoid ghost items.
|
||||||
|
// Clear PrimaryVersionId first so DeleteItem doesn't try to update the primary being deleted.
|
||||||
|
foreach (var missing in missingAlternates)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Deleting missing alternate version {Name} ({Path})",
|
||||||
|
missing.Name ?? "Unknown name",
|
||||||
|
missing.Path ?? string.Empty);
|
||||||
|
missing.SetPrimaryVersionId(null);
|
||||||
|
missing.OwnerId = Guid.Empty;
|
||||||
|
missing.LocalAlternateVersions = [];
|
||||||
|
missing.LinkedAlternateVersions = [];
|
||||||
|
DeleteItem(missing, new DeleteOptions { DeleteFileLocation = false }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alternateVersions.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
|
||||||
|
alternateVersions.Count,
|
||||||
|
item.Name ?? "Unknown name");
|
||||||
|
|
||||||
|
// Promote the first alternate version to be the new primary
|
||||||
|
var newPrimary = alternateVersions[0];
|
||||||
|
newPrimary.SetPrimaryVersionId(null);
|
||||||
|
newPrimary.OwnerId = Guid.Empty;
|
||||||
|
|
||||||
|
// Transfer alternate version arrays from old primary to new primary
|
||||||
|
// so UpdateToRepositoryAsync creates correct LinkedChildren entries
|
||||||
|
newPrimary.LocalAlternateVersions = video.LocalAlternateVersions
|
||||||
|
.Where(p => !string.Equals(p, newPrimary.Path, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToArray();
|
||||||
|
newPrimary.LinkedAlternateVersions = video.LinkedAlternateVersions
|
||||||
|
.Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(newPrimary.Id))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
// Re-route playlist/collection references from deleted primary to new primary
|
||||||
|
RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
// Update remaining alternates to point to new primary
|
||||||
|
foreach (var alternate in alternateVersions.Skip(1))
|
||||||
|
{
|
||||||
|
alternate.SetPrimaryVersionId(newPrimary.Id);
|
||||||
|
// Only set OwnerId for local alternates; linked alternates are independent items
|
||||||
|
alternate.OwnerId = localAlternateIds.Contains(alternate.Id) ? newPrimary.Id : Guid.Empty;
|
||||||
|
alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue)
|
||||||
|
{
|
||||||
|
// If deleting an alternate version, re-route references to its primary
|
||||||
|
RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
// Remove deleted alternate from primary's LinkedAlternateVersions
|
||||||
|
if (GetItemById(alternateVideo.PrimaryVersionId.Value) is Video primaryVideo)
|
||||||
|
{
|
||||||
|
primaryVideo.LinkedAlternateVersions = primaryVideo.LinkedAlternateVersions
|
||||||
|
.Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(alternateVideo.Id))
|
||||||
|
.ToArray();
|
||||||
|
primaryVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var children = item.IsFolder
|
var children = item.IsFolder
|
||||||
? ((Folder)item).GetRecursiveChildren(false)
|
? ((Folder)item).GetRecursiveChildren(false)
|
||||||
: [];
|
: [];
|
||||||
@@ -450,7 +570,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
item.SetParent(null);
|
item.SetParent(null);
|
||||||
|
|
||||||
_itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
|
_persistenceService.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
|
||||||
_cache.TryRemove(item.Id, out _);
|
_cache.TryRemove(item.Id, out _);
|
||||||
foreach (var child in children)
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
@@ -576,6 +696,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
// Trickplay
|
// Trickplay
|
||||||
list.Add(_pathManager.GetTrickplayDirectory(video));
|
list.Add(_pathManager.GetTrickplayDirectory(video));
|
||||||
|
|
||||||
|
// Chapter Images
|
||||||
|
list.Add(_pathManager.GetChapterImageFolderPath(video));
|
||||||
|
|
||||||
// Subtitles and attachments
|
// Subtitles and attachments
|
||||||
foreach (var mediaSource in item.GetMediaSources(false))
|
foreach (var mediaSource in item.GetMediaSources(false))
|
||||||
{
|
{
|
||||||
@@ -657,8 +780,63 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return key.GetMD5();
|
return key.GetMD5();
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null)
|
public BaseItem? ResolvePath(
|
||||||
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
|
FileSystemMetadata fileInfo,
|
||||||
|
Folder? parent = null,
|
||||||
|
IDirectoryService? directoryService = null,
|
||||||
|
CollectionType? collectionType = null)
|
||||||
|
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? collectionType)
|
||||||
|
{
|
||||||
|
// Clean up any existing item saved with wrong type (e.g. Video instead of Movie).
|
||||||
|
// This happens when items were previously resolved without proper type context
|
||||||
|
// in mixed-content libraries where collectionType is null.
|
||||||
|
var expectedId = GetNewItemId(path, expectedVideoType);
|
||||||
|
if (expectedVideoType != typeof(Video))
|
||||||
|
{
|
||||||
|
var wrongTypeId = GetNewItemId(path, typeof(Video));
|
||||||
|
if (!wrongTypeId.Equals(expectedId) && GetItemById(wrongTypeId) is Video wrongTypeItem)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Removing alternate version with wrong type {WrongType}, expected {ExpectedType}: {Path}",
|
||||||
|
wrongTypeItem.GetType().Name,
|
||||||
|
expectedVideoType.Name,
|
||||||
|
path);
|
||||||
|
DeleteItem(wrongTypeItem, new DeleteOptions { DeleteFileLocation = false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolved = ResolvePath(
|
||||||
|
_fileSystem.GetFileSystemInfo(path),
|
||||||
|
parent,
|
||||||
|
collectionType: collectionType) as Video;
|
||||||
|
|
||||||
|
if (resolved is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the alternate version has the same concrete type as the primary video.
|
||||||
|
// ResolvePath may return a generic Video for files in mixed-content libraries
|
||||||
|
// where collectionType is null, even though the primary is a Movie/Episode/etc.
|
||||||
|
if (resolved.GetType() != expectedVideoType)
|
||||||
|
{
|
||||||
|
if (Activator.CreateInstance(expectedVideoType) is Video correctVideo)
|
||||||
|
{
|
||||||
|
correctVideo.Path = resolved.Path;
|
||||||
|
correctVideo.Name = resolved.Name;
|
||||||
|
correctVideo.VideoType = resolved.VideoType;
|
||||||
|
correctVideo.ProductionYear = resolved.ProductionYear;
|
||||||
|
correctVideo.ExtraType = resolved.ExtraType;
|
||||||
|
resolved = correctVideo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved.Id = expectedId;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
private BaseItem? ResolvePath(
|
private BaseItem? ResolvePath(
|
||||||
FileSystemMetadata fileInfo,
|
FileSystemMetadata fileInfo,
|
||||||
@@ -1041,7 +1219,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
|
public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
|
||||||
{
|
{
|
||||||
return _itemRepository.FindArtists(names);
|
return _linkedChildrenService.FindArtists(names);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MusicArtist GetArtist(string name, DtoOptions options)
|
public MusicArtist GetArtist(string name, DtoOptions options)
|
||||||
@@ -1058,6 +1236,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
IncludeItemTypes = [BaseItemKind.MusicArtist],
|
IncludeItemTypes = [BaseItemKind.MusicArtist],
|
||||||
Name = name,
|
Name = name,
|
||||||
|
UseRawName = true,
|
||||||
DtoOptions = options
|
DtoOptions = options
|
||||||
}).Cast<MusicArtist>()
|
}).Cast<MusicArtist>()
|
||||||
.OrderBy(i => i.IsAccessedByName ? 1 : 0)
|
.OrderBy(i => i.IsAccessedByName ? 1 : 0)
|
||||||
@@ -1130,6 +1309,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
IsScanRunning = true;
|
IsScanRunning = true;
|
||||||
|
ClearIgnoreRuleCache();
|
||||||
LibraryMonitor.Stop();
|
LibraryMonitor.Stop();
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -1138,6 +1318,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
ClearIgnoreRuleCache();
|
||||||
LibraryMonitor.Start();
|
LibraryMonitor.Start();
|
||||||
IsScanRunning = false;
|
IsScanRunning = false;
|
||||||
}
|
}
|
||||||
@@ -1145,6 +1326,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
||||||
{
|
{
|
||||||
|
ClearIgnoreRuleCache();
|
||||||
RootFolder.Children = null;
|
RootFolder.Children = null;
|
||||||
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -1185,8 +1367,16 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
if (toDelete.Count > 0)
|
if (toDelete.Count > 0)
|
||||||
{
|
{
|
||||||
_itemRepository.DeleteItem(toDelete.ToArray());
|
_persistenceService.DeleteItem(toDelete.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClearIgnoreRuleCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void ClearIgnoreRuleCache()
|
||||||
|
{
|
||||||
|
_dotIgnoreIgnoreRule.ClearDirectoryCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
|
private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
@@ -1261,7 +1451,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
progress.Report(percent * 100);
|
progress.Report(percent * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.UpdateInheritedValues();
|
_persistenceService.UpdateInheritedValues();
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
@@ -1420,14 +1610,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
AddUserToQuery(query, query.User, allowExternalContent);
|
AddUserToQuery(query, query.User, allowExternalContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemList = _itemRepository.GetItemList(query);
|
return _itemRepository.GetItemList(query);
|
||||||
var user = query.User;
|
|
||||||
if (user is not null)
|
|
||||||
{
|
|
||||||
return itemList.Where(i => i.IsVisible(user)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return itemList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
|
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
|
||||||
@@ -1451,7 +1634,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
AddUserToQuery(query, query.User);
|
AddUserToQuery(query, query.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _itemRepository.GetCount(query);
|
return _countService.GetCount(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ItemCounts GetItemCounts(InternalItemsQuery query)
|
public ItemCounts GetItemCounts(InternalItemsQuery query)
|
||||||
@@ -1470,7 +1653,30 @@ namespace Emby.Server.Implementations.Library
|
|||||||
AddUserToQuery(query, query.User);
|
AddUserToQuery(query, query.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _itemRepository.GetItemCounts(query);
|
return _countService.GetItemCounts(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? user)
|
||||||
|
{
|
||||||
|
var query = new InternalItemsQuery(user);
|
||||||
|
if (user is not null)
|
||||||
|
{
|
||||||
|
AddUserToQuery(query, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
|
||||||
|
{
|
||||||
|
return _countService.GetChildCountBatch(parentIds, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user)
|
||||||
|
{
|
||||||
|
return _countService.GetPlayedAndTotalCountBatch(folderIds, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
|
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
|
||||||
@@ -1515,7 +1721,17 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
|
return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisodesBatch(
|
||||||
|
InternalItemsQuery query,
|
||||||
|
IReadOnlyList<string> seriesKeys,
|
||||||
|
bool includeSpecials,
|
||||||
|
bool includeWatchedForRewatching)
|
||||||
|
{
|
||||||
|
return _nextUpService.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
|
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
|
||||||
@@ -1682,6 +1898,25 @@ namespace Emby.Server.Implementations.Library
|
|||||||
query.TopParentIds = [Guid.NewGuid()];
|
query.TopParentIds = [Guid.NewGuid()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (parents.Count == 1 && parents.First() is Folder folder
|
||||||
|
&& (folder is Playlist || folder is BoxSet)
|
||||||
|
&& folder.LinkedChildren.Length > 0)
|
||||||
|
{
|
||||||
|
// Playlists and BoxSets store their contents in LinkedChildren and never
|
||||||
|
// populate AncestorIds for those items, so a recursive AncestorIds query
|
||||||
|
// would return zero rows. Resolve to the linked child IDs up front and
|
||||||
|
// route through the existing indexed ItemIds filter.
|
||||||
|
query.ItemIds = folder.LinkedChildren
|
||||||
|
.Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
|
||||||
|
.Select(lc => lc.ItemId!.Value)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Empty linked-children should still return empty rather than scanning everything.
|
||||||
|
if (query.ItemIds.Length == 0)
|
||||||
|
{
|
||||||
|
query.ItemIds = [Guid.NewGuid()];
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// We need to be able to query from any arbitrary ancestor up the tree
|
// We need to be able to query from any arbitrary ancestor up the tree
|
||||||
@@ -1699,6 +1934,11 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
|
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
|
||||||
{
|
{
|
||||||
|
if (query.User is null)
|
||||||
|
{
|
||||||
|
query.SetUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
if (query.AncestorIds.Length == 0 &&
|
if (query.AncestorIds.Length == 0 &&
|
||||||
query.ParentId.IsEmpty() &&
|
query.ParentId.IsEmpty() &&
|
||||||
query.ChannelIds.Count == 0 &&
|
query.ChannelIds.Count == 0 &&
|
||||||
@@ -1724,6 +1964,15 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void ConfigureUserAccess(InternalItemsQuery query, User user)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
|
||||||
|
AddUserToQuery(query, user);
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
|
private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
|
||||||
{
|
{
|
||||||
if (item is UserView view)
|
if (item is UserView view)
|
||||||
@@ -1888,6 +2137,44 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return video;
|
return video;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(video);
|
||||||
|
|
||||||
|
var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LocalAlternateVersion);
|
||||||
|
if (linkedIds.Count > 0)
|
||||||
|
{
|
||||||
|
return linkedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(video);
|
||||||
|
|
||||||
|
var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LinkedAlternateVersion);
|
||||||
|
if (linkedIds.Count > 0)
|
||||||
|
{
|
||||||
|
return linkedIds
|
||||||
|
.Select(id => GetItemById(id))
|
||||||
|
.Where(i => i is not null)
|
||||||
|
.OfType<Video>()
|
||||||
|
.OrderBy(i => i.SortName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType childType)
|
||||||
|
{
|
||||||
|
_linkedChildrenService.UpsertLinkedChild(parentId, childId, childType);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
|
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
|
||||||
{
|
{
|
||||||
@@ -1992,9 +2279,44 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
|
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_itemRepository.SaveItems(items, cancellationToken);
|
// Resolve and add any local alternate version items that don't exist yet
|
||||||
|
// This ensures they exist in the database when LinkedChildren are processed
|
||||||
|
var allItems = new List<BaseItem>(items);
|
||||||
|
var parentFolder = parent as Folder;
|
||||||
|
var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null;
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item is Video video && video.LocalAlternateVersions.Length > 0)
|
||||||
|
{
|
||||||
|
var videoType = video.GetType();
|
||||||
|
foreach (var path in video.LocalAlternateVersions)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the primary video's type for ID calculation to ensure consistency
|
||||||
|
var altId = GetNewItemId(path, videoType);
|
||||||
|
if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
|
||||||
|
{
|
||||||
|
// Alternate version doesn't exist, resolve and create it
|
||||||
|
// ensuring it has the same type as the primary video
|
||||||
|
var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
|
||||||
|
if (altVideo is not null)
|
||||||
|
{
|
||||||
|
altVideo.OwnerId = video.Id;
|
||||||
|
altVideo.SetPrimaryVersionId(video.Id);
|
||||||
|
allItems.Add(altVideo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistenceService.SaveItems(allItems, cancellationToken);
|
||||||
|
|
||||||
|
foreach (var item in allItems)
|
||||||
{
|
{
|
||||||
RegisterItem(item);
|
RegisterItem(item);
|
||||||
}
|
}
|
||||||
@@ -2143,7 +2465,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
item.ValidateImages();
|
item.ValidateImages();
|
||||||
|
|
||||||
_itemRepository.SaveImages(item);
|
await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false);
|
||||||
|
|
||||||
RegisterItem(item);
|
RegisterItem(item);
|
||||||
}
|
}
|
||||||
@@ -2160,7 +2482,50 @@ namespace Emby.Server.Implementations.Library
|
|||||||
item.DateLastSaved = DateTime.UtcNow;
|
item.DateLastSaved = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.SaveItems(items, cancellationToken);
|
// Resolve and add any local alternate version items that don't exist yet
|
||||||
|
// This ensures they exist in the database when LinkedChildren are processed
|
||||||
|
var allItems = new List<BaseItem>(items);
|
||||||
|
var parentFolder = parent as Folder;
|
||||||
|
var parentCollectionType = GetTopFolderContentType(parent);
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item is Video video && video.LocalAlternateVersions.Length > 0)
|
||||||
|
{
|
||||||
|
var videoType = video.GetType();
|
||||||
|
foreach (var path in video.LocalAlternateVersions)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the primary video's type for ID calculation to ensure consistency
|
||||||
|
var altId = GetNewItemId(path, videoType);
|
||||||
|
if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
|
||||||
|
{
|
||||||
|
// Alternate version doesn't exist, resolve and create it
|
||||||
|
// ensuring it has the same type as the primary video
|
||||||
|
var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
|
||||||
|
if (altVideo is not null)
|
||||||
|
{
|
||||||
|
altVideo.OwnerId = video.Id;
|
||||||
|
altVideo.SetPrimaryVersionId(video.Id);
|
||||||
|
allItems.Add(altVideo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistenceService.SaveItems(allItems, cancellationToken);
|
||||||
|
|
||||||
|
foreach (var item in allItems)
|
||||||
|
{
|
||||||
|
if (!items.Contains(item))
|
||||||
|
{
|
||||||
|
RegisterItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (parent is Folder folder)
|
if (parent is Folder folder)
|
||||||
{
|
{
|
||||||
@@ -2201,6 +2566,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||||
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
|
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
||||||
{
|
{
|
||||||
if (item.IsFileProtocol)
|
if (item.IsFileProtocol)
|
||||||
@@ -2282,7 +2653,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
if (item is null)
|
if (item is null)
|
||||||
{
|
{
|
||||||
return new List<Folder>();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetCollectionFoldersInternal(item, allUserRootChildren);
|
return GetCollectionFoldersInternal(item, allUserRootChildren);
|
||||||
@@ -2826,8 +3197,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||||
{
|
{
|
||||||
// Apply .ignore rules
|
// Apply .ignore rules
|
||||||
var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList();
|
var filtered = fileSystemChildren.Where(c => !_dotIgnoreIgnoreRule.ShouldIgnore(c, owner)).ToList();
|
||||||
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
|
var isFolder = owner.IsFolder || (owner is Video video && (video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd));
|
||||||
|
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
|
||||||
if (ownerVideoInfo is null)
|
if (ownerVideoInfo is null)
|
||||||
{
|
{
|
||||||
yield break;
|
yield break;
|
||||||
@@ -2889,10 +3261,16 @@ namespace Emby.Server.Implementations.Library
|
|||||||
extra.ExtraType = extraType;
|
extra.ExtraType = extraType;
|
||||||
}
|
}
|
||||||
|
|
||||||
extra.ParentId = Guid.Empty;
|
// Only return items that are actual extras (have ExtraType set)
|
||||||
extra.OwnerId = owner.Id;
|
// Note: OwnerId and ParentId are set by RefreshExtras, not here,
|
||||||
extra.IsInMixedFolder = isInMixedFolder;
|
// so that RefreshExtras can detect when they need updating and set ForceSave.
|
||||||
return extra;
|
if (extra.ExtraType is not null)
|
||||||
|
{
|
||||||
|
extra.IsInMixedFolder = isInMixedFolder;
|
||||||
|
return extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2911,7 +3289,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
|
public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
|
||||||
{
|
{
|
||||||
return _peopleRepository.GetPeople(query);
|
return _peopleRepository.GetPeople(query).Items;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
|
public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
|
||||||
@@ -2932,24 +3310,33 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
|
public QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query)
|
||||||
{
|
{
|
||||||
return _peopleRepository.GetPeopleNames(query)
|
var queryResult = _peopleRepository.GetPeople(query);
|
||||||
.Select(i =>
|
var baseItems = queryResult.Items.Select(i =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return GetPerson(i.Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "error retrieving BaseItem for person: {0}", i.Name);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.Where(i => i is not null)
|
||||||
|
.Where(i => query.User is null || i!.IsVisible(query.User))
|
||||||
|
.OfType<BaseItem>()
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
|
||||||
|
return new QueryResult<BaseItem>
|
||||||
{
|
{
|
||||||
try
|
StartIndex = queryResult.StartIndex,
|
||||||
{
|
TotalRecordCount = queryResult.TotalRecordCount,
|
||||||
return GetPerson(i);
|
Items = baseItems,
|
||||||
}
|
};
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error getting person");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.Where(i => i is not null)
|
|
||||||
.Where(i => query.User is null || i!.IsVisible(query.User))
|
|
||||||
.ToList()!; // null values are filtered out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
|
public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
|
||||||
@@ -3194,19 +3581,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||||
|
|
||||||
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
|
CreateShortcut(virtualFolderPath, pathInfo);
|
||||||
|
|
||||||
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
|
||||||
|
|
||||||
while (File.Exists(lnk))
|
|
||||||
{
|
|
||||||
shortcutFilename += "1";
|
|
||||||
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
|
||||||
}
|
|
||||||
|
|
||||||
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
|
|
||||||
|
|
||||||
RemoveContentTypeOverrides(path);
|
|
||||||
|
|
||||||
if (saveLibraryOptions)
|
if (saveLibraryOptions)
|
||||||
{
|
{
|
||||||
@@ -3371,5 +3746,59 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
return item is UserRootFolder || item.IsVisibleStandalone(user);
|
return item is UserRootFolder || item.IsVisibleStandalone(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
|
||||||
|
{
|
||||||
|
var path = pathInfo.Path;
|
||||||
|
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||||
|
|
||||||
|
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||||
|
|
||||||
|
while (File.Exists(lnk))
|
||||||
|
{
|
||||||
|
shortcutFilename += "1";
|
||||||
|
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
|
||||||
|
RemoveContentTypeOverrides(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId)
|
||||||
|
{
|
||||||
|
var affectedParentIds = _linkedChildrenService.RerouteLinkedChildren(fromChildId, toChildId);
|
||||||
|
|
||||||
|
// Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents
|
||||||
|
foreach (var parentId in affectedParentIds)
|
||||||
|
{
|
||||||
|
if (GetItemById(parentId) is Folder parent)
|
||||||
|
{
|
||||||
|
foreach (var lc in parent.LinkedChildren)
|
||||||
|
{
|
||||||
|
if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId))
|
||||||
|
{
|
||||||
|
lc.ItemId = toChildId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query)
|
||||||
|
{
|
||||||
|
if (query.User is not null)
|
||||||
|
{
|
||||||
|
AddUserToQuery(query, query.User);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetTopParentOrAncestorIds(query);
|
||||||
|
return _itemRepository.GetQueryFiltersLegacy(query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,15 +37,25 @@ namespace Emby.Server.Implementations.Library
|
|||||||
while (attributeIndex > -1 && attributeIndex < maxIndex)
|
while (attributeIndex > -1 && attributeIndex < maxIndex)
|
||||||
{
|
{
|
||||||
var attributeEnd = attributeIndex + attribute.Length;
|
var attributeEnd = attributeIndex + attribute.Length;
|
||||||
if (attributeIndex > 0
|
if (attributeIndex > 0)
|
||||||
&& str[attributeIndex - 1] == '['
|
|
||||||
&& (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
|
|
||||||
{
|
{
|
||||||
var closingIndex = str[attributeEnd..].IndexOf(']');
|
var attributeOpener = str[attributeIndex - 1];
|
||||||
// Must be at least 1 character before the closing bracket.
|
var attributeCloser = attributeOpener switch
|
||||||
if (closingIndex > 1)
|
|
||||||
{
|
{
|
||||||
return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
|
'[' => ']',
|
||||||
|
'(' => ')',
|
||||||
|
'{' => '}',
|
||||||
|
_ => '\0'
|
||||||
|
};
|
||||||
|
if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
|
||||||
|
{
|
||||||
|
var closingIndex = str[attributeEnd..].IndexOf(attributeCloser);
|
||||||
|
|
||||||
|
// Must be at least 1 character before the closing bracket.
|
||||||
|
if (closingIndex > 1)
|
||||||
|
{
|
||||||
|
return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Emby.Naming.Book;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Resolvers.Books
|
namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||||
{
|
{
|
||||||
@@ -35,17 +35,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||||||
|
|
||||||
var extension = Path.GetExtension(args.Path.AsSpan());
|
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||||
|
|
||||||
if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
if (!_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// It's a book
|
return null;
|
||||||
return new Book
|
|
||||||
{
|
|
||||||
Path = args.Path,
|
|
||||||
IsInMixedFolder = true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
var result = BookFileNameParser.Parse(Path.GetFileNameWithoutExtension(args.Path));
|
||||||
|
|
||||||
|
return new Book
|
||||||
|
{
|
||||||
|
Path = args.Path,
|
||||||
|
Name = result.Name ?? string.Empty,
|
||||||
|
IndexNumber = result.Index,
|
||||||
|
ProductionYear = result.Year,
|
||||||
|
SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)),
|
||||||
|
IsInMixedFolder = true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Book GetBook(ItemResolveArgs args)
|
private Book GetBook(ItemResolveArgs args)
|
||||||
@@ -59,15 +64,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||||||
StringComparison.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase);
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
// Don't return a Book if there is more (or less) than one document in the directory
|
// directory is only considered a book when it contains exactly one supported file
|
||||||
|
// other library structures with multiple books to a directory will get picked up as individual files
|
||||||
if (bookFiles.Count != 1)
|
if (bookFiles.Count != 1)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var result = BookFileNameParser.Parse(Path.GetFileName(args.Path));
|
||||||
|
|
||||||
return new Book
|
return new Book
|
||||||
{
|
{
|
||||||
Path = bookFiles[0].FullName
|
Path = bookFiles[0].FullName,
|
||||||
|
Name = result.Name ?? string.Empty,
|
||||||
|
IndexNumber = result.Index,
|
||||||
|
ProductionYear = result.Year,
|
||||||
|
SeriesName = result.SeriesName ?? string.Empty,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
|
results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.Limit.HasValue)
|
if (query.Limit.HasValue && query.Limit.Value > 0)
|
||||||
{
|
{
|
||||||
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
|
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,53 +177,74 @@ namespace Emby.Server.Implementations.Library
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
|
/// <inheritdoc />
|
||||||
|
public Dictionary<Guid, UserItemData> GetUserDataBatch(IReadOnlyList<BaseItem> items, User user)
|
||||||
{
|
{
|
||||||
var cacheKey = GetCacheKey(user.InternalId, itemId);
|
var result = new Dictionary<Guid, UserItemData>(items.Count);
|
||||||
|
var itemsNeedingQuery = new List<(BaseItem Item, List<string> Keys)>();
|
||||||
|
|
||||||
if (_cache.TryGet(cacheKey, out var data))
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
return data;
|
var cacheKey = GetCacheKey(user.InternalId, item.Id);
|
||||||
}
|
if (_cache.TryGet(cacheKey, out var cachedData))
|
||||||
|
|
||||||
data = GetUserDataInternal(user.Id, itemId, keys);
|
|
||||||
|
|
||||||
if (data is null)
|
|
||||||
{
|
|
||||||
return new UserItemData()
|
|
||||||
{
|
{
|
||||||
Key = keys[0],
|
result[item.Id] = cachedData;
|
||||||
};
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
return _cache.GetOrAdd(cacheKey, _ => data);
|
var userData = item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault();
|
||||||
}
|
if (userData is not null)
|
||||||
|
{
|
||||||
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
|
result[item.Id] = userData;
|
||||||
{
|
_cache.AddOrUpdate(cacheKey, userData);
|
||||||
if (keys.Count == 0)
|
}
|
||||||
{
|
else
|
||||||
return null;
|
{
|
||||||
}
|
var keys = item.GetUserDataKeys();
|
||||||
|
itemsNeedingQuery.Add((item, keys));
|
||||||
using var context = _repository.CreateDbContext();
|
}
|
||||||
var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
|
|
||||||
|
|
||||||
if (userData.Length > 0)
|
|
||||||
{
|
|
||||||
var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
|
|
||||||
if (directDataReference is not null)
|
|
||||||
{
|
|
||||||
return Map(directDataReference);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Map(userData.First());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new UserItemData
|
if (itemsNeedingQuery.Count == 0)
|
||||||
{
|
{
|
||||||
Key = keys.Last()!
|
return result;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Build a single query for all missing items
|
||||||
|
var allItemIds = itemsNeedingQuery.Select(x => x.Item.Id).ToList();
|
||||||
|
var allKeys = itemsNeedingQuery.SelectMany(x => x.Keys).Distinct().ToList();
|
||||||
|
if (allKeys.Count > 0)
|
||||||
|
{
|
||||||
|
using var context = _repository.CreateDbContext();
|
||||||
|
var userDataArray = context.UserData
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(e => e.UserId.Equals(user.Id))
|
||||||
|
.WhereOneOrMany(allItemIds, e => e.ItemId)
|
||||||
|
.WhereOneOrMany(allKeys, e => e.CustomDataKey)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray());
|
||||||
|
foreach (var (item, keys) in itemsNeedingQuery)
|
||||||
|
{
|
||||||
|
UserItemData userData;
|
||||||
|
if (userDataByItem.TryGetValue(item.Id, out var itemUserData) && itemUserData.Length > 0)
|
||||||
|
{
|
||||||
|
var directDataReference = itemUserData.FirstOrDefault(e => e.CustomDataKey == item.Id.ToString("N"));
|
||||||
|
userData = directDataReference is not null ? Map(directDataReference) : Map(itemUserData.First());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userData = new UserItemData { Key = keys.Count > 0 ? keys[0] : string.Empty };
|
||||||
|
}
|
||||||
|
|
||||||
|
result[item.Id] = userData;
|
||||||
|
var cacheKey = GetCacheKey(user.InternalId, item.Id);
|
||||||
|
_cache.AddOrUpdate(cacheKey, userData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var collectionFolder = folder as ICollectionFolder;
|
var collectionFolder = folder as ICollectionFolder;
|
||||||
var folderViewType = collectionFolder?.CollectionType;
|
var folderViewType = collectionFolder?.CollectionType;
|
||||||
|
|
||||||
// Playlist library requires special handling because the folder only references user playlists
|
// Playlist and BoxSet libraries require special handling because the folder only references linked items
|
||||||
if (folderViewType == CollectionType.playlists)
|
if (folderViewType == CollectionType.playlists || folderViewType == CollectionType.boxsets)
|
||||||
{
|
{
|
||||||
var items = folder.GetItemList(new InternalItemsQuery(user)
|
var items = folder.GetItemList(new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
@@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
list = list.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
|
list = list.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
|
var sorted = _libraryManager.Sort(list, user, [ItemSortBy.SortName], SortOrder.Ascending).ToList();
|
||||||
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
|
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
|
||||||
|
|
||||||
return list
|
return list
|
||||||
@@ -205,7 +205,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var libraryItems = GetItemsForLatestItems(request.User, request, options);
|
var libraryItems = GetItemsForLatestItems(request.User, request, options);
|
||||||
|
|
||||||
var list = new List<Tuple<BaseItem, List<BaseItem>>>();
|
var list = new List<Tuple<BaseItem, List<BaseItem>>>();
|
||||||
|
var containerIndexMap = new Dictionary<Guid, int>();
|
||||||
foreach (var item in libraryItems)
|
foreach (var item in libraryItems)
|
||||||
{
|
{
|
||||||
// Only grab the index container for media
|
// Only grab the index container for media
|
||||||
@@ -213,20 +213,16 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
if (container is null)
|
if (container is null)
|
||||||
{
|
{
|
||||||
list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item }));
|
list.Add(new Tuple<BaseItem, List<BaseItem>>(null!, new List<BaseItem> { item }));
|
||||||
|
}
|
||||||
|
else if (containerIndexMap.TryGetValue(container.Id, out var existingIndex))
|
||||||
|
{
|
||||||
|
list[existingIndex].Item2.Add(item);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var current = list.FirstOrDefault(i => i.Item1 is not null && i.Item1.Id.Equals(container.Id));
|
containerIndexMap[container.Id] = list.Count;
|
||||||
|
list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
|
||||||
if (current is not null)
|
|
||||||
{
|
|
||||||
current.Item2.Add(item);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (list.Count >= request.Limit)
|
if (list.Count >= request.Limit)
|
||||||
@@ -255,7 +251,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return _channelManager.GetLatestChannelItemsInternal(
|
return _channelManager.GetLatestChannelItemsInternal(
|
||||||
new InternalItemsQuery(user)
|
new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
ChannelIds = new[] { parentId },
|
ChannelIds = [parentId],
|
||||||
IsPlayed = request.IsPlayed,
|
IsPlayed = request.IsPlayed,
|
||||||
StartIndex = request.StartIndex,
|
StartIndex = request.StartIndex,
|
||||||
Limit = request.Limit,
|
Limit = request.Limit,
|
||||||
@@ -301,11 +297,11 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
|
if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
|
||||||
{
|
{
|
||||||
includeItemTypes = new[] { BaseItemKind.Movie };
|
includeItemTypes = [BaseItemKind.Movie];
|
||||||
}
|
}
|
||||||
else if (hasCollectionType.All(i => i.CollectionType == CollectionType.tvshows))
|
else if (hasCollectionType.All(i => i.CollectionType == CollectionType.tvshows))
|
||||||
{
|
{
|
||||||
includeItemTypes = new[] { BaseItemKind.Episode };
|
includeItemTypes = [BaseItemKind.Episode];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,29 +340,29 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0
|
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0
|
||||||
? new[]
|
?
|
||||||
{
|
[
|
||||||
BaseItemKind.Person,
|
BaseItemKind.Person,
|
||||||
BaseItemKind.Studio,
|
BaseItemKind.Studio,
|
||||||
BaseItemKind.Year,
|
BaseItemKind.Year,
|
||||||
BaseItemKind.MusicGenre,
|
BaseItemKind.MusicGenre,
|
||||||
BaseItemKind.Genre
|
BaseItemKind.Genre
|
||||||
}
|
]
|
||||||
: Array.Empty<BaseItemKind>();
|
: Array.Empty<BaseItemKind>();
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
var query = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
IncludeItemTypes = includeItemTypes,
|
IncludeItemTypes = includeItemTypes,
|
||||||
OrderBy = new[]
|
OrderBy =
|
||||||
{
|
[
|
||||||
(ItemSortBy.DateCreated, SortOrder.Descending),
|
(ItemSortBy.DateCreated, SortOrder.Descending),
|
||||||
(ItemSortBy.SortName, SortOrder.Descending),
|
(ItemSortBy.SortName, SortOrder.Descending),
|
||||||
(ItemSortBy.ProductionYear, SortOrder.Descending)
|
(ItemSortBy.ProductionYear, SortOrder.Descending)
|
||||||
},
|
],
|
||||||
IsFolder = includeItemTypes.Length == 0 ? false : null,
|
IsFolder = includeItemTypes.Length == 0 ? false : null,
|
||||||
ExcludeItemTypes = excludeItemTypes,
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
Limit = limit * 5,
|
Limit = limit * 2,
|
||||||
IsPlayed = isPlayed,
|
IsPlayed = isPlayed,
|
||||||
DtoOptions = options,
|
DtoOptions = options,
|
||||||
MediaTypes = mediaTypes
|
MediaTypes = mediaTypes
|
||||||
@@ -394,6 +390,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
query.Limit = limit;
|
query.Limit = limit;
|
||||||
return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
|
return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (collectionType == CollectionType.movies)
|
||||||
|
{
|
||||||
|
query.Limit = limit;
|
||||||
|
return _libraryManager.GetLatestItemList(query, parents, CollectionType.movies);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _libraryManager.GetItemList(query, parents);
|
return _libraryManager.GetItemList(query, parents);
|
||||||
|
|||||||
@@ -50,21 +50,40 @@ public class ArtistsValidator
|
|||||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var names = _itemRepo.GetAllArtistNames();
|
var names = _itemRepo.GetAllArtistNames();
|
||||||
|
var existingArtistIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.MusicArtist]
|
||||||
|
}).ToHashSet();
|
||||||
|
|
||||||
|
var existingArtists = _libraryManager.GetArtists(names);
|
||||||
|
|
||||||
var numComplete = 0;
|
var numComplete = 0;
|
||||||
var count = names.Count;
|
var count = names.Count;
|
||||||
|
var refreshed = 0;
|
||||||
|
|
||||||
foreach (var name in names)
|
foreach (var name in names)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetArtist(name);
|
MusicArtist? item = null;
|
||||||
|
if (existingArtists.TryGetValue(name, out var artists) && artists.Length > 0)
|
||||||
|
{
|
||||||
|
item = artists.OrderBy(i => i.IsAccessedByName ? 1 : 0).First();
|
||||||
|
}
|
||||||
|
|
||||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
// Fall back to GetArtist if not found (creates new item if needed)
|
||||||
|
item ??= _libraryManager.GetArtist(name);
|
||||||
|
var isNew = !existingArtistIds.Contains(item.Id);
|
||||||
|
var neverRefreshed = item.DateLastRefreshed == default;
|
||||||
|
|
||||||
|
if (isNew || neverRefreshed)
|
||||||
|
{
|
||||||
|
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
refreshed++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Don't clutter the log
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -80,31 +99,24 @@ public class ArtistsValidator
|
|||||||
progress.Report(percent);
|
progress.Report(percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new artists out of {TotalCount} total", refreshed, count);
|
||||||
|
|
||||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = [BaseItemKind.MusicArtist],
|
IncludeItemTypes = [BaseItemKind.MusicArtist],
|
||||||
IsDeadArtist = true,
|
IsDeadArtist = true,
|
||||||
IsLocked = false
|
IsLocked = false
|
||||||
}).Cast<MusicArtist>().ToList();
|
}).Cast<MusicArtist>()
|
||||||
|
.Where(item => item.IsAccessedByName)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
foreach (var item in deadEntities)
|
foreach (var item in deadEntities)
|
||||||
{
|
{
|
||||||
if (!item.IsAccessedByName)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||||
|
|
||||||
_libraryManager.DeleteItem(
|
|
||||||
item,
|
|
||||||
new DeleteOptions
|
|
||||||
{
|
|
||||||
DeleteFileLocation = false
|
|
||||||
},
|
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public class CollectionPostScanTask : ILibraryPostScanTask
|
|||||||
|
|
||||||
foreach (var m in movies)
|
foreach (var m in movies)
|
||||||
{
|
{
|
||||||
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
|
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName) && !movie.PrimaryVersionId.HasValue)
|
||||||
{
|
{
|
||||||
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
|
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@@ -48,17 +49,40 @@ public class GenresValidator
|
|||||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var names = _itemRepo.GetGenreNames();
|
var names = _itemRepo.GetGenreNames();
|
||||||
|
var existingGenreIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Genre]
|
||||||
|
}).ToHashSet();
|
||||||
|
|
||||||
|
var existingGenres = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Genre]
|
||||||
|
}).Cast<Genre>()
|
||||||
|
.GroupBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var numComplete = 0;
|
var numComplete = 0;
|
||||||
var count = names.Count;
|
var count = names.Count;
|
||||||
|
var refreshed = 0;
|
||||||
|
|
||||||
foreach (var name in names)
|
foreach (var name in names)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetGenre(name);
|
Genre? item = null;
|
||||||
|
if (existingGenres.TryGetValue(name, out var existingGenre))
|
||||||
|
{
|
||||||
|
item = existingGenre;
|
||||||
|
}
|
||||||
|
|
||||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
// Fall back to GetGenre if not found (creates new item if needed)
|
||||||
|
item ??= _libraryManager.GetGenre(name);
|
||||||
|
|
||||||
|
if (!existingGenreIds.Contains(item.Id))
|
||||||
|
{
|
||||||
|
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
refreshed++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -78,6 +102,8 @@ public class GenresValidator
|
|||||||
progress.Report(percent);
|
progress.Report(percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new genres out of {TotalCount} total", refreshed, count);
|
||||||
|
|
||||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
|
IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
|
||||||
@@ -88,16 +114,10 @@ public class GenresValidator
|
|||||||
foreach (var item in deadEntities)
|
foreach (var item in deadEntities)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||||
|
|
||||||
_libraryManager.DeleteItem(
|
|
||||||
item,
|
|
||||||
new DeleteOptions
|
|
||||||
{
|
|
||||||
DeleteFileLocation = false
|
|
||||||
},
|
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -45,17 +48,25 @@ public class MusicGenresValidator
|
|||||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var names = _itemRepo.GetMusicGenreNames();
|
var names = _itemRepo.GetMusicGenreNames();
|
||||||
|
var existingMusicGenreIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.MusicGenre]
|
||||||
|
}).ToHashSet();
|
||||||
|
|
||||||
var numComplete = 0;
|
var numComplete = 0;
|
||||||
var count = names.Count;
|
var count = names.Count;
|
||||||
|
var refreshed = 0;
|
||||||
|
|
||||||
foreach (var name in names)
|
foreach (var name in names)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetMusicGenre(name);
|
var item = _libraryManager.GetMusicGenre(name);
|
||||||
|
if (!existingMusicGenreIds.Contains(item.Id))
|
||||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
{
|
||||||
|
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
refreshed++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -75,6 +86,8 @@ public class MusicGenresValidator
|
|||||||
progress.Report(percent);
|
progress.Report(percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new music genres out of {TotalCount} total", refreshed, count);
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public class PeopleValidator
|
|||||||
var i = 0;
|
var i = 0;
|
||||||
foreach (var item in deadEntities.Chunk(500))
|
foreach (var item in deadEntities.Chunk(500))
|
||||||
{
|
{
|
||||||
_libraryManager.DeleteItemsUnsafeFast(item);
|
_libraryManager.DeleteItemsUnsafeFast(item, true);
|
||||||
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
|
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@@ -49,17 +50,40 @@ public class StudiosValidator
|
|||||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var names = _itemRepo.GetStudioNames();
|
var names = _itemRepo.GetStudioNames();
|
||||||
|
var existingStudioIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Studio]
|
||||||
|
}).ToHashSet();
|
||||||
|
|
||||||
|
var existingStudios = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Studio]
|
||||||
|
}).Cast<Studio>()
|
||||||
|
.GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var numComplete = 0;
|
var numComplete = 0;
|
||||||
var count = names.Count;
|
var count = names.Count;
|
||||||
|
var refreshed = 0;
|
||||||
|
|
||||||
foreach (var name in names)
|
foreach (var name in names)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetStudio(name);
|
Studio? item = null;
|
||||||
|
if (existingStudios.TryGetValue(name, out var existingStudio))
|
||||||
|
{
|
||||||
|
item = existingStudio;
|
||||||
|
}
|
||||||
|
|
||||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
// Fall back to GetStudio if not found (creates new item if needed)
|
||||||
|
item ??= _libraryManager.GetStudio(name);
|
||||||
|
|
||||||
|
if (!existingStudioIds.Contains(item.Id))
|
||||||
|
{
|
||||||
|
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
refreshed++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -79,6 +103,8 @@ public class StudiosValidator
|
|||||||
progress.Report(percent);
|
progress.Report(percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new studios out of {TotalCount} total", refreshed, count);
|
||||||
|
|
||||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = [BaseItemKind.Studio],
|
IncludeItemTypes = [BaseItemKind.Studio],
|
||||||
@@ -89,16 +115,10 @@ public class StudiosValidator
|
|||||||
foreach (var item in deadEntities)
|
foreach (var item in deadEntities)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||||
|
|
||||||
_libraryManager.DeleteItem(
|
|
||||||
item,
|
|
||||||
new DeleteOptions
|
|
||||||
{
|
|
||||||
DeleteFileLocation = false
|
|
||||||
},
|
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Albums": "аальбомқәа"
|
"Albums": "аальбомқәа",
|
||||||
|
"AppDeviceValues": "Апп: {0}, Априбор: {1}",
|
||||||
|
"Application": "Апрограмма"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,12 +128,12 @@
|
|||||||
"TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling.",
|
"TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling.",
|
||||||
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
|
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
|
||||||
"TaskAudioNormalization": "Odio Normalisering",
|
"TaskAudioNormalization": "Odio Normalisering",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.",
|
|
||||||
"TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
|
"TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
|
||||||
"TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
|
"TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
|
||||||
"TaskExtractMediaSegments": "Media Segment Skandeer",
|
"TaskExtractMediaSegments": "Media Segment Skandeer",
|
||||||
"TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
|
"TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
|
||||||
"TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
|
"TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
|
||||||
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
|
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings.",
|
||||||
|
"CleanupUserDataTask": "Gebruikers data skoon maak taak",
|
||||||
|
"CleanupUserDataTaskDescription": "Maak alle gebruikers data (kykstatus, gunstelingstatus, ens.) skoon van media wat nie meer vir ten minste 90 dae teenwoordig is nie."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,142 +1,139 @@
|
|||||||
{
|
{
|
||||||
"Albums": "ألبومات",
|
"Albums": "الألبومات",
|
||||||
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
|
"AppDeviceValues": "التطبيق: {0}، الجهاز: {1}",
|
||||||
"Application": "تطبيق",
|
"Application": "التطبيق",
|
||||||
"Artists": "الفنانون",
|
"Artists": "الفنانون",
|
||||||
"AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}",
|
"AuthenticationSucceededWithUserName": "تمت مصادقة {0} بنجاح",
|
||||||
"Books": "الكتب",
|
"Books": "الكتب",
|
||||||
"CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}",
|
"CameraImageUploadedFrom": "تم رفع صورة كاميرا جديدة من {0}",
|
||||||
"Channels": "القنوات",
|
"Channels": "القنوات",
|
||||||
"ChapterNameValue": "الفصل {0}",
|
"ChapterNameValue": "الفصل {0}",
|
||||||
"Collections": "المجموعات",
|
"Collections": "المجموعات",
|
||||||
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
|
"DeviceOfflineWithName": "انقطع اتصال {0}",
|
||||||
"DeviceOnlineWithName": "{0} متصل",
|
"DeviceOnlineWithName": "{0} متصل",
|
||||||
"FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}",
|
"FailedLoginAttemptWithUserName": "محاولة تسجيل دخول فاشلة من {0}",
|
||||||
"Favorites": "المفضلة",
|
"Favorites": "المفضلة",
|
||||||
"Folders": "المجلدات",
|
"Folders": "المجلدات",
|
||||||
"Genres": "التصنيفات",
|
"Genres": "الأنواع",
|
||||||
"HeaderAlbumArtists": "فناني الألبوم",
|
"HeaderAlbumArtists": "فنانو الألبوم",
|
||||||
"HeaderContinueWatching": "إستئناف المشاهدة",
|
"HeaderContinueWatching": "متابعة المشاهدة",
|
||||||
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
||||||
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
||||||
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
|
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
|
||||||
"HeaderFavoriteShows": "المسلسلات المفضلة",
|
"HeaderFavoriteShows": "المسلسلات المفضلة",
|
||||||
"HeaderFavoriteSongs": "الأغاني المفضلة",
|
"HeaderFavoriteSongs": "الأغاني المفضلة",
|
||||||
"HeaderLiveTV": "التلفاز المباشر",
|
"HeaderLiveTV": "البث التلفزيوني المباشر",
|
||||||
"HeaderNextUp": "التالي",
|
"HeaderNextUp": "التالي",
|
||||||
"HeaderRecordingGroups": "مجموعات التسجيل",
|
"HeaderRecordingGroups": "مجموعات التسجيل",
|
||||||
"HomeVideos": "الفيديوهات الشخصية",
|
"HomeVideos": "فيديوهات منزلية",
|
||||||
"Inherit": "توريث",
|
"Inherit": "وراثة",
|
||||||
"ItemAddedWithName": "أُضيف {0} للمكتبة",
|
"ItemAddedWithName": "تمت إضافة {0} إلى المكتبة",
|
||||||
"ItemRemovedWithName": "أُزيل {0} من المكتبة",
|
"ItemRemovedWithName": "تمت إزالة {0} من المكتبة",
|
||||||
"LabelIpAddressValue": "عنوان الآي بي: {0}",
|
"LabelIpAddressValue": "عنوان IP: {0}",
|
||||||
"LabelRunningTimeValue": "مدة التشغيل: {0}",
|
"LabelRunningTimeValue": "مدة التشغيل: {0}",
|
||||||
"Latest": "الأحدث",
|
"Latest": "الأحدث",
|
||||||
"MessageApplicationUpdated": "حُدث خادم Jellyfin",
|
"MessageApplicationUpdated": "تم تحديث خادم Jellyfin",
|
||||||
"MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}",
|
"MessageApplicationUpdatedTo": "تم تحديث خادم Jellyfin إلى {0}",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}",
|
"MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث قسم إعدادات الخادم {0}",
|
||||||
"MessageServerConfigurationUpdated": "حُدثت إعدادات الخادم",
|
"MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم",
|
||||||
"MixedContent": "محتوى مختلط",
|
"MixedContent": "محتوى مختلط",
|
||||||
"Movies": "الأفلام",
|
"Movies": "الأفلام",
|
||||||
"Music": "الموسيقى",
|
"Music": "الموسيقى",
|
||||||
"MusicVideos": "الفيديوهات الموسيقية",
|
"MusicVideos": "الفيديوهات الموسيقية",
|
||||||
"NameInstallFailed": "فشل تثبيت {0}",
|
"NameInstallFailed": "فشل تثبيت {0}",
|
||||||
"NameSeasonNumber": "الموسم {0}",
|
"NameSeasonNumber": "الموسم {0}",
|
||||||
"NameSeasonUnknown": "الموسم غير معروف",
|
"NameSeasonUnknown": "موسم غير معروف",
|
||||||
"NewVersionIsAvailable": "نسخة جديدة من خادم Jellyfin متوفرة للتحميل.",
|
"NewVersionIsAvailable": "يتوفر إصدار جديد من خادم Jellyfin للتنزيل.",
|
||||||
"NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق",
|
"NotificationOptionApplicationUpdateAvailable": "تحديث التطبيق متاح",
|
||||||
"NotificationOptionApplicationUpdateInstalled": "نُصب تحديث التطبيق",
|
"NotificationOptionApplicationUpdateInstalled": "تم تثبيت تحديث التطبيق",
|
||||||
"NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
|
"NotificationOptionAudioPlayback": "بدأ تشغيل الصوت",
|
||||||
"NotificationOptionAudioPlaybackStopped": "أُوقف تشغيل المقطع الصوتي",
|
"NotificationOptionAudioPlaybackStopped": "توقف تشغيل الصوت",
|
||||||
"NotificationOptionCameraImageUploaded": "رُفعت صورة الكاميرا",
|
"NotificationOptionCameraImageUploaded": "تم رفع صورة كاميرا",
|
||||||
"NotificationOptionInstallationFailed": "فشل في التثبيت",
|
"NotificationOptionInstallationFailed": "فشل التثبيت",
|
||||||
"NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا",
|
"NotificationOptionNewLibraryContent": "تمت إضافة محتوى جديد",
|
||||||
"NotificationOptionPluginError": "فشل في الملحق",
|
"NotificationOptionPluginError": "خطأ في الملحق",
|
||||||
"NotificationOptionPluginInstalled": "ثُبتت الملحق",
|
"NotificationOptionPluginInstalled": "تم تثبيت الملحق",
|
||||||
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
|
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
|
||||||
"NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
|
"NotificationOptionPluginUpdateInstalled": "تم تحديث الملحق",
|
||||||
"NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
|
"NotificationOptionServerRestartRequired": "مطلوب إعادة تشغيل الخادم",
|
||||||
"NotificationOptionTaskFailed": "فشل في المهمة المجدولة",
|
"NotificationOptionTaskFailed": "فشل المهمة المجدولة",
|
||||||
"NotificationOptionUserLockedOut": "تم إقفال حساب المستخدم",
|
"NotificationOptionUserLockedOut": "تم قفل حساب المستخدم",
|
||||||
"NotificationOptionVideoPlayback": "بدأ تشغيل الفيديو",
|
"NotificationOptionVideoPlayback": "بدأ تشغيل الفيديو",
|
||||||
"NotificationOptionVideoPlaybackStopped": "تم إيقاف تشغيل الفيديو",
|
"NotificationOptionVideoPlaybackStopped": "توقف تشغيل الفيديو",
|
||||||
"Photos": "الصور",
|
"Photos": "الصور",
|
||||||
"Playlists": "قوائم التشغيل",
|
"Playlists": "قوائم التشغيل",
|
||||||
"Plugin": "الملحق",
|
"Plugin": "الملحق",
|
||||||
"PluginInstalledWithName": "تم تثبيت {0}",
|
"PluginInstalledWithName": "تم تثبيت {0}",
|
||||||
"PluginUninstalledWithName": "تمت إزالة {0}",
|
"PluginUninstalledWithName": "تمت إزالة {0}",
|
||||||
"PluginUpdatedWithName": "تم تحديث {0}",
|
"PluginUpdatedWithName": "تم تحديث {0}",
|
||||||
"ProviderValue": "المزود: {0}",
|
"ProviderValue": "المزوّد: {0}",
|
||||||
"ScheduledTaskFailedWithName": "فشلت العملية {0}",
|
"ScheduledTaskFailedWithName": "فشلت {0}",
|
||||||
"ScheduledTaskStartedWithName": "تم بدء العملية {0}",
|
"ScheduledTaskStartedWithName": "بدأت {0}",
|
||||||
"ServerNameNeedsToBeRestarted": "يحتاج {0} لإعادة التشغيل",
|
"ServerNameNeedsToBeRestarted": "يحتاج {0} إلى إعادة التشغيل",
|
||||||
"Shows": "العروض",
|
"Shows": "المسلسلات",
|
||||||
"Songs": "الأغاني",
|
"Songs": "الأغاني",
|
||||||
"StartupEmbyServerIsLoading": "يتم تحميل خادم Jellyfin . الرجاء المحاولة بعد قليل.",
|
"StartupEmbyServerIsLoading": "يتم الآن تحميل خادم Jellyfin. يرجى المحاولة مرة أخرى بعد قليل.",
|
||||||
"SubtitleDownloadFailureForItem": "عملية إنزال الترجمة فشلت لـ{0}",
|
"SubtitleDownloadFailureFromForItem": "فشل تنزيل الترجمات من {0} لـ {1}",
|
||||||
"SubtitleDownloadFailureFromForItem": "فشل تحميل الترجمات من {0} ل {1}",
|
|
||||||
"Sync": "مزامنة",
|
"Sync": "مزامنة",
|
||||||
"System": "النظام",
|
"System": "النظام",
|
||||||
"TvShows": "البرامج التلفزيونية",
|
"TvShows": "البرامج التلفزيونية",
|
||||||
"User": "المستخدم",
|
"User": "المستخدم",
|
||||||
"UserCreatedWithName": "تم إنشاء المستخدم {0}",
|
"UserCreatedWithName": "تم إنشاء المستخدم {0}",
|
||||||
"UserDeletedWithName": "تم حذف المستخدم {0}",
|
"UserDeletedWithName": "تم حذف المستخدم {0}",
|
||||||
"UserDownloadingItemWithValues": "يقوم {0} بتنزيل {1}",
|
"UserDownloadingItemWithValues": "{0} يقوم بتنزيل {1}",
|
||||||
"UserLockedOutWithName": "تم منع المستخدم {0} من الدخول",
|
"UserLockedOutWithName": "تم قفل حساب المستخدم {0}",
|
||||||
"UserOfflineFromDevice": "تم قطع اتصال {0} من {1}",
|
"UserOfflineFromDevice": "انقطع اتصال {0} من {1}",
|
||||||
"UserOnlineFromDevice": "{0} متصل عبر {1}",
|
"UserOnlineFromDevice": "{0} متصل من {1}",
|
||||||
"UserPasswordChangedWithName": "تم تغيير كلمة السر للمستخدم {0}",
|
"UserPasswordChangedWithName": "تم تغيير كلمة المرور للمستخدم {0}",
|
||||||
"UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم {0}",
|
"UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم لـ {0}",
|
||||||
"UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}",
|
"UserStartedPlayingItemWithValues": "{0} يقوم بتشغيل {1} على {2}",
|
||||||
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
|
"UserStoppedPlayingItemWithValues": "أنهى {0} تشغيل {1} على {2}",
|
||||||
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
|
"ValueHasBeenAddedToLibrary": "تمت إضافة {0} إلى مكتبة المحتوى الخاصة بك",
|
||||||
"ValueSpecialEpisodeName": "حلقة خاصه - {0}",
|
"ValueSpecialEpisodeName": "خاص - {0}",
|
||||||
"VersionNumber": "الإصدار {0}",
|
"VersionNumber": "الإصدار {0}",
|
||||||
"TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
|
"TaskCleanCacheDescription": "يحذف ملفات ذاكرة التخزين المؤقت التي لم يعد النظام بحاجة إليها.",
|
||||||
"TaskCleanCache": "حذف الملفات المؤقتة",
|
"TaskCleanCache": "تنظيف مجلد ذاكرة التخزين المؤقت",
|
||||||
"TasksChannelsCategory": "قنوات الإنترنت",
|
"TasksChannelsCategory": "قنوات الإنترنت",
|
||||||
"TasksLibraryCategory": "مكتبة",
|
"TasksLibraryCategory": "المكتبة",
|
||||||
"TasksMaintenanceCategory": "صيانة",
|
"TasksMaintenanceCategory": "الصيانة",
|
||||||
"TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يُحدث البيانات الوصفية.",
|
"TaskRefreshLibraryDescription": "يفحص مكتبة المحتوى الخاصة بك بحثاً عن ملفات جديدة ويحدّث البيانات الوصفية.",
|
||||||
"TaskRefreshLibrary": "افحص مكتبة الوسائط",
|
"TaskRefreshLibrary": "فحص مكتبة المحتوى",
|
||||||
"TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
|
"TaskRefreshChapterImagesDescription": "ينشئ صوراً مصغرة للفيديوهات التي تحتوي على فصول.",
|
||||||
"TaskRefreshChapterImages": "استخراج صور الفصل",
|
"TaskRefreshChapterImages": "استخراج صور الفصول",
|
||||||
"TasksApplicationCategory": "تطبيق",
|
"TasksApplicationCategory": "التطبيق",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت على الترجمات الناقصة استنادا على البيانات الوصفية.",
|
"TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت عن الترجمات المفقودة بناءً على إعدادات البيانات الوصفية.",
|
||||||
"TaskDownloadMissingSubtitles": "تحميل الترجمات الناقصة",
|
"TaskDownloadMissingSubtitles": "تنزيل الترجمات المفقودة",
|
||||||
"TaskRefreshChannelsDescription": "يحدث معلومات قنوات الإنترنت.",
|
"TaskRefreshChannelsDescription": "يحدّث معلومات قنوات الإنترنت.",
|
||||||
"TaskRefreshChannels": "إعادة تحديث القنوات",
|
"TaskRefreshChannels": "تحديث القنوات",
|
||||||
"TaskCleanTranscodeDescription": "يحذف ملفات الترميز الأقدم من يوم واحد.",
|
"TaskCleanTranscodeDescription": "يحذف ملفات تحويل الترميز التي مر عليها أكثر من يوم واحد.",
|
||||||
"TaskCleanTranscode": "حذف ما بمجلد الترميز",
|
"TaskCleanTranscode": "تنظيف مجلد تحويل الترميز",
|
||||||
"TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.",
|
"TaskUpdatePluginsDescription": "ينزّل ويثبّت التحديثات للملحقات المهيأة للتحديث التلقائي.",
|
||||||
"TaskUpdatePlugins": "تحديث الإضافات",
|
"TaskUpdatePlugins": "تحديث الملحقات",
|
||||||
"TaskRefreshPeopleDescription": "يقوم بتحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
|
"TaskRefreshPeopleDescription": "يحدّث البيانات الوصفية للممثلين والمخرجين في مكتبة المحتوى الخاصة بك.",
|
||||||
"TaskRefreshPeople": "إعادة تحميل الأشخاص",
|
"TaskRefreshPeople": "تحديث الأشخاص",
|
||||||
"TaskCleanLogsDescription": "يحذف السجلات الأقدم من {0} يوم.",
|
"TaskCleanLogsDescription": "يحذف ملفات السجل التي يزيد عمرها عن {0} أيام.",
|
||||||
"TaskCleanLogs": "حذف مسار السجل",
|
"TaskCleanLogs": "تنظيف مجلد السجلات",
|
||||||
"TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الذي تم تحديده.",
|
"TaskCleanActivityLogDescription": "يحذف إدخالات سجل النشاط الأقدم من العمر المحدد.",
|
||||||
"TaskCleanActivityLog": "حذف سجل الأنشطة",
|
"TaskCleanActivityLog": "تنظيف سجل النشاط",
|
||||||
"Default": "افتراضي",
|
"Default": "الافتراضي",
|
||||||
"Undefined": "غير معرف",
|
"Undefined": "غير محدد",
|
||||||
"Forced": "ملحقة",
|
"Forced": "إجباري",
|
||||||
"TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات في قاعدة البيانات قد تؤدي إلى تحسين الأداء.",
|
"TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقلل المساحة الحرة. قد يؤدي تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات على قاعدة البيانات إلى تحسين الأداء.",
|
||||||
"TaskOptimizeDatabase": "تحسين قاعدة البيانات",
|
"TaskOptimizeDatabase": "تحسين قاعدة البيانات",
|
||||||
"TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.",
|
"TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لإنشاء قوائم تشغيل HLS أكثر دقة. قد يستغرق تشغيل هذه المهمة وقتاً طويلاً.",
|
||||||
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
|
"TaskKeyframeExtractor": "مستخرج الإطارات الرئيسية",
|
||||||
"External": "خارجي",
|
"External": "خارجي",
|
||||||
"HearingImpaired": "ضعاف السمع",
|
"HearingImpaired": "لضعاف السمع",
|
||||||
"TaskRefreshTrickplayImages": "توليد صور المعاينة السريعة",
|
"TaskRefreshTrickplayImages": "إنشاء صور معاينات التنقل (Trickplay)",
|
||||||
"TaskRefreshTrickplayImagesDescription": "يُولّد معاينات تنقل سريع لمقاطع الفيديو ضمن المكتبات المفعّلة.",
|
"TaskRefreshTrickplayImagesDescription": "ينشئ صور معاينات التنقل السريع للفيديوهات في المكتبات المفعّلة.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
|
"TaskAudioNormalization": "تطبيع الصوت",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
|
"TaskAudioNormalizationDescription": "يفحص الملفات لجمع بيانات تطبيع الصوت.",
|
||||||
"TaskAudioNormalization": "تسوية الصوت",
|
"TaskDownloadMissingLyrics": "تنزيل الكلمات المفقودة",
|
||||||
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.",
|
"TaskDownloadMissingLyricsDescription": "ينزّل الكلمات للأغاني.",
|
||||||
"TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة",
|
"TaskExtractMediaSegments": "فحص مقاطع المحتوى",
|
||||||
"TaskDownloadMissingLyricsDescription": "كلمات",
|
"TaskExtractMediaSegmentsDescription": "يستخرج أو يحصل على مقاطع المحتوى من الملحقات المفعّلة لمقاطع المحتوى (MediaSegment).",
|
||||||
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
|
"TaskMoveTrickplayImages": "نقل موقع صور معاينات التنقل",
|
||||||
"TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
|
"TaskMoveTrickplayImagesDescription": "ينقل ملفات معاينات التنقل الحالية وفقاً لإعدادات المكتبة.",
|
||||||
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
|
|
||||||
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
|
|
||||||
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
|
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
|
||||||
"CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
|
"CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل."
|
||||||
}
|
}
|
||||||
|
|||||||
1
Emby.Server.Implementations/Localization/Core/ar_SA.json
Normal file
1
Emby.Server.Implementations/Localization/Core/ar_SA.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"Playlists": "Плэй-лісты",
|
"Playlists": "Плэй-лісты",
|
||||||
"Latest": "Апошняе",
|
"Latest": "Апошняе",
|
||||||
"LabelIpAddressValue": "IP-адрас: {0}",
|
"LabelIpAddressValue": "IP-адрас: {0}",
|
||||||
"ItemAddedWithName": "{0} даданы ў бібліятэку",
|
"ItemAddedWithName": "{0} дададзены ў бібліятэку",
|
||||||
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
|
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
|
||||||
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
|
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
|
||||||
"PluginInstalledWithName": "{0} быў усталяваны",
|
"PluginInstalledWithName": "{0} быў усталяваны",
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
"Channels": "Каналы",
|
"Channels": "Каналы",
|
||||||
"ChapterNameValue": "Раздзел {0}",
|
"ChapterNameValue": "Раздзел {0}",
|
||||||
"Collections": "Калекцыі",
|
"Collections": "Калекцыі",
|
||||||
"Default": "Па змаўчанні",
|
"Default": "Прадвызначана",
|
||||||
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
|
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
|
||||||
"Folders": "Тэчкі",
|
"Folders": "Папкі",
|
||||||
"Favorites": "Абранае",
|
"Favorites": "Абранае",
|
||||||
"External": "Знешні",
|
"External": "Знешні",
|
||||||
"Genres": "Жанры",
|
"Genres": "Жанры",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"User": "Карыстальнік",
|
"User": "Карыстальнік",
|
||||||
"UserDeletedWithName": "Карыстальнік {0} быў выдалены",
|
"UserDeletedWithName": "Карыстальнік {0} быў выдалены",
|
||||||
"UserDownloadingItemWithValues": "{0} спампоўваецца {1}",
|
"UserDownloadingItemWithValues": "{0} спампоўваецца {1}",
|
||||||
"TaskOptimizeDatabase": "Аптымізаваць базу дадзеных",
|
"TaskOptimizeDatabase": "Аптымізацыя базы даных",
|
||||||
"Artists": "Выканаўцы",
|
"Artists": "Выканаўцы",
|
||||||
"UserOfflineFromDevice": "{0} адлучыўся ад {1}",
|
"UserOfflineFromDevice": "{0} адлучыўся ад {1}",
|
||||||
"UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
|
"UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
"TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.",
|
"TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.",
|
||||||
"TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія сканфігураваныя на аўтаматычнае абнаўленне.",
|
"TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія сканфігураваныя на аўтаматычнае абнаўленне.",
|
||||||
"TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.",
|
"TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субцітры на аснове канфігурацыі метададзеных.",
|
"TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субцітры на аснове канфігурацыі метаданых.",
|
||||||
"TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых зменаў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць выдайнасць.",
|
"TaskOptimizeDatabaseDescription": "Сціскае базу даных і вызваляе вольную прастору. Выкананне гэтай задачы пасля сканіравання бібліятэкі або іншых змяненняў, якія мадыфікуюць базу даных, можа палепшыць прадукцыйнасць.",
|
||||||
"TaskKeyframeExtractor": "Экстрактар ключавых кадраў",
|
"TaskKeyframeExtractor": "Экстрактар ключавых кадраў",
|
||||||
"TasksApplicationCategory": "Праграма",
|
"TasksApplicationCategory": "Праграма",
|
||||||
"AppDeviceValues": "Праграма: {0}, Прылада: {1}",
|
"AppDeviceValues": "Праграма: {0}, Прылада: {1}",
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
"NotificationOptionInstallationFailed": "Збой усталёўкі",
|
"NotificationOptionInstallationFailed": "Збой усталёўкі",
|
||||||
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
|
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
|
||||||
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
|
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
|
||||||
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена",
|
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыя спынена",
|
||||||
"NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося",
|
"NotificationOptionAudioPlayback": "Прайграванне аўдыя пачалося",
|
||||||
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
|
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
|
||||||
"NotificationOptionPluginError": "Збой плагіна",
|
"NotificationOptionPluginError": "Збой плагіна",
|
||||||
"NotificationOptionPluginUninstalled": "Плагін выдалены",
|
"NotificationOptionPluginUninstalled": "Плагін выдалены",
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
"ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску",
|
"ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску",
|
||||||
"Shows": "Шоу",
|
"Shows": "Шоу",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
|
||||||
"SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}",
|
"SubtitleDownloadFailureFromForItem": "Субцітры для {1} не ўдалося спампаваць з {0}",
|
||||||
"TvShows": "Тэлепраграма",
|
"TvShows": "Тэлепраграма",
|
||||||
"Undefined": "Нявызначана",
|
"Undefined": "Нявызначана",
|
||||||
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
|
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}",
|
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
|
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
|
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
|
||||||
"ValueSpecialEpisodeName": "Спецэпізод - {0}",
|
"ValueSpecialEpisodeName": "Спецвыпуск - {0}",
|
||||||
"VersionNumber": "Версія {0}",
|
"VersionNumber": "Версія {0}",
|
||||||
"TasksMaintenanceCategory": "Абслугоўванне",
|
"TasksMaintenanceCategory": "Абслугоўванне",
|
||||||
"TasksLibraryCategory": "Бібліятэка",
|
"TasksLibraryCategory": "Бібліятэка",
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
|
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
|
||||||
"TaskRefreshChapterImages": "Вынуць выявы раздзелаў",
|
"TaskRefreshChapterImages": "Вынуць выявы раздзелаў",
|
||||||
"TaskRefreshLibrary": "Сканаваць бібліятэку",
|
"TaskRefreshLibrary": "Сканаваць бібліятэку",
|
||||||
"TaskRefreshLibraryDescription": "Скануе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
|
"TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метаданыя.",
|
||||||
"TaskCleanLogs": "Ачысціць журнал",
|
"TaskCleanLogs": "Ачысціць журнал",
|
||||||
"TaskRefreshPeople": "Абнавіць выканаўцаў",
|
"TaskRefreshPeople": "Абнавіць выканаўцаў",
|
||||||
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
|
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
|
||||||
@@ -123,11 +123,9 @@
|
|||||||
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
|
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
|
||||||
"TaskRefreshChannels": "Абнавіць каналы",
|
"TaskRefreshChannels": "Абнавіць каналы",
|
||||||
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры",
|
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры",
|
||||||
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.",
|
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа выконвацца доўга.",
|
||||||
"TaskRefreshTrickplayImages": "Стварыць выявы Trickplay",
|
"TaskRefreshTrickplayImages": "Стварыць выявы Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.",
|
"TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.",
|
|
||||||
"TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.",
|
"TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.",
|
||||||
"TaskAudioNormalization": "Нармалізацыя гуку",
|
"TaskAudioNormalization": "Нармалізацыя гуку",
|
||||||
"TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.",
|
"TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.",
|
||||||
@@ -136,6 +134,6 @@
|
|||||||
"TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песняў",
|
"TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песняў",
|
||||||
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
|
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
|
||||||
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
|
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
|
||||||
"CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
|
"CleanupUserDataTask": "Задача па ачыстцы даных карыстальніка",
|
||||||
"CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
|
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"Favorites": "Любими",
|
"Favorites": "Любими",
|
||||||
"Folders": "Папки",
|
"Folders": "Папки",
|
||||||
"Genres": "Жанрове",
|
"Genres": "Жанрове",
|
||||||
"HeaderAlbumArtists": "Изпълнители на албуми",
|
"HeaderAlbumArtists": "Изпълнители на албума",
|
||||||
"HeaderContinueWatching": "Продължаване на гледането",
|
"HeaderContinueWatching": "Продължаване на гледането",
|
||||||
"HeaderFavoriteAlbums": "Любими албуми",
|
"HeaderFavoriteAlbums": "Любими албуми",
|
||||||
"HeaderFavoriteArtists": "Любими изпълнители",
|
"HeaderFavoriteArtists": "Любими изпълнители",
|
||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Сериали",
|
"Shows": "Сериали",
|
||||||
"Songs": "Песни",
|
"Songs": "Песни",
|
||||||
"StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.",
|
"StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.",
|
||||||
"SubtitleDownloadFailureForItem": "Неуспешно изтегляне на субтитри за {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени",
|
"SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени",
|
||||||
"Sync": "Синхронизиране",
|
"Sync": "Синхронизиране",
|
||||||
"System": "Система",
|
"System": "Система",
|
||||||
@@ -129,8 +128,6 @@
|
|||||||
"TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки.",
|
"TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки.",
|
||||||
"TaskDownloadMissingLyrics": "Свали липсващи текстове",
|
"TaskDownloadMissingLyrics": "Свали липсващи текстове",
|
||||||
"TaskDownloadMissingLyricsDescription": "Свали текстове за песни",
|
"TaskDownloadMissingLyricsDescription": "Свали текстове за песни",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Изчисти колекциите и плейлистите",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Премахни несъществуващи файлове в колекциите и плейлистите.",
|
|
||||||
"TaskAudioNormalization": "Нормализиране на звука",
|
"TaskAudioNormalization": "Нормализиране на звука",
|
||||||
"TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука.",
|
"TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука.",
|
||||||
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
|
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
|
||||||
|
|||||||
@@ -127,8 +127,6 @@
|
|||||||
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
|
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
|
||||||
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
|
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
|
||||||
"TaskDownloadMissingLyricsDescription": "গানের জন্য লিরিকস ডাউনলোড করুন",
|
"TaskDownloadMissingLyricsDescription": "গানের জন্য লিরিকস ডাউনলোড করুন",
|
||||||
"TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
|
|
||||||
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
|
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
|
||||||
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্ট বের করে বা অর্জন করে।",
|
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্ট বের করে বা অর্জন করে।",
|
||||||
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
|
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
|
||||||
|
|||||||
139
Emby.Server.Implementations/Localization/Core/bs.json
Normal file
139
Emby.Server.Implementations/Localization/Core/bs.json
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
{
|
||||||
|
"Albums": "Albumi",
|
||||||
|
"Artists": "Umjetnici",
|
||||||
|
"Books": "Knjige",
|
||||||
|
"Channels": "Kanalima",
|
||||||
|
"Collections": "Zbirke",
|
||||||
|
"Default": "Zadano",
|
||||||
|
"Favorites": "Omiljeni",
|
||||||
|
"Folders": "Mape",
|
||||||
|
"Genres": "Žanrovi",
|
||||||
|
"HeaderAlbumArtists": "Umjetnici albuma",
|
||||||
|
"HeaderContinueWatching": "Nastavi gledati",
|
||||||
|
"Movies": "Filmovi",
|
||||||
|
"MusicVideos": "Muzički spotovi",
|
||||||
|
"Photos": "Slike",
|
||||||
|
"Playlists": "Plejliste",
|
||||||
|
"Shows": "Pokazuje",
|
||||||
|
"Songs": "Pjesme",
|
||||||
|
"ValueSpecialEpisodeName": "Posebno - {0}",
|
||||||
|
"AppDeviceValues": "Aplikacija: {0}, Uređaj: {1}",
|
||||||
|
"Application": "Prijava",
|
||||||
|
"AuthenticationSucceededWithUserName": "{0} uspješno autentificirano",
|
||||||
|
"CameraImageUploadedFrom": "Nova slika s kamere je postavljena sa {0}",
|
||||||
|
"ChapterNameValue": "Poglavlje {0}",
|
||||||
|
"DeviceOfflineWithName": "{0} se odspojio",
|
||||||
|
"DeviceOnlineWithName": "{0} je povezan",
|
||||||
|
"External": "Vanjsko",
|
||||||
|
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave sa {0}",
|
||||||
|
"Forced": "Prisilno",
|
||||||
|
"HeaderFavoriteAlbums": "Omiljeni albumi",
|
||||||
|
"HeaderFavoriteArtists": "Omiljeni umjetnici",
|
||||||
|
"HeaderFavoriteEpisodes": "Omiljene epizode",
|
||||||
|
"HeaderFavoriteShows": "Omiljene emisije",
|
||||||
|
"HeaderFavoriteSongs": "Omiljene pjesme",
|
||||||
|
"HeaderLiveTV": "TV uživo",
|
||||||
|
"HeaderNextUp": "Slijedi",
|
||||||
|
"HeaderRecordingGroups": "Grupe za snimanje",
|
||||||
|
"HearingImpaired": "Oštećen sluh",
|
||||||
|
"HomeVideos": "Kućni videozapisi",
|
||||||
|
"Inherit": "Nasljedi",
|
||||||
|
"ItemAddedWithName": "{0} je dodan u biblioteku",
|
||||||
|
"ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
|
||||||
|
"LabelIpAddressValue": "IP adresa: {0}",
|
||||||
|
"LabelRunningTimeValue": "Trajanje: {0}",
|
||||||
|
"Latest": "Posljednje dodano",
|
||||||
|
"MessageApplicationUpdated": "Jellyfin Server je ažuriran",
|
||||||
|
"MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
|
||||||
|
"MessageNamedServerConfigurationUpdatedWithValue": "Sekcija za konfiguraciju servera {0} je ažurirana",
|
||||||
|
"MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
|
||||||
|
"MixedContent": "Miješani sadržaj",
|
||||||
|
"Music": "Muzika",
|
||||||
|
"NameInstallFailed": "{0} instalacija je propala",
|
||||||
|
"NameSeasonNumber": "Sezona {0}",
|
||||||
|
"NameSeasonUnknown": "Sezona nepoznata",
|
||||||
|
"NewVersionIsAvailable": "Dostupna je nova verzija Jellyfin Servera za preuzimanje.",
|
||||||
|
"NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
|
||||||
|
"NotificationOptionApplicationUpdateInstalled": "Ažuriranje aplikacije instalirano",
|
||||||
|
"NotificationOptionAudioPlayback": "Pokrenuto je reproduciranje zvuka",
|
||||||
|
"NotificationOptionAudioPlaybackStopped": "Zaustavljeno je reproduciranje zvuka",
|
||||||
|
"NotificationOptionCameraImageUploaded": "Učitana slika s kamere",
|
||||||
|
"NotificationOptionInstallationFailed": "Neuspjeh instalacije",
|
||||||
|
"NotificationOptionNewLibraryContent": "Dodan novi sadržaj",
|
||||||
|
"NotificationOptionPluginError": "Neuspjeh dodatka",
|
||||||
|
"NotificationOptionPluginInstalled": "Dodatak je instaliran",
|
||||||
|
"NotificationOptionPluginUninstalled": "Dodatak je deinstaliran",
|
||||||
|
"NotificationOptionPluginUpdateInstalled": "Ažuriranje dodatka je instalirano",
|
||||||
|
"NotificationOptionServerRestartRequired": "Potreban je ponovni pokret servera",
|
||||||
|
"NotificationOptionTaskFailed": "Neuspjeh zakazane zadatke",
|
||||||
|
"NotificationOptionUserLockedOut": "Korisnik je zaključan",
|
||||||
|
"NotificationOptionVideoPlayback": "Pokrenuto je reproduciranje videa",
|
||||||
|
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videa je zaustavljena",
|
||||||
|
"Plugin": "Plugin",
|
||||||
|
"PluginInstalledWithName": "{0} je instaliran",
|
||||||
|
"PluginUninstalledWithName": "{0} je deinstaliran",
|
||||||
|
"PluginUpdatedWithName": "{0} je ažurirano",
|
||||||
|
"ProviderValue": "Pružatelj: {0}",
|
||||||
|
"ScheduledTaskFailedWithName": "{0} nije uspjelo",
|
||||||
|
"ScheduledTaskStartedWithName": "{0} počelo",
|
||||||
|
"ServerNameNeedsToBeRestarted": "{0} treba ponovo pokrenuti",
|
||||||
|
"StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Molimo pokušajte ponovo za kratko vrijeme.",
|
||||||
|
"SubtitleDownloadFailureFromForItem": "Podtitlovi nisu uspjeli preuzeti sa {0} za {1}",
|
||||||
|
"Sync": "Sinkronizacija",
|
||||||
|
"System": "Sistem",
|
||||||
|
"TvShows": "TV serije",
|
||||||
|
"Undefined": "Nedefinirano",
|
||||||
|
"User": "Korisnik",
|
||||||
|
"UserCreatedWithName": "Korisnik {0} je kreiran",
|
||||||
|
"UserDeletedWithName": "Korisnik {0} je izbrisan",
|
||||||
|
"UserDownloadingItemWithValues": "{0} preuzima {1}",
|
||||||
|
"UserLockedOutWithName": "Korisnik {0} je zaključan",
|
||||||
|
"UserOfflineFromDevice": "{0} se odspojio od {1}",
|
||||||
|
"UserOnlineFromDevice": "{0} je online od {1}",
|
||||||
|
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
|
||||||
|
"UserPolicyUpdatedWithName": "Pravila za korisnike su ažurirana za {0}",
|
||||||
|
"UserStartedPlayingItemWithValues": "{0} igra protiv {1} na {2}",
|
||||||
|
"UserStoppedPlayingItemWithValues": "{0} je završio igru protiv {1} na {2}",
|
||||||
|
"ValueHasBeenAddedToLibrary": "{0} je dodan u vašu medijsku biblioteku",
|
||||||
|
"VersionNumber": "Verzija {0}",
|
||||||
|
"TasksMaintenanceCategory": "Održavanje",
|
||||||
|
"TasksLibraryCategory": "Biblioteka",
|
||||||
|
"TasksApplicationCategory": "Prijava",
|
||||||
|
"TasksChannelsCategory": "Internetski kanali",
|
||||||
|
"TaskCleanActivityLog": "Očisti dnevnik aktivnosti",
|
||||||
|
"TaskCleanActivityLogDescription": "Brisanje unosa u dnevnik aktivnosti starijih od konfigurisane starosti.",
|
||||||
|
"TaskCleanCache": "Očistite direktorij keša",
|
||||||
|
"TaskCleanCacheDescription": "Brisanje keš datoteka koje sistemu više nisu potrebne.",
|
||||||
|
"TaskRefreshChapterImages": "Izvadi slike iz poglavlja",
|
||||||
|
"TaskRefreshChapterImagesDescription": "Stvara minijature za videozapise koji imaju poglavlja.",
|
||||||
|
"TaskAudioNormalization": "Normalizacija zvuka",
|
||||||
|
"TaskAudioNormalizationDescription": "Skeneriše datoteke radi podataka za normalizaciju zvuka.",
|
||||||
|
"TaskRefreshLibrary": "Skenerisati medijsku biblioteku",
|
||||||
|
"TaskRefreshLibraryDescription": "Skenerira vašu medijsku biblioteku na nove datoteke i osvježava metapodatke.",
|
||||||
|
"TaskCleanLogs": "Očisti direktorij dnevnika",
|
||||||
|
"TaskCleanLogsDescription": "Brisanje dnevničkih datoteka starijih od {0} dana.",
|
||||||
|
"TaskRefreshPeople": "Osvježite ljude",
|
||||||
|
"TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i režisere u vašoj medijskoj biblioteci.",
|
||||||
|
"TaskRefreshTrickplayImages": "Generirajte Trickplay slike",
|
||||||
|
"TaskRefreshTrickplayImagesDescription": "Stvara pregled trik-igara za videozapise u omogućenim bibliotekama.",
|
||||||
|
"TaskUpdatePlugins": "Ažuriraj dodatke",
|
||||||
|
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja dodataka koji su konfigurisani da se automatski ažuriraju.",
|
||||||
|
"TaskCleanTranscode": "Očisti Transcode direktorij",
|
||||||
|
"TaskCleanTranscodeDescription": "Brisanje transkodiranih datoteka starijih od jednog dana.",
|
||||||
|
"TaskRefreshChannels": "Osvježi kanale",
|
||||||
|
"TaskRefreshChannelsDescription": "Osvježava informacije o internetskom kanalu.",
|
||||||
|
"TaskDownloadMissingLyrics": "Preuzmi nedostajuće tekstove",
|
||||||
|
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
|
||||||
|
"TaskDownloadMissingSubtitles": "Preuzmite nedostajuće titlove",
|
||||||
|
"TaskDownloadMissingSubtitlesDescription": "Pretražuje internet u potrazi za nedostajućim titlovima na osnovu konfiguracije metapodataka.",
|
||||||
|
"TaskOptimizeDatabase": "Optimizirajte bazu podataka",
|
||||||
|
"TaskOptimizeDatabaseDescription": "Komprimira bazu podataka i čisti slobodan prostor. Pokretanje ovog zadatka nakon skeniranja biblioteke ili izvođenja drugih promjena koje podrazumijevaju izmjene baze podataka može poboljšati performanse.",
|
||||||
|
"TaskKeyframeExtractor": "Izvađač ključnih sličica",
|
||||||
|
"TaskKeyframeExtractorDescription": "Izvlači ključne okvire iz video datoteka kako bi kreirao preciznije HLS playliste. Ovaj zadatak može trajati dugo.",
|
||||||
|
"TaskExtractMediaSegments": "Analiza medijskog segmenta",
|
||||||
|
"TaskExtractMediaSegmentsDescription": "Izvlači ili dobija medijske segmente iz dodataka koji podržavaju MediaSegment.",
|
||||||
|
"TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay",
|
||||||
|
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke trik-igara prema postavkama biblioteke.",
|
||||||
|
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
|
||||||
|
"CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana."
|
||||||
|
}
|
||||||
@@ -63,8 +63,8 @@
|
|||||||
"Photos": "Fotos",
|
"Photos": "Fotos",
|
||||||
"Playlists": "Llistes de reproducció",
|
"Playlists": "Llistes de reproducció",
|
||||||
"Plugin": "Complement",
|
"Plugin": "Complement",
|
||||||
"PluginInstalledWithName": "{0} ha estat instal·lat",
|
"PluginInstalledWithName": "S'ha instal·lat {0}",
|
||||||
"PluginUninstalledWithName": "S'ha instal·lat {0}",
|
"PluginUninstalledWithName": "S'ha desinstal·lat {0}",
|
||||||
"PluginUpdatedWithName": "S'ha actualitzat {0}",
|
"PluginUpdatedWithName": "S'ha actualitzat {0}",
|
||||||
"ProviderValue": "Proveïdor: {0}",
|
"ProviderValue": "Proveïdor: {0}",
|
||||||
"ScheduledTaskFailedWithName": "{0} ha fallat",
|
"ScheduledTaskFailedWithName": "{0} ha fallat",
|
||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Sèries",
|
"Shows": "Sèries",
|
||||||
"Songs": "Cançons",
|
"Songs": "Cançons",
|
||||||
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.",
|
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
|
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
|
||||||
"Sync": "Sincronitza",
|
"Sync": "Sincronitza",
|
||||||
"System": "Sistema",
|
"System": "Sistema",
|
||||||
@@ -105,7 +104,7 @@
|
|||||||
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
|
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
|
||||||
"TaskCleanLogs": "Neteja dels registres",
|
"TaskCleanLogs": "Neteja dels registres",
|
||||||
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
|
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
|
||||||
"TaskRefreshLibrary": "Escaneig de les mediateques",
|
"TaskRefreshLibrary": "Escaneja la mediateca",
|
||||||
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
|
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
|
||||||
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
|
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
|
||||||
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
|
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
|
||||||
@@ -127,8 +126,6 @@
|
|||||||
"HearingImpaired": "Discapacitat auditiva",
|
"HearingImpaired": "Discapacitat auditiva",
|
||||||
"TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
|
"TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.",
|
"TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
|
|
||||||
"TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció",
|
|
||||||
"TaskAudioNormalization": "Estabilització de l'àudio",
|
"TaskAudioNormalization": "Estabilització de l'àudio",
|
||||||
"TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.",
|
"TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.",
|
||||||
"TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
|
"TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Seriály",
|
"Shows": "Seriály",
|
||||||
"Songs": "Skladby",
|
"Songs": "Skladby",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
|
||||||
"SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo",
|
"SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo",
|
||||||
"Sync": "Synchronizace",
|
"Sync": "Synchronizace",
|
||||||
"System": "Systém",
|
"System": "Systém",
|
||||||
@@ -127,8 +126,6 @@
|
|||||||
"HearingImpaired": "Sluchově postižení",
|
"HearingImpaired": "Sluchově postižení",
|
||||||
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
|
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.",
|
"TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání.",
|
|
||||||
"TaskAudioNormalization": "Normalizace zvuku",
|
"TaskAudioNormalization": "Normalizace zvuku",
|
||||||
"TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.",
|
"TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.",
|
||||||
"TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni",
|
"TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
|
"CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
|
||||||
"Books": "Llyfrau",
|
"Books": "Llyfrau",
|
||||||
"AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus",
|
"AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus",
|
||||||
"Artists": "Artistiaid",
|
"Artists": "Crewyr",
|
||||||
"AppDeviceValues": "Ap: {0}, Dyfais: {1}",
|
"AppDeviceValues": "Ap: {0}, Dyfais: {1}",
|
||||||
"Albums": "Albwmau",
|
"Albums": "Albwmau",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain",
|
"NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain",
|
||||||
"MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru",
|
"MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru",
|
||||||
"FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu gan {0}",
|
"FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu o {0}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau",
|
"ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}",
|
"UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}",
|
||||||
"UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}",
|
"UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}",
|
||||||
@@ -123,5 +123,12 @@
|
|||||||
"TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
|
"TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
|
||||||
"TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
|
"TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
|
||||||
"TaskCleanCache": "Gwaghau Ffolder Cache",
|
"TaskCleanCache": "Gwaghau Ffolder Cache",
|
||||||
"HearingImpaired": "Nam ar y clyw"
|
"HearingImpaired": "Nam ar y clyw",
|
||||||
|
"TaskAudioNormalization": "Gwastatau Sain",
|
||||||
|
"TaskAudioNormalizationDescription": "Yn sganio ffeiliau am ddata gwastatau sain.",
|
||||||
|
"TaskRefreshTrickplayImages": "Creuwch lluniau Trickplay",
|
||||||
|
"TaskRefreshTrickplayImagesDescription": "Creu rhagolygon Trickplay ar gyfer fideos mewn llyfrgelloedd gweithredol.",
|
||||||
|
"TaskDownloadMissingLyrics": "Lawrlwytho geiriau coll",
|
||||||
|
"TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon",
|
||||||
|
"TaskExtractMediaSegments": "Sganio Darnau Cyfryngau"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Serier",
|
"Shows": "Serier",
|
||||||
"Songs": "Sange",
|
"Songs": "Sange",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
|
"StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
|
||||||
"SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
|
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
|
||||||
"Sync": "Synkroniser",
|
"Sync": "Synkroniser",
|
||||||
"System": "System",
|
"System": "System",
|
||||||
@@ -127,8 +126,6 @@
|
|||||||
"HearingImpaired": "Hørehæmmet",
|
"HearingImpaired": "Hørehæmmet",
|
||||||
"TaskRefreshTrickplayImages": "Generer trickplay-billeder",
|
"TaskRefreshTrickplayImages": "Generer trickplay-billeder",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Laver trickplay-billeder for videoer i aktiverede biblioteker.",
|
"TaskRefreshTrickplayImagesDescription": "Laver trickplay-billeder for videoer i aktiverede biblioteker.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
|
|
||||||
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende lydnormalisering.",
|
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende lydnormalisering.",
|
||||||
"TaskAudioNormalization": "Lydnormalisering",
|
"TaskAudioNormalization": "Lydnormalisering",
|
||||||
"TaskDownloadMissingLyricsDescription": "Søger på internettet efter manglende sangtekster baseret på metadata-konfigurationen",
|
"TaskDownloadMissingLyricsDescription": "Søger på internettet efter manglende sangtekster baseret på metadata-konfigurationen",
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
"Channels": "Kanäle",
|
"Channels": "Kanäle",
|
||||||
"ChapterNameValue": "Kapitel {0}",
|
"ChapterNameValue": "Kapitel {0}",
|
||||||
"Collections": "Sammlungen",
|
"Collections": "Sammlungen",
|
||||||
"DeviceOfflineWithName": "{0} hat die Verbindung getrennt",
|
"DeviceOfflineWithName": "{0} ist offline",
|
||||||
"DeviceOnlineWithName": "{0} ist verbunden",
|
"DeviceOnlineWithName": "{0} ist online",
|
||||||
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
|
"FailedLoginAttemptWithUserName": "Anmeldung von {0} fehlgeschlagen",
|
||||||
"Favorites": "Favoriten",
|
"Favorites": "Favoriten",
|
||||||
"Folders": "Verzeichnisse",
|
"Folders": "Verzeichnisse",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
@@ -19,9 +19,9 @@
|
|||||||
"HeaderContinueWatching": "Weiterschauen",
|
"HeaderContinueWatching": "Weiterschauen",
|
||||||
"HeaderFavoriteAlbums": "Lieblingsalben",
|
"HeaderFavoriteAlbums": "Lieblingsalben",
|
||||||
"HeaderFavoriteArtists": "Lieblingsinterpreten",
|
"HeaderFavoriteArtists": "Lieblingsinterpreten",
|
||||||
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
|
"HeaderFavoriteEpisodes": "Lieblingsfolgen",
|
||||||
"HeaderFavoriteShows": "Lieblingsserien",
|
"HeaderFavoriteShows": "Lieblingsserien",
|
||||||
"HeaderFavoriteSongs": "Lieblingslieder",
|
"HeaderFavoriteSongs": "Lieblingssongs",
|
||||||
"HeaderLiveTV": "Live TV",
|
"HeaderLiveTV": "Live TV",
|
||||||
"HeaderNextUp": "Als Nächstes",
|
"HeaderNextUp": "Als Nächstes",
|
||||||
"HeaderRecordingGroups": "Aufnahme-Gruppen",
|
"HeaderRecordingGroups": "Aufnahme-Gruppen",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.",
|
"NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.",
|
||||||
"NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
|
"NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
|
||||||
"NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
|
"NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
|
||||||
"NotificationOptionAudioPlayback": "Audiowiedergabe gestartet",
|
"NotificationOptionAudioPlayback": "Audio wird abgespielt",
|
||||||
"NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt",
|
"NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt",
|
||||||
"NotificationOptionCameraImageUploaded": "Foto hochgeladen",
|
"NotificationOptionCameraImageUploaded": "Foto hochgeladen",
|
||||||
"NotificationOptionInstallationFailed": "Installation fehlgeschlagen",
|
"NotificationOptionInstallationFailed": "Installation fehlgeschlagen",
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
"NotificationOptionPluginUpdateInstalled": "Pluginaktualisierung installiert",
|
"NotificationOptionPluginUpdateInstalled": "Pluginaktualisierung installiert",
|
||||||
"NotificationOptionServerRestartRequired": "Serverneustart notwendig",
|
"NotificationOptionServerRestartRequired": "Serverneustart notwendig",
|
||||||
"NotificationOptionTaskFailed": "Geplante Aufgabe fehlgeschlagen",
|
"NotificationOptionTaskFailed": "Geplante Aufgabe fehlgeschlagen",
|
||||||
"NotificationOptionUserLockedOut": "Benutzer ausgeschlossen",
|
"NotificationOptionUserLockedOut": "Benutzer gesperrt",
|
||||||
"NotificationOptionVideoPlayback": "Videowiedergabe gestartet",
|
"NotificationOptionVideoPlayback": "Video wird abgespielt",
|
||||||
"NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt",
|
"NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt",
|
||||||
"Photos": "Fotos",
|
"Photos": "Fotos",
|
||||||
"Playlists": "Wiedergabelisten",
|
"Playlists": "Wiedergabelisten",
|
||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Serien",
|
"Shows": "Serien",
|
||||||
"Songs": "Lieder",
|
"Songs": "Lieder",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.",
|
"StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.",
|
||||||
"SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden",
|
"SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden",
|
||||||
"Sync": "Synchronisation",
|
"Sync": "Synchronisation",
|
||||||
"System": "System",
|
"System": "System",
|
||||||
@@ -82,7 +81,7 @@
|
|||||||
"UserCreatedWithName": "Benutzer {0} wurde erstellt",
|
"UserCreatedWithName": "Benutzer {0} wurde erstellt",
|
||||||
"UserDeletedWithName": "Benutzer {0} wurde gelöscht",
|
"UserDeletedWithName": "Benutzer {0} wurde gelöscht",
|
||||||
"UserDownloadingItemWithValues": "{0} lädt {1} herunter",
|
"UserDownloadingItemWithValues": "{0} lädt {1} herunter",
|
||||||
"UserLockedOutWithName": "Benutzer {0} wurde ausgeschlossen",
|
"UserLockedOutWithName": "Benutzer {0} wurde gesperrt",
|
||||||
"UserOfflineFromDevice": "{0} wurde getrennt von {1}",
|
"UserOfflineFromDevice": "{0} wurde getrennt von {1}",
|
||||||
"UserOnlineFromDevice": "{0} ist online von {1}",
|
"UserOnlineFromDevice": "{0} ist online von {1}",
|
||||||
"UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert",
|
"UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert",
|
||||||
@@ -97,25 +96,25 @@
|
|||||||
"TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.",
|
"TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.",
|
||||||
"TaskRefreshChannels": "Kanäle aktualisieren",
|
"TaskRefreshChannels": "Kanäle aktualisieren",
|
||||||
"TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.",
|
"TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.",
|
||||||
"TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen",
|
"TaskCleanTranscode": "Transkodierungsverzeichnis leeren",
|
||||||
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
|
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
|
||||||
"TaskUpdatePlugins": "Plugins aktualisieren",
|
"TaskUpdatePlugins": "Plugins aktualisieren",
|
||||||
"TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
|
"TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
|
||||||
"TaskRefreshPeople": "Personen aktualisieren",
|
"TaskRefreshPeople": "Personen aktualisieren",
|
||||||
"TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.",
|
"TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.",
|
||||||
"TaskCleanLogs": "Log-Verzeichnis aufräumen",
|
"TaskCleanLogs": "Protokollverzeichnis leeren",
|
||||||
"TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.",
|
"TaskRefreshLibraryDescription": "Durchsucht deine Medienbibliothek nach neuen Dateien und aktualisiert Metadaten.",
|
||||||
"TaskRefreshLibrary": "Medien-Bibliothek scannen",
|
"TaskRefreshLibrary": "Medien-Bibliothek scannen",
|
||||||
"TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.",
|
"TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videokapitel.",
|
||||||
"TaskRefreshChapterImages": "Kapitel-Bilder extrahieren",
|
"TaskRefreshChapterImages": "Kapitelvorschauen erstellen",
|
||||||
"TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.",
|
"TaskCleanCacheDescription": "Löscht Cache-Dateien, die vom System nicht mehr benötigt werden.",
|
||||||
"TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen",
|
"TaskCleanCache": "Cache-Verzeichnis leeren",
|
||||||
"TasksChannelsCategory": "Internet-Kanäle",
|
"TasksChannelsCategory": "Internet-Kanäle",
|
||||||
"TasksApplicationCategory": "Anwendung",
|
"TasksApplicationCategory": "Anwendung",
|
||||||
"TasksLibraryCategory": "Bibliothek",
|
"TasksLibraryCategory": "Bibliothek",
|
||||||
"TasksMaintenanceCategory": "Wartung",
|
"TasksMaintenanceCategory": "Wartung",
|
||||||
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
|
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
|
||||||
"TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen",
|
"TaskCleanActivityLog": "Aktivitätsverlauf bereinigen",
|
||||||
"Undefined": "Undefiniert",
|
"Undefined": "Undefiniert",
|
||||||
"Forced": "Erzwungen",
|
"Forced": "Erzwungen",
|
||||||
"Default": "Standard",
|
"Default": "Standard",
|
||||||
@@ -127,8 +126,6 @@
|
|||||||
"HearingImpaired": "Hörgeschädigt",
|
"HearingImpaired": "Hörgeschädigt",
|
||||||
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
|
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
|
"TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Löscht nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
|
|
||||||
"TaskAudioNormalization": "Audio Normalisierung",
|
"TaskAudioNormalization": "Audio Normalisierung",
|
||||||
"TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
|
"TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
|
||||||
"TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
|
"TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Σειρές",
|
"Shows": "Σειρές",
|
||||||
"Songs": "Τραγούδια",
|
"Songs": "Τραγούδια",
|
||||||
"StartupEmbyServerIsLoading": "Ο διακομιστής Jellyfin φορτώνει. Περιμένετε λίγο και δοκιμάστε ξανά.",
|
"StartupEmbyServerIsLoading": "Ο διακομιστής Jellyfin φορτώνει. Περιμένετε λίγο και δοκιμάστε ξανά.",
|
||||||
"SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}",
|
"SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}",
|
||||||
"Sync": "Συγχρονισμός",
|
"Sync": "Συγχρονισμός",
|
||||||
"System": "Σύστημα",
|
"System": "Σύστημα",
|
||||||
@@ -129,8 +128,6 @@
|
|||||||
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
|
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
|
||||||
"TaskAudioNormalization": "Ομοιομορφία ήχου",
|
"TaskAudioNormalization": "Ομοιομορφία ήχου",
|
||||||
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
|
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον.",
|
|
||||||
"TaskMoveTrickplayImages": "Αλλαγή τοποθεσίας εικόνων Trickplay",
|
"TaskMoveTrickplayImages": "Αλλαγή τοποθεσίας εικόνων Trickplay",
|
||||||
"TaskDownloadMissingLyrics": "Λήψη στίχων που λείπουν",
|
"TaskDownloadMissingLyrics": "Λήψη στίχων που λείπουν",
|
||||||
"TaskMoveTrickplayImagesDescription": "Μετακινεί τα υπάρχοντα αρχεία trickplay σύμφωνα με τις ρυθμίσεις της βιβλιοθήκης.",
|
"TaskMoveTrickplayImagesDescription": "Μετακινεί τα υπάρχοντα αρχεία trickplay σύμφωνα με τις ρυθμίσεις της βιβλιοθήκης.",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Shows",
|
"Shows": "Shows",
|
||||||
"Songs": "Songs",
|
"Songs": "Songs",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
||||||
"Sync": "Sync",
|
"Sync": "Sync",
|
||||||
"System": "System",
|
"System": "System",
|
||||||
@@ -127,8 +126,6 @@
|
|||||||
"HearingImpaired": "Hearing Impaired",
|
"HearingImpaired": "Hearing Impaired",
|
||||||
"TaskRefreshTrickplayImages": "Generate Trickplay Images",
|
"TaskRefreshTrickplayImages": "Generate Trickplay Images",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
|
"TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
|
|
||||||
"TaskAudioNormalization": "Audio Normalisation",
|
"TaskAudioNormalization": "Audio Normalisation",
|
||||||
"TaskAudioNormalizationDescription": "Scans files for audio normalisation data.",
|
"TaskAudioNormalizationDescription": "Scans files for audio normalisation data.",
|
||||||
"TaskDownloadMissingLyrics": "Download missing lyrics",
|
"TaskDownloadMissingLyrics": "Download missing lyrics",
|
||||||
|
|||||||
@@ -130,8 +130,6 @@
|
|||||||
"TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.",
|
"TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.",
|
||||||
"TaskKeyframeExtractor": "Keyframe Extractor",
|
"TaskKeyframeExtractor": "Keyframe Extractor",
|
||||||
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
|
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
|
|
||||||
"TaskExtractMediaSegments": "Media Segment Scan",
|
"TaskExtractMediaSegments": "Media Segment Scan",
|
||||||
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
|
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
|
||||||
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
|
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||||
"HeaderFavoriteEpisodes": "Capítulos favoritos",
|
"HeaderFavoriteEpisodes": "Capítulos favoritos",
|
||||||
"HeaderFavoriteShows": "Programas favoritos",
|
"HeaderFavoriteShows": "Series favoritas",
|
||||||
"HeaderFavoriteSongs": "Canciones favoritas",
|
"HeaderFavoriteSongs": "Canciones favoritas",
|
||||||
"HeaderLiveTV": "TV en vivo",
|
"HeaderLiveTV": "TV en vivo",
|
||||||
"HeaderNextUp": "Siguiente",
|
"HeaderNextUp": "Siguiente",
|
||||||
@@ -70,10 +70,9 @@
|
|||||||
"ScheduledTaskFailedWithName": "{0} falló",
|
"ScheduledTaskFailedWithName": "{0} falló",
|
||||||
"ScheduledTaskStartedWithName": "{0} iniciado",
|
"ScheduledTaskStartedWithName": "{0} iniciado",
|
||||||
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
|
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
|
||||||
"Shows": "Programas",
|
"Shows": "Series",
|
||||||
"Songs": "Canciones",
|
"Songs": "Canciones",
|
||||||
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
|
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}",
|
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}",
|
||||||
"Sync": "Sincronizar",
|
"Sync": "Sincronizar",
|
||||||
"System": "Sistema",
|
"System": "Sistema",
|
||||||
@@ -129,8 +128,6 @@
|
|||||||
"TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.",
|
"TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.",
|
||||||
"TaskAudioNormalization": "Normalización de audio",
|
"TaskAudioNormalization": "Normalización de audio",
|
||||||
"TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.",
|
"TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
|
|
||||||
"TaskDownloadMissingLyrics": "Descargar letra faltante",
|
"TaskDownloadMissingLyrics": "Descargar letra faltante",
|
||||||
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
|
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
|
||||||
"TaskExtractMediaSegments": "Escanear Segmentos de Media",
|
"TaskExtractMediaSegments": "Escanear Segmentos de Media",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Programas",
|
"Shows": "Programas",
|
||||||
"Songs": "Canciones",
|
"Songs": "Canciones",
|
||||||
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
|
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
|
||||||
"SubtitleDownloadFailureForItem": "Falló la descarga de subtítulos para {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
|
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
|
||||||
"Sync": "Sincronizar",
|
"Sync": "Sincronizar",
|
||||||
"System": "Sistema",
|
"System": "Sistema",
|
||||||
@@ -129,8 +128,6 @@
|
|||||||
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
|
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
|
||||||
"TaskAudioNormalization": "Normalización de audio",
|
"TaskAudioNormalization": "Normalización de audio",
|
||||||
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
|
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
|
|
||||||
"TaskDownloadMissingLyrics": "descargar letras que faltan",
|
"TaskDownloadMissingLyrics": "descargar letras que faltan",
|
||||||
"TaskDownloadMissingLyricsDescription": "Descargar letras de canciones",
|
"TaskDownloadMissingLyricsDescription": "Descargar letras de canciones",
|
||||||
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Series",
|
"Shows": "Series",
|
||||||
"Songs": "Canciones",
|
"Songs": "Canciones",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
|
||||||
"SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Fallo en la descarga de subtítulos desde {0} para {1}",
|
"SubtitleDownloadFailureFromForItem": "Fallo en la descarga de subtítulos desde {0} para {1}",
|
||||||
"Sync": "Sincronizar",
|
"Sync": "Sincronizar",
|
||||||
"System": "Sistema",
|
"System": "Sistema",
|
||||||
@@ -127,8 +126,6 @@
|
|||||||
"HearingImpaired": "Discapacidad Auditiva",
|
"HearingImpaired": "Discapacidad Auditiva",
|
||||||
"TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
|
"TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas.",
|
"TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
|
|
||||||
"TaskAudioNormalization": "Normalización de audio",
|
"TaskAudioNormalization": "Normalización de audio",
|
||||||
"TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización.",
|
"TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización.",
|
||||||
"TaskDownloadMissingLyricsDescription": "Descargar letras para las canciones",
|
"TaskDownloadMissingLyricsDescription": "Descargar letras para las canciones",
|
||||||
|
|||||||
@@ -127,9 +127,7 @@
|
|||||||
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
|
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
|
||||||
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
|
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
|
||||||
"TaskAudioNormalization": "Normalización de audio",
|
"TaskAudioNormalization": "Normalización de audio",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
|
|
||||||
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
|
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
|
|
||||||
"TaskDownloadMissingLyrics": "Descargar letra faltante",
|
"TaskDownloadMissingLyrics": "Descargar letra faltante",
|
||||||
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
|
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
|
||||||
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de complementos habilitados para MediaSegment.",
|
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de complementos habilitados para MediaSegment.",
|
||||||
|
|||||||
@@ -41,8 +41,6 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
||||||
"TaskAudioNormalization": "Normalización de audio",
|
"TaskAudioNormalization": "Normalización de audio",
|
||||||
"TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.",
|
"TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remover elementos de colecciones y listas de reproducción que no existen.",
|
|
||||||
"TvShows": "Series de TV",
|
"TvShows": "Series de TV",
|
||||||
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
|
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
|
||||||
"TaskRefreshChannels": "Actualizar canales",
|
"TaskRefreshChannels": "Actualizar canales",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
"NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
|
"NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
|
||||||
"NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.",
|
"NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.",
|
||||||
"NameSeasonUnknown": "Tundmatu hooaeg",
|
"NameSeasonUnknown": "Tundmatu hooaeg",
|
||||||
"NameSeasonNumber": "Hooaeg {0}",
|
"NameSeasonNumber": "{0}. hooaeg",
|
||||||
"NameInstallFailed": "{0} paigaldamine nurjus",
|
"NameInstallFailed": "{0} paigaldamine nurjus",
|
||||||
"MusicVideos": "Muusikavideod",
|
"MusicVideos": "Muusikavideod",
|
||||||
"Music": "Muusika",
|
"Music": "Muusika",
|
||||||
@@ -128,13 +128,11 @@
|
|||||||
"TaskRefreshTrickplayImagesDescription": "Loob trickplay eelvaated videotele lubatud meediakogudes.",
|
"TaskRefreshTrickplayImagesDescription": "Loob trickplay eelvaated videotele lubatud meediakogudes.",
|
||||||
"TaskAudioNormalization": "Normaliseeri helitugevus",
|
"TaskAudioNormalization": "Normaliseeri helitugevus",
|
||||||
"TaskAudioNormalizationDescription": "Otsib failidest helitugevuse normaliseerimise teavet.",
|
"TaskAudioNormalizationDescription": "Otsib failidest helitugevuse normaliseerimise teavet.",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest üksused, mida enam ei eksisteeri.",
|
|
||||||
"TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid",
|
|
||||||
"TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
|
"TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
|
||||||
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
|
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
|
||||||
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
|
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
|
||||||
"TaskExtractMediaSegments": "Skaneeri meediasegmente",
|
"TaskExtractMediaSegments": "Skaneeri meedialõike",
|
||||||
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
|
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.",
|
||||||
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
|
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
|
||||||
"CleanupUserDataTask": "Puhasta kasutajaandmed",
|
"CleanupUserDataTask": "Puhasta kasutajaandmed",
|
||||||
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."
|
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."
|
||||||
|
|||||||
@@ -130,8 +130,6 @@
|
|||||||
"TaskDownloadMissingLyrics": "Deskargatu falta diren letrak",
|
"TaskDownloadMissingLyrics": "Deskargatu falta diren letrak",
|
||||||
"TaskDownloadMissingLyricsDescription": "Deskargatu abestientzako letrak",
|
"TaskDownloadMissingLyricsDescription": "Deskargatu abestientzako letrak",
|
||||||
"TaskExtractMediaSegments": "Multimedia segmentuen eskaneoa",
|
"TaskExtractMediaSegments": "Multimedia segmentuen eskaneoa",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Jada existitzen ez diren bildumak eta erreprodukzio-zerrendak kentzen ditu.",
|
|
||||||
"TaskCleanCollectionsAndPlaylists": "Garbitu bildumak eta erreprodukzio-zerrendak",
|
|
||||||
"TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
|
"TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
|
||||||
"TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
|
"TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
|
||||||
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
|
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "سریالها",
|
"Shows": "سریالها",
|
||||||
"Songs": "موسیقیها",
|
"Songs": "موسیقیها",
|
||||||
"StartupEmbyServerIsLoading": "سرور Jellyfin در حال بارگیری است. لطفا کمی بعد دوباره تلاش کنید.",
|
"StartupEmbyServerIsLoading": "سرور Jellyfin در حال بارگیری است. لطفا کمی بعد دوباره تلاش کنید.",
|
||||||
"SubtitleDownloadFailureForItem": "دانلود زیرنویس برای {0} ناموفق بود",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "بارگیری زیرنویس برای {1} از {0} شکست خورد",
|
"SubtitleDownloadFailureFromForItem": "بارگیری زیرنویس برای {1} از {0} شکست خورد",
|
||||||
"Sync": "همگامسازی",
|
"Sync": "همگامسازی",
|
||||||
"System": "سیستم",
|
"System": "سیستم",
|
||||||
@@ -127,8 +126,6 @@
|
|||||||
"HearingImpaired": "مشکل شنوایی",
|
"HearingImpaired": "مشکل شنوایی",
|
||||||
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
|
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "تولید پیشنمایش های trickplay برای ویدیو های فعال شده در کتابخانه.",
|
"TaskRefreshTrickplayImagesDescription": "تولید پیشنمایش های trickplay برای ویدیو های فعال شده در کتابخانه.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "پاکسازی مجموعه ها و لیست پخش",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "موارد را از مجموعه ها و لیست پخش هایی که دیگر وجود ندارند حذف میکند.",
|
|
||||||
"TaskAudioNormalizationDescription": "بررسی فایل برای دادههای نرمال کردن صدا.",
|
"TaskAudioNormalizationDescription": "بررسی فایل برای دادههای نرمال کردن صدا.",
|
||||||
"TaskDownloadMissingLyrics": "دانلود متنهای ناموجود",
|
"TaskDownloadMissingLyrics": "دانلود متنهای ناموجود",
|
||||||
"TaskDownloadMissingLyricsDescription": "دانلود متن شعرها",
|
"TaskDownloadMissingLyricsDescription": "دانلود متن شعرها",
|
||||||
|
|||||||
@@ -39,8 +39,8 @@
|
|||||||
"Channels": "Kanavat",
|
"Channels": "Kanavat",
|
||||||
"CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}",
|
"CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}",
|
||||||
"Books": "Kirjat",
|
"Books": "Kirjat",
|
||||||
"AuthenticationSucceededWithUserName": "{0} on todennettu",
|
"AuthenticationSucceededWithUserName": "{0} todennus onnistunut",
|
||||||
"Artists": "Esittäjät",
|
"Artists": "Artistit",
|
||||||
"Application": "Sovellus",
|
"Application": "Sovellus",
|
||||||
"AppDeviceValues": "Sovellus: {0}, Laite: {1}",
|
"AppDeviceValues": "Sovellus: {0}, Laite: {1}",
|
||||||
"Albums": "Albumit",
|
"Albums": "Albumit",
|
||||||
@@ -126,8 +126,6 @@
|
|||||||
"HearingImpaired": "Kuulorajoitteinen",
|
"HearingImpaired": "Kuulorajoitteinen",
|
||||||
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
|
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
|
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
|
|
||||||
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
|
|
||||||
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
|
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
|
||||||
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja.",
|
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja.",
|
||||||
"TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka",
|
"TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka",
|
||||||
|
|||||||
@@ -14,5 +14,9 @@
|
|||||||
"DeviceOnlineWithName": "{0} er sambundið",
|
"DeviceOnlineWithName": "{0} er sambundið",
|
||||||
"Favorites": "Yndis",
|
"Favorites": "Yndis",
|
||||||
"Folders": "Mappur",
|
"Folders": "Mappur",
|
||||||
"Forced": "Kravt"
|
"Forced": "Kravt",
|
||||||
|
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
|
||||||
|
"HeaderFavoriteEpisodes": "Yndispartar",
|
||||||
|
"HeaderFavoriteSongs": "Yndissangir",
|
||||||
|
"LabelIpAddressValue": "IP atsetur: {0}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Séries",
|
"Shows": "Séries",
|
||||||
"Songs": "Chansons",
|
"Songs": "Chansons",
|
||||||
"StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.",
|
"StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
|
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
|
||||||
"Sync": "Synchroniser",
|
"Sync": "Synchroniser",
|
||||||
"System": "Système",
|
"System": "Système",
|
||||||
@@ -127,8 +126,6 @@
|
|||||||
"HearingImpaired": "Malentendants",
|
"HearingImpaired": "Malentendants",
|
||||||
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
|
|
||||||
"TaskAudioNormalization": "Normalisation audio",
|
"TaskAudioNormalization": "Normalisation audio",
|
||||||
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
|
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
|
||||||
"TaskExtractMediaSegments": "Analyse des segments de média",
|
"TaskExtractMediaSegments": "Analyse des segments de média",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Séries",
|
"Shows": "Séries",
|
||||||
"Songs": "Chansons",
|
"Songs": "Chansons",
|
||||||
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
|
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
|
||||||
"SubtitleDownloadFailureForItem": "Le téléchargement des sous-titres pour {0} a échoué.",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
|
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
|
||||||
"Sync": "Synchroniser",
|
"Sync": "Synchroniser",
|
||||||
"System": "Système",
|
"System": "Système",
|
||||||
@@ -127,8 +126,6 @@
|
|||||||
"HearingImpaired": "Malentendants",
|
"HearingImpaired": "Malentendants",
|
||||||
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
|
|
||||||
"TaskAudioNormalization": "Normalisation audio",
|
"TaskAudioNormalization": "Normalisation audio",
|
||||||
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
|
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
|
||||||
"TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons",
|
"TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons",
|
||||||
|
|||||||
@@ -29,12 +29,10 @@
|
|||||||
"TaskRefreshChannelsDescription": "Athnuachan eolas faoi chainéil idirlín.",
|
"TaskRefreshChannelsDescription": "Athnuachan eolas faoi chainéil idirlín.",
|
||||||
"TaskOptimizeDatabase": "Bunachar sonraí a bharrfheabhsú",
|
"TaskOptimizeDatabase": "Bunachar sonraí a bharrfheabhsú",
|
||||||
"TaskKeyframeExtractorDescription": "Baintear eochairfhrámaí as comhaid físe chun seinmliostaí HLS níos cruinne a chruthú. Féadfaidh an tasc seo a bheith ar siúl ar feadh i bhfad.",
|
"TaskKeyframeExtractorDescription": "Baintear eochairfhrámaí as comhaid físe chun seinmliostaí HLS níos cruinne a chruthú. Féadfaidh an tasc seo a bheith ar siúl ar feadh i bhfad.",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Baintear míreanna as bailiúcháin agus seinmliostaí nach ann dóibh a thuilleadh.",
|
|
||||||
"TaskDownloadMissingLyricsDescription": "Íosluchtaigh liricí do na hamhráin",
|
"TaskDownloadMissingLyricsDescription": "Íosluchtaigh liricí do na hamhráin",
|
||||||
"TaskUpdatePluginsDescription": "Íoslódálann agus suiteálann nuashonruithe do bhreiseáin atá cumraithe le nuashonrú go huathoibríoch.",
|
"TaskUpdatePluginsDescription": "Íoslódálann agus suiteálann nuashonruithe do bhreiseáin atá cumraithe le nuashonrú go huathoibríoch.",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "Déanann sé cuardach ar an idirlíon le haghaidh fotheidil atá ar iarraidh bunaithe ar chumraíocht meiteashonraí.",
|
"TaskDownloadMissingSubtitlesDescription": "Déanann sé cuardach ar an idirlíon le haghaidh fotheidil atá ar iarraidh bunaithe ar chumraíocht meiteashonraí.",
|
||||||
"TaskExtractMediaSegmentsDescription": "Sliocht nó faigheann codanna meán ó bhreiseáin chumasaithe MediaSegment.",
|
"TaskExtractMediaSegmentsDescription": "Sliocht nó faigheann codanna meán ó bhreiseáin chumasaithe MediaSegment.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Glan suas bailiúcháin agus seinmliostaí",
|
|
||||||
"TaskOptimizeDatabaseDescription": "Comhdhlúthaíonn bunachar sonraí agus gearrtar spás saor in aisce. Má ritheann tú an tasc seo tar éis scanadh a dhéanamh ar an leabharlann nó athruithe eile a dhéanamh a thugann le tuiscint gur cheart go bhfeabhsófaí an fheidhmíocht.",
|
"TaskOptimizeDatabaseDescription": "Comhdhlúthaíonn bunachar sonraí agus gearrtar spás saor in aisce. Má ritheann tú an tasc seo tar éis scanadh a dhéanamh ar an leabharlann nó athruithe eile a dhéanamh a thugann le tuiscint gur cheart go bhfeabhsófaí an fheidhmíocht.",
|
||||||
"TaskMoveTrickplayImagesDescription": "Bogtar comhaid trickplay atá ann cheana de réir socruithe na leabharlainne.",
|
"TaskMoveTrickplayImagesDescription": "Bogtar comhaid trickplay atá ann cheana de réir socruithe na leabharlainne.",
|
||||||
"AppDeviceValues": "Aip: {0}, Gléas: {1}",
|
"AppDeviceValues": "Aip: {0}, Gléas: {1}",
|
||||||
|
|||||||
@@ -128,8 +128,6 @@
|
|||||||
"TaskRefreshTrickplayImagesDescription": "Crea miniaturas de previsualización para os vídeos nas bibliotecas habilitadas.",
|
"TaskRefreshTrickplayImagesDescription": "Crea miniaturas de previsualización para os vídeos nas bibliotecas habilitadas.",
|
||||||
"TaskDownloadMissingLyrics": "Descargar letras que faltan",
|
"TaskDownloadMissingLyrics": "Descargar letras que faltan",
|
||||||
"TaskDownloadMissingLyricsDescription": "Descarga as letras das cancións",
|
"TaskDownloadMissingLyricsDescription": "Descarga as letras das cancións",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Quita ítems que xa non existen das coleccións e listas de reprodución.",
|
|
||||||
"TaskExtractMediaSegmentsDescription": "Procura segmentos de medios cos plugins habilitados.",
|
"TaskExtractMediaSegmentsDescription": "Procura segmentos de medios cos plugins habilitados.",
|
||||||
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
||||||
"TaskMoveTrickplayImages": "Migrar as miniaturas de previsualización a outra ubicación",
|
"TaskMoveTrickplayImages": "Migrar as miniaturas de previsualización a outra ubicación",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Serie",
|
"Shows": "Serie",
|
||||||
"Songs": "Lieder",
|
"Songs": "Lieder",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde",
|
"SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde",
|
||||||
"Sync": "Synchronisation",
|
"Sync": "Synchronisation",
|
||||||
"System": "System",
|
"System": "System",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "סדרות",
|
"Shows": "סדרות",
|
||||||
"Songs": "שירים",
|
"Songs": "שירים",
|
||||||
"StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
|
"StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
|
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
|
||||||
"Sync": "סנכרון",
|
"Sync": "סנכרון",
|
||||||
"System": "מערכת",
|
"System": "מערכת",
|
||||||
@@ -128,9 +127,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "יצירת תמונות Trickplay",
|
"TaskRefreshTrickplayImages": "יצירת תמונות Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.",
|
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.",
|
||||||
"TaskAudioNormalization": "נרמול שמע",
|
"TaskAudioNormalization": "נרמול שמע",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
|
|
||||||
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
|
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה",
|
|
||||||
"TaskDownloadMissingLyrics": "הורדת מילים חסרות",
|
"TaskDownloadMissingLyrics": "הורדת מילים חסרות",
|
||||||
"TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
|
"TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
|
||||||
"TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",
|
"TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",
|
||||||
|
|||||||
@@ -1 +1,27 @@
|
|||||||
{}
|
{
|
||||||
|
"Books": "ספרים",
|
||||||
|
"NameSeasonNumber": "עונה {0}",
|
||||||
|
"Channels": "ערוצים",
|
||||||
|
"Movies": "סרטים",
|
||||||
|
"Music": "מוזיקה",
|
||||||
|
"Collections": "אוספים",
|
||||||
|
"Albums": "אלבומים",
|
||||||
|
"Application": "אפליקציה",
|
||||||
|
"Artists": "אמנים",
|
||||||
|
"ChapterNameValue": "פרק {0}",
|
||||||
|
"External": "חיצונית",
|
||||||
|
"Favorites": "מועדפים",
|
||||||
|
"Folders": "תיקיות",
|
||||||
|
"Genres": "ז'אנרים",
|
||||||
|
"HeaderAlbumArtists": "אמני אלבומים",
|
||||||
|
"HeaderContinueWatching": "להמשיך לצפות",
|
||||||
|
"HeaderFavoriteAlbums": "אלבומים אהובים",
|
||||||
|
"HeaderFavoriteArtists": "אמנים אהובים",
|
||||||
|
"HeaderFavoriteEpisodes": "פרקים אהובים",
|
||||||
|
"HeaderFavoriteShows": "תוכניות אהובות",
|
||||||
|
"HeaderFavoriteSongs": "שירים אהובים",
|
||||||
|
"HeaderLiveTV": "טלוויזיה בשידור חי",
|
||||||
|
"HeaderNextUp": "הבא",
|
||||||
|
"HearingImpaired": "ללקויי שמיעה",
|
||||||
|
"HomeVideos": "סרטונים ביתיים"
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,14 +127,12 @@
|
|||||||
"TaskRefreshTrickplayImages": "ट्रिकप्लै चित्रों को सृजन करे",
|
"TaskRefreshTrickplayImages": "ट्रिकप्लै चित्रों को सृजन करे",
|
||||||
"TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे.",
|
"TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे.",
|
||||||
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
|
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
|
||||||
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
|
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें।",
|
||||||
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
|
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
|
||||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
|
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
|
||||||
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
|
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
|
||||||
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
|
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
|
||||||
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
|
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
|
||||||
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
|
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
|
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य"
|
||||||
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
|
|
||||||
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
|
"CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
|
||||||
"Channels": "Kanali",
|
"Channels": "Kanali",
|
||||||
"ChapterNameValue": "Poglavlje {0}",
|
"ChapterNameValue": "Poglavlje {0}",
|
||||||
"Collections": "Kolekcije",
|
"Collections": "Zbirke",
|
||||||
"DeviceOfflineWithName": "{0} je prekinuo vezu",
|
"DeviceOfflineWithName": "{0} je prekinuo vezu",
|
||||||
"DeviceOnlineWithName": "{0} je povezan",
|
"DeviceOnlineWithName": "{0} je povezan",
|
||||||
"FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
|
"FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"HeaderFavoriteShows": "Omiljene serije",
|
"HeaderFavoriteShows": "Omiljene serije",
|
||||||
"HeaderFavoriteSongs": "Omiljene pjesme",
|
"HeaderFavoriteSongs": "Omiljene pjesme",
|
||||||
"HeaderLiveTV": "TV uživo",
|
"HeaderLiveTV": "TV uživo",
|
||||||
"HeaderNextUp": "Slijedi",
|
"HeaderNextUp": "Sljedeće na redu",
|
||||||
"HeaderRecordingGroups": "Grupa snimka",
|
"HeaderRecordingGroups": "Grupa snimka",
|
||||||
"HomeVideos": "Kućni video",
|
"HomeVideos": "Kućni video",
|
||||||
"Inherit": "Naslijedi",
|
"Inherit": "Naslijedi",
|
||||||
@@ -70,14 +70,13 @@
|
|||||||
"ScheduledTaskFailedWithName": "{0} neuspjelo",
|
"ScheduledTaskFailedWithName": "{0} neuspjelo",
|
||||||
"ScheduledTaskStartedWithName": "{0} pokrenuto",
|
"ScheduledTaskStartedWithName": "{0} pokrenuto",
|
||||||
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
|
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
|
||||||
"Shows": "Serije",
|
"Shows": "Emisije",
|
||||||
"Songs": "Pjesme",
|
"Songs": "Pjesme",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
|
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
|
||||||
"SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
|
"SubtitleDownloadFailureFromForItem": "Titlovi nisu uspješno preuzeti od {0} za {1}",
|
||||||
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
|
|
||||||
"Sync": "Sinkronizacija",
|
"Sync": "Sinkronizacija",
|
||||||
"System": "Sustav",
|
"System": "Sustav",
|
||||||
"TvShows": "Serije",
|
"TvShows": "TV emisije",
|
||||||
"User": "Korisnik",
|
"User": "Korisnik",
|
||||||
"UserCreatedWithName": "Korisnik {0} je kreiran",
|
"UserCreatedWithName": "Korisnik {0} je kreiran",
|
||||||
"UserDeletedWithName": "Korisnik {0} je obrisan",
|
"UserDeletedWithName": "Korisnik {0} je obrisan",
|
||||||
@@ -89,26 +88,26 @@
|
|||||||
"UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
|
"UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
|
||||||
"UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
|
"UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
|
"UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
|
"ValueHasBeenAddedToLibrary": "{0} je dodano u biblioteku medija",
|
||||||
"ValueSpecialEpisodeName": "Posebno - {0}",
|
"ValueSpecialEpisodeName": "Posebno – {0}",
|
||||||
"VersionNumber": "Verzija {0}",
|
"VersionNumber": "Verzija {0}",
|
||||||
"TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
|
"TaskRefreshLibraryDescription": "Skenira biblioteku medija radi novih datoteka i osvježava metapodatke.",
|
||||||
"TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
|
"TaskRefreshLibrary": "Skeniraj biblioteku medija",
|
||||||
"TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
|
"TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
|
||||||
"TaskRefreshChapterImages": "Izdvoji slike poglavlja",
|
"TaskRefreshChapterImages": "Izdvoji slike poglavlja",
|
||||||
"TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
|
"TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
|
||||||
"TaskCleanCache": "Očisti mapu predmemorije",
|
"TaskCleanCache": "Očisti mapu predmemorije",
|
||||||
"TasksApplicationCategory": "Aplikacija",
|
"TasksApplicationCategory": "Aplikacija",
|
||||||
"TasksMaintenanceCategory": "Održavanje",
|
"TasksMaintenanceCategory": "Održavanje",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
|
"TaskDownloadMissingSubtitlesDescription": "Pretraži internet za nedsotajućim titlovima ne osnovi konfiguracije metapodataka.",
|
||||||
"TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
|
"TaskDownloadMissingSubtitles": "Preuzmi nedostajuće titlove",
|
||||||
"TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
|
"TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
|
||||||
"TaskRefreshChannels": "Osvježi kanale",
|
"TaskRefreshChannels": "Osvježi kanale",
|
||||||
"TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
|
"TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
|
||||||
"TaskCleanTranscode": "Očisti mapu transkodiranja",
|
"TaskCleanTranscode": "Očisti mapu transkodiranja",
|
||||||
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
|
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
|
||||||
"TaskUpdatePlugins": "Ažuriraj dodatke",
|
"TaskUpdatePlugins": "Ažuriraj dodatke",
|
||||||
"TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
|
"TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u biblioteci medija.",
|
||||||
"TaskRefreshPeople": "Osvježi osobe",
|
"TaskRefreshPeople": "Osvježi osobe",
|
||||||
"TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
|
"TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
|
||||||
"TaskCleanLogs": "Očisti mapu dnevnika zapisa",
|
"TaskCleanLogs": "Očisti mapu dnevnika zapisa",
|
||||||
@@ -120,7 +119,7 @@
|
|||||||
"Forced": "Forsirani",
|
"Forced": "Forsirani",
|
||||||
"Default": "Zadano",
|
"Default": "Zadano",
|
||||||
"TaskOptimizeDatabase": "Optimiziraj bazu podataka",
|
"TaskOptimizeDatabase": "Optimiziraj bazu podataka",
|
||||||
"External": "Vanjski",
|
"External": "Eksterni",
|
||||||
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
|
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
|
||||||
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
|
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
|
||||||
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
|
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
|
||||||
@@ -129,14 +128,12 @@
|
|||||||
"TaskRefreshTrickplayImagesDescription": "Stvara preglede brzog pregledavanja za videa u aktiviranim bibliotekama.",
|
"TaskRefreshTrickplayImagesDescription": "Stvara preglede brzog pregledavanja za videa u aktiviranim bibliotekama.",
|
||||||
"TaskAudioNormalization": "Normalizacija zvuka",
|
"TaskAudioNormalization": "Normalizacija zvuka",
|
||||||
"TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.",
|
"TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz zbirki i popisa za reprodukciju koje više ne postoje.",
|
|
||||||
"TaskCleanCollectionsAndPlaylists": "Očisti zbirke i popise za reprodukciju",
|
|
||||||
"TaskExtractMediaSegments": "Skeniranje dijelova medija",
|
"TaskExtractMediaSegments": "Skeniranje dijelova medija",
|
||||||
"TaskDownloadMissingLyrics": "Preuzmi tekstove koji nedostaju",
|
"TaskDownloadMissingLyrics": "Preuzmi tekstove koji nedostaju",
|
||||||
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
|
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
|
||||||
"TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.",
|
"TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.",
|
||||||
"TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja",
|
"TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja",
|
||||||
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja prema postavkama biblioteke.",
|
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja u postavke biblioteke.",
|
||||||
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
|
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
|
||||||
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana."
|
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,5 +58,8 @@
|
|||||||
"ValueSpecialEpisodeName": "Spesyal - {0}",
|
"ValueSpecialEpisodeName": "Spesyal - {0}",
|
||||||
"VersionNumber": "Vesyon {0}",
|
"VersionNumber": "Vesyon {0}",
|
||||||
"TasksApplicationCategory": "Aplikasyon",
|
"TasksApplicationCategory": "Aplikasyon",
|
||||||
"TasksMaintenanceCategory": "Antretyen"
|
"TasksMaintenanceCategory": "Antretyen",
|
||||||
|
"AppDeviceValues": "Aplikasyon: {0}, Aparèy: {1}",
|
||||||
|
"AuthenticationSucceededWithUserName": "{0} otantifye avèk siksè",
|
||||||
|
"CameraImageUploadedFrom": "Une nouvelle image de la caméra a été téléchargée depuis {0}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
"NotificationOptionPluginInstalled": "Bővítmény telepítve",
|
"NotificationOptionPluginInstalled": "Bővítmény telepítve",
|
||||||
"NotificationOptionPluginUninstalled": "Bővítmény eltávolítva",
|
"NotificationOptionPluginUninstalled": "Bővítmény eltávolítva",
|
||||||
"NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve",
|
"NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve",
|
||||||
"NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges",
|
"NotificationOptionServerRestartRequired": "A szerver újraindítása szükséges",
|
||||||
"NotificationOptionTaskFailed": "Hiba az ütemezett feladatban",
|
"NotificationOptionTaskFailed": "Hiba az ütemezett feladatban",
|
||||||
"NotificationOptionUserLockedOut": "Felhasználó tiltva",
|
"NotificationOptionUserLockedOut": "Felhasználó tiltva",
|
||||||
"NotificationOptionVideoPlayback": "Videólejátszás elkezdve",
|
"NotificationOptionVideoPlayback": "Videólejátszás elkezdve",
|
||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Sorozatok",
|
"Shows": "Sorozatok",
|
||||||
"Songs": "Számok",
|
"Songs": "Számok",
|
||||||
"StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.",
|
"StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}",
|
"SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}",
|
||||||
"Sync": "Szinkronizálás",
|
"Sync": "Szinkronizálás",
|
||||||
"System": "Rendszer",
|
"System": "Rendszer",
|
||||||
@@ -128,9 +127,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Trickplay képek előállítása",
|
"TaskRefreshTrickplayImages": "Trickplay képek előállítása",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.",
|
"TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.",
|
||||||
"TaskAudioNormalization": "Hangerő-normalizálás",
|
"TaskAudioNormalization": "Hangerő-normalizálás",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.",
|
|
||||||
"TaskAudioNormalizationDescription": "Hangerő-normalizálási adatok keresése.",
|
"TaskAudioNormalizationDescription": "Hangerő-normalizálási adatok keresése.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása",
|
|
||||||
"TaskExtractMediaSegments": "Médiaszegmens felismerése",
|
"TaskExtractMediaSegments": "Médiaszegmens felismerése",
|
||||||
"TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése",
|
"TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése",
|
||||||
"TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
|
"TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
|
||||||
|
|||||||
@@ -128,8 +128,6 @@
|
|||||||
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan.",
|
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan.",
|
||||||
"TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
|
"TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
|
||||||
"TaskAudioNormalization": "Normalisasi Audio",
|
"TaskAudioNormalization": "Normalisasi Audio",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada.",
|
|
||||||
"TaskDownloadMissingLyricsDescription": "Unduh lirik untuk lagu",
|
"TaskDownloadMissingLyricsDescription": "Unduh lirik untuk lagu",
|
||||||
"TaskExtractMediaSegmentsDescription": "Mengekstrak atau memperoleh segmen media dari plugin yang mendukung MediaSegment.",
|
"TaskExtractMediaSegmentsDescription": "Mengekstrak atau memperoleh segmen media dari plugin yang mendukung MediaSegment.",
|
||||||
"TaskMoveTrickplayImagesDescription": "Memindahkan file trickplay yang sudah ada sesuai dengan pengaturan pustaka.",
|
"TaskMoveTrickplayImagesDescription": "Memindahkan file trickplay yang sudah ada sesuai dengan pengaturan pustaka.",
|
||||||
|
|||||||
@@ -128,8 +128,6 @@
|
|||||||
"TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir",
|
"TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir",
|
||||||
"TaskAudioNormalization": "Hljóðstöðlun",
|
"TaskAudioNormalization": "Hljóðstöðlun",
|
||||||
"TaskAudioNormalizationDescription": "Leitar að hljóðstöðlunargögnum í skrám.",
|
"TaskAudioNormalizationDescription": "Leitar að hljóðstöðlunargögnum í skrám.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Hreinsa söfn og spilunarlista",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjarlægir hluti úr söfnum og spilalistum sem eru ekki lengur til.",
|
|
||||||
"TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
|
"TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
|
||||||
"TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar",
|
"TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar",
|
||||||
"TaskExtractMediaSegments": "Skönnun efnishluta",
|
"TaskExtractMediaSegments": "Skönnun efnishluta",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
||||||
"Application": "Applicazione",
|
"Application": "Applicazione",
|
||||||
"Artists": "Artisti",
|
"Artists": "Artisti",
|
||||||
"AuthenticationSucceededWithUserName": "{0} autenticato con successo",
|
"AuthenticationSucceededWithUserName": "{0} autenticato correttamente",
|
||||||
"Books": "Libri",
|
"Books": "Libri",
|
||||||
"CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}",
|
"CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}",
|
||||||
"Channels": "Canali",
|
"Channels": "Canali",
|
||||||
@@ -11,36 +11,36 @@
|
|||||||
"Collections": "Collezioni",
|
"Collections": "Collezioni",
|
||||||
"DeviceOfflineWithName": "{0} si è disconnesso",
|
"DeviceOfflineWithName": "{0} si è disconnesso",
|
||||||
"DeviceOnlineWithName": "{0} è connesso",
|
"DeviceOnlineWithName": "{0} è connesso",
|
||||||
"FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}",
|
"FailedLoginAttemptWithUserName": "Tentativo di accesso non riuscito da {0}",
|
||||||
"Favorites": "Preferiti",
|
"Favorites": "Preferiti",
|
||||||
"Folders": "Cartelle",
|
"Folders": "Cartelle",
|
||||||
"Genres": "Generi",
|
"Genres": "Generi",
|
||||||
"HeaderAlbumArtists": "Artisti dell'album",
|
"HeaderAlbumArtists": "Artisti dell'album",
|
||||||
"HeaderContinueWatching": "Continua a guardare",
|
"HeaderContinueWatching": "Continua a guardare",
|
||||||
"HeaderFavoriteAlbums": "Album Preferiti",
|
"HeaderFavoriteAlbums": "Album preferiti",
|
||||||
"HeaderFavoriteArtists": "Artisti Preferiti",
|
"HeaderFavoriteArtists": "Artisti preferiti",
|
||||||
"HeaderFavoriteEpisodes": "Episodi Preferiti",
|
"HeaderFavoriteEpisodes": "Episodi preferiti",
|
||||||
"HeaderFavoriteShows": "Serie TV Preferite",
|
"HeaderFavoriteShows": "Serie TV preferite",
|
||||||
"HeaderFavoriteSongs": "Brani Preferiti",
|
"HeaderFavoriteSongs": "Brani preferiti",
|
||||||
"HeaderLiveTV": "Diretta TV",
|
"HeaderLiveTV": "Diretta TV",
|
||||||
"HeaderNextUp": "Prossimo",
|
"HeaderNextUp": "Prossimo",
|
||||||
"HeaderRecordingGroups": "Gruppi di Registrazione",
|
"HeaderRecordingGroups": "Gruppi di registrazione",
|
||||||
"HomeVideos": "Video Personali",
|
"HomeVideos": "Video personali",
|
||||||
"Inherit": "Eredita",
|
"Inherit": "Eredita",
|
||||||
"ItemAddedWithName": "{0} è stato aggiunto alla libreria",
|
"ItemAddedWithName": "{0} è stato aggiunto alla libreria",
|
||||||
"ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
|
"ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
|
||||||
"LabelIpAddressValue": "Indirizzo IP: {0}",
|
"LabelIpAddressValue": "Indirizzo IP: {0}",
|
||||||
"LabelRunningTimeValue": "Durata: {0}",
|
"LabelRunningTimeValue": "Durata: {0}",
|
||||||
"Latest": "Novità",
|
"Latest": "Novità",
|
||||||
"MessageApplicationUpdated": "Il Server Jellyfin è stato aggiornato",
|
"MessageApplicationUpdated": "Jellyfin Server è stato aggiornato",
|
||||||
"MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}",
|
"MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata",
|
"MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata",
|
||||||
"MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata",
|
"MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata",
|
||||||
"MixedContent": "Contenuto misto",
|
"MixedContent": "Contenuto misto",
|
||||||
"Movies": "Film",
|
"Movies": "Film",
|
||||||
"Music": "Musica",
|
"Music": "Musica",
|
||||||
"MusicVideos": "Video Musicali",
|
"MusicVideos": "Video musicali",
|
||||||
"NameInstallFailed": "{0} installazione fallita",
|
"NameInstallFailed": "{0} installazione non riuscita",
|
||||||
"NameSeasonNumber": "Stagione {0}",
|
"NameSeasonNumber": "Stagione {0}",
|
||||||
"NameSeasonUnknown": "Stagione sconosciuta",
|
"NameSeasonUnknown": "Stagione sconosciuta",
|
||||||
"NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.",
|
"NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.",
|
||||||
@@ -49,38 +49,37 @@
|
|||||||
"NotificationOptionAudioPlayback": "La riproduzione audio è iniziata",
|
"NotificationOptionAudioPlayback": "La riproduzione audio è iniziata",
|
||||||
"NotificationOptionAudioPlaybackStopped": "La riproduzione audio è stata interrotta",
|
"NotificationOptionAudioPlaybackStopped": "La riproduzione audio è stata interrotta",
|
||||||
"NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata",
|
"NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata",
|
||||||
"NotificationOptionInstallationFailed": "Installazione fallita",
|
"NotificationOptionInstallationFailed": "Installazione non riuscita",
|
||||||
"NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto",
|
"NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto",
|
||||||
"NotificationOptionPluginError": "Errore del plugin",
|
"NotificationOptionPluginError": "Errore del plugin",
|
||||||
"NotificationOptionPluginInstalled": "Plugin installato",
|
"NotificationOptionPluginInstalled": "Plugin installato",
|
||||||
"NotificationOptionPluginUninstalled": "Plugin disinstallato",
|
"NotificationOptionPluginUninstalled": "Plugin disinstallato",
|
||||||
"NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato",
|
"NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato",
|
||||||
"NotificationOptionServerRestartRequired": "Riavvio del server necessario",
|
"NotificationOptionServerRestartRequired": "Riavvio del server necessario",
|
||||||
"NotificationOptionTaskFailed": "Operazione pianificata fallita",
|
"NotificationOptionTaskFailed": "Operazione pianificata non riuscita",
|
||||||
"NotificationOptionUserLockedOut": "Utente bloccato",
|
"NotificationOptionUserLockedOut": "Utente bloccato",
|
||||||
"NotificationOptionVideoPlayback": "Riproduzione video iniziata",
|
"NotificationOptionVideoPlayback": "Riproduzione video iniziata",
|
||||||
"NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
|
"NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
|
||||||
"Photos": "Foto",
|
"Photos": "Foto",
|
||||||
"Playlists": "Playlist",
|
"Playlists": "Scalette",
|
||||||
"Plugin": "Plugin",
|
"Plugin": "Plugin",
|
||||||
"PluginInstalledWithName": "{0} è stato Installato",
|
"PluginInstalledWithName": "{0} è stato installato",
|
||||||
"PluginUninstalledWithName": "{0} è stato disinstallato",
|
"PluginUninstalledWithName": "{0} è stato disinstallato",
|
||||||
"PluginUpdatedWithName": "{0} è stato aggiornato",
|
"PluginUpdatedWithName": "{0} è stato aggiornato",
|
||||||
"ProviderValue": "Provider: {0}",
|
"ProviderValue": "Provider: {0}",
|
||||||
"ScheduledTaskFailedWithName": "{0} fallito",
|
"ScheduledTaskFailedWithName": "{0} non riuscito",
|
||||||
"ScheduledTaskStartedWithName": "{0} avviato",
|
"ScheduledTaskStartedWithName": "{0} avviato",
|
||||||
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
|
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
|
||||||
"Shows": "Serie TV",
|
"Shows": "Serie TV",
|
||||||
"Songs": "Brani",
|
"Songs": "Brani",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server si sta avviando. Riprova più tardi.",
|
||||||
"SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}",
|
"SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}",
|
||||||
"Sync": "Sincronizza",
|
"Sync": "Sincronizza",
|
||||||
"System": "Sistema",
|
"System": "Sistema",
|
||||||
"TvShows": "Serie TV",
|
"TvShows": "Serie TV",
|
||||||
"User": "Utente",
|
"User": "Utente",
|
||||||
"UserCreatedWithName": "L'utente {0} è stato creato",
|
"UserCreatedWithName": "L'utente {0} è stato creato",
|
||||||
"UserDeletedWithName": "L'utente {0} è stato rimosso",
|
"UserDeletedWithName": "L'utente {0} è stato eliminato",
|
||||||
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
||||||
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
||||||
"UserOfflineFromDevice": "{0} si è disconnesso da {1}",
|
"UserOfflineFromDevice": "{0} si è disconnesso da {1}",
|
||||||
@@ -115,20 +114,18 @@
|
|||||||
"TasksLibraryCategory": "Libreria",
|
"TasksLibraryCategory": "Libreria",
|
||||||
"TasksMaintenanceCategory": "Manutenzione",
|
"TasksMaintenanceCategory": "Manutenzione",
|
||||||
"TaskCleanActivityLog": "Attività di Registro Completate",
|
"TaskCleanActivityLog": "Attività di Registro Completate",
|
||||||
"TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell’età configurata.",
|
"TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell'età configurata.",
|
||||||
"Undefined": "Non Definito",
|
"Undefined": "Non specificato",
|
||||||
"Forced": "Forzato",
|
"Forced": "Forzato",
|
||||||
"Default": "Predefinito",
|
"Default": "Predefinito",
|
||||||
"TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.",
|
"TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.",
|
||||||
"TaskOptimizeDatabase": "Ottimizza database",
|
"TaskOptimizeDatabase": "Ottimizza database",
|
||||||
"TaskKeyframeExtractor": "Estrattore di Keyframe",
|
"TaskKeyframeExtractor": "Estrattore di Keyframe",
|
||||||
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
|
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori scalette HLS. Questa procedura potrebbe richiedere molto tempo.",
|
||||||
"External": "Esterno",
|
"External": "Esterno",
|
||||||
"HearingImpaired": "Non Udenti",
|
"HearingImpaired": "Non udenti",
|
||||||
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
|
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
|
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle playlist che non esistono più.",
|
|
||||||
"TaskAudioNormalization": "Normalizzazione dell'audio",
|
"TaskAudioNormalization": "Normalizzazione dell'audio",
|
||||||
"TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
|
"TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
|
||||||
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",
|
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",
|
||||||
|
|||||||
@@ -43,32 +43,32 @@
|
|||||||
"NameInstallFailed": "{0}のインストールに失敗しました",
|
"NameInstallFailed": "{0}のインストールに失敗しました",
|
||||||
"NameSeasonNumber": "シーズン {0}",
|
"NameSeasonNumber": "シーズン {0}",
|
||||||
"NameSeasonUnknown": "シーズン不明",
|
"NameSeasonUnknown": "シーズン不明",
|
||||||
"NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。",
|
"NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロードできます。",
|
||||||
"NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります",
|
"NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります",
|
||||||
"NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です",
|
"NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です",
|
||||||
"NotificationOptionAudioPlayback": "オーディオの再生を開始",
|
"NotificationOptionAudioPlayback": "オーディオの再生を開始",
|
||||||
"NotificationOptionAudioPlaybackStopped": "オーディオの再生をストップしました",
|
"NotificationOptionAudioPlaybackStopped": "オーディオの再生を停止",
|
||||||
"NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました",
|
"NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました",
|
||||||
"NotificationOptionInstallationFailed": "インストール失敗",
|
"NotificationOptionInstallationFailed": "インストール失敗",
|
||||||
"NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました",
|
"NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました",
|
||||||
"NotificationOptionPluginError": "プラグインに障害が発生しました",
|
"NotificationOptionPluginError": "プラグインに障害が発生しました",
|
||||||
"NotificationOptionPluginInstalled": "プラグインがインストールされました",
|
"NotificationOptionPluginInstalled": "プラグインをインストールしました",
|
||||||
"NotificationOptionPluginUninstalled": "プラグインがアンインストールされました",
|
"NotificationOptionPluginUninstalled": "プラグインをアンインストールしました",
|
||||||
"NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました",
|
"NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました",
|
||||||
"NotificationOptionServerRestartRequired": "サーバーを再起動してください",
|
"NotificationOptionServerRestartRequired": "サーバーを再起動してください",
|
||||||
"NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗",
|
"NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗",
|
||||||
"NotificationOptionUserLockedOut": "ユーザーはロックされています",
|
"NotificationOptionUserLockedOut": "ユーザーはロックされています",
|
||||||
"NotificationOptionVideoPlayback": "ビデオの再生を開始しました",
|
"NotificationOptionVideoPlayback": "ビデオの再生を開始",
|
||||||
"NotificationOptionVideoPlaybackStopped": "ビデオを停止しました",
|
"NotificationOptionVideoPlaybackStopped": "ビデオの再生を停止",
|
||||||
"Photos": "フォト",
|
"Photos": "フォト",
|
||||||
"Playlists": "プレイリスト",
|
"Playlists": "プレイリスト",
|
||||||
"Plugin": "プラグイン",
|
"Plugin": "プラグイン",
|
||||||
"PluginInstalledWithName": "{0} がインストールされました",
|
"PluginInstalledWithName": "{0} をインストールしました",
|
||||||
"PluginUninstalledWithName": "{0} がアンインストールされました",
|
"PluginUninstalledWithName": "{0} をアンインストールしました",
|
||||||
"PluginUpdatedWithName": "{0} が更新されました",
|
"PluginUpdatedWithName": "{0} を更新しました",
|
||||||
"ProviderValue": "プロバイダ: {0}",
|
"ProviderValue": "プロバイダ: {0}",
|
||||||
"ScheduledTaskFailedWithName": "{0} が失敗しました",
|
"ScheduledTaskFailedWithName": "{0} が失敗しました",
|
||||||
"ScheduledTaskStartedWithName": "{0} が開始されました",
|
"ScheduledTaskStartedWithName": "{0} を開始",
|
||||||
"ServerNameNeedsToBeRestarted": "{0} を再起動してください",
|
"ServerNameNeedsToBeRestarted": "{0} を再起動してください",
|
||||||
"Shows": "番組",
|
"Shows": "番組",
|
||||||
"Songs": "曲",
|
"Songs": "曲",
|
||||||
@@ -126,10 +126,8 @@
|
|||||||
"HearingImpaired": "聴覚障害の方",
|
"HearingImpaired": "聴覚障害の方",
|
||||||
"TaskRefreshTrickplayImages": "トリックプレー画像を生成",
|
"TaskRefreshTrickplayImages": "トリックプレー画像を生成",
|
||||||
"TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。",
|
"TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。",
|
||||||
"TaskCleanCollectionsAndPlaylists": "コレクションとプレイリストをクリーンアップ",
|
|
||||||
"TaskAudioNormalization": "音声の正規化",
|
"TaskAudioNormalization": "音声の正規化",
|
||||||
"TaskAudioNormalizationDescription": "音声の正規化データのためにファイルをスキャンします。",
|
"TaskAudioNormalizationDescription": "音声の正規化データのためにファイルをスキャンします。",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "在しなくなったコレクションやプレイリストからアイテムを削除します。",
|
|
||||||
"TaskDownloadMissingLyricsDescription": "歌詞をダウンロード",
|
"TaskDownloadMissingLyricsDescription": "歌詞をダウンロード",
|
||||||
"TaskExtractMediaSegments": "メディアセグメントを読み取る",
|
"TaskExtractMediaSegments": "メディアセグメントを読み取る",
|
||||||
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
|
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
|
||||||
|
|||||||
@@ -9,46 +9,46 @@
|
|||||||
"Artists": "არტისტი",
|
"Artists": "არტისტი",
|
||||||
"AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია",
|
"AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია",
|
||||||
"Books": "წიგნები",
|
"Books": "წიგნები",
|
||||||
"Forced": "ძალით",
|
"Forced": "იძულებითი",
|
||||||
"Inherit": "მემკვიდრეობით",
|
"Inherit": "მემკვიდრეობით",
|
||||||
"Latest": "უახლესი",
|
"Latest": "უახლესი",
|
||||||
"Movies": "ფილმები",
|
"Movies": "ფილმები",
|
||||||
"Music": "მუსიკა",
|
"Music": "მუსიკა",
|
||||||
"Photos": "ფოტოები",
|
"Photos": "ფოტოები",
|
||||||
"Playlists": "დასაკრავი სიები",
|
"Playlists": "დასაკრავი სიები",
|
||||||
"Plugin": "დამატება",
|
"Plugin": "მოდული",
|
||||||
"Shows": "სერიალები",
|
"Shows": "სერიალები",
|
||||||
"Songs": "სიმღერები",
|
"Songs": "სიმღერები",
|
||||||
"Sync": "სინქრონიზაცია",
|
"Sync": "სინქრონიზაცია",
|
||||||
"System": "სისტემა",
|
"System": "სისტემა",
|
||||||
"Undefined": "აღუწერელი",
|
"Undefined": "განუსაზღვრელი",
|
||||||
"User": "მომხმარებელი",
|
"User": "მომხმარებელი",
|
||||||
"TasksMaintenanceCategory": "რემონტი",
|
"TasksMaintenanceCategory": "რემონტი",
|
||||||
"TasksLibraryCategory": "ბიბლიოთეკა",
|
"TasksLibraryCategory": "ბიბლიოთეკა",
|
||||||
"ChapterNameValue": "თავი {0}",
|
"ChapterNameValue": "თავი {0}",
|
||||||
"HeaderContinueWatching": "ყურების გაგრძელება",
|
"HeaderContinueWatching": "ყურების გაგრძელება",
|
||||||
"HeaderFavoriteArtists": "რჩეული შემსრულებლები",
|
"HeaderFavoriteArtists": "რჩეული შემსრულებლები",
|
||||||
"DeviceOfflineWithName": "{0} გაითიშა",
|
"DeviceOfflineWithName": "{0} გამოეთიშა",
|
||||||
"External": "გარე",
|
"External": "გარე",
|
||||||
"HeaderFavoriteEpisodes": "რჩეული ეპიზოდები",
|
"HeaderFavoriteEpisodes": "რჩეული ეპიზოდები",
|
||||||
"HeaderFavoriteSongs": "რჩეული სიმღერები",
|
"HeaderFavoriteSongs": "რჩეული სიმღერები",
|
||||||
"HeaderRecordingGroups": "ჩამწერი ჯგუფები",
|
"HeaderRecordingGroups": "ჩამწერი ჯგუფები",
|
||||||
"HearingImpaired": "სმენადაქვეითებული",
|
"HearingImpaired": "სმენადაქვეითებული",
|
||||||
"LabelRunningTimeValue": "გაშვებულობის დრო: {0}",
|
"LabelRunningTimeValue": "ხანგრძლივობა: {0}",
|
||||||
"MessageApplicationUpdatedTo": "Jellyfin-ის სერვერი განახლდა {0}-ზე",
|
"MessageApplicationUpdatedTo": "Jellyfin-ის სერვერი განახლდა {0}-ზე",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "სერვერის კონფიგურაციის სექცია {0} განახლდა",
|
"MessageNamedServerConfigurationUpdatedWithValue": "სერვერის კონფიგურაციის სექცია {0} განახლდა",
|
||||||
"MixedContent": "შერეული შემცველობა",
|
"MixedContent": "შერეული შემცველობა",
|
||||||
"MusicVideos": "მუსიკის ვიდეოები",
|
"MusicVideos": "მუსიკალური ვიდეოები",
|
||||||
"NotificationOptionInstallationFailed": "დაყენების შეცდომა",
|
"NotificationOptionInstallationFailed": "დაყენების შეცდომა",
|
||||||
"NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია",
|
"NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია",
|
||||||
"NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია",
|
"NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია",
|
||||||
"NotificationOptionCameraImageUploaded": "კამერის გამოსახულება ატვირთულია",
|
"NotificationOptionCameraImageUploaded": "კამერის გამოსახულება ატვირთულია",
|
||||||
"NotificationOptionVideoPlaybackStopped": "ვიდეოს დაკვრა გაჩერებულია",
|
"NotificationOptionVideoPlaybackStopped": "ვიდეოს დაკვრა გაჩერებულია",
|
||||||
"PluginUninstalledWithName": "{0} წაიშალა",
|
"PluginUninstalledWithName": "{0} წაიშალა",
|
||||||
"ScheduledTaskStartedWithName": "{0} გაეშვა",
|
"ScheduledTaskStartedWithName": "{0} დაიწყო",
|
||||||
"VersionNumber": "ვერსია {0}",
|
"VersionNumber": "ვერსია {0}",
|
||||||
"TasksChannelsCategory": "ინტერნეტ-არხები",
|
"TasksChannelsCategory": "ინტერნეტ-არხები",
|
||||||
"ValueSpecialEpisodeName": "სპეციალური - {0}",
|
"ValueSpecialEpisodeName": "დამატებითი - {0}",
|
||||||
"TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.",
|
"TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.",
|
||||||
"Channels": "არხები",
|
"Channels": "არხები",
|
||||||
"Collections": "კოლექციები",
|
"Collections": "კოლექციები",
|
||||||
@@ -56,31 +56,31 @@
|
|||||||
"Favorites": "რჩეულები",
|
"Favorites": "რჩეულები",
|
||||||
"Folders": "საქაღალდეები",
|
"Folders": "საქაღალდეები",
|
||||||
"HeaderFavoriteShows": "რჩეული სერიალები",
|
"HeaderFavoriteShows": "რჩეული სერიალები",
|
||||||
"HeaderLiveTV": "ცოცხალი TV",
|
"HeaderLiveTV": "ლაივ ტელევიზია",
|
||||||
"HeaderNextUp": "შემდეგი ზემოთ",
|
"HeaderNextUp": "შემდეგი",
|
||||||
"HomeVideos": "სახლის ვიდეოები",
|
"HomeVideos": "სახლის ვიდეოები",
|
||||||
"NameSeasonNumber": "სეზონი {0}",
|
"NameSeasonNumber": "სეზონი {0}",
|
||||||
"NameSeasonUnknown": "სეზონი უცნობია",
|
"NameSeasonUnknown": "სეზონი უცნობია",
|
||||||
"NotificationOptionPluginError": "დამატების შეცდომა",
|
"NotificationOptionPluginError": "მოდულის შეცდომა",
|
||||||
"NotificationOptionPluginInstalled": "დამატება დაყენებულია",
|
"NotificationOptionPluginInstalled": "მოდული დაყენებულია",
|
||||||
"NotificationOptionPluginUninstalled": "დამატება წაიშალა",
|
"NotificationOptionPluginUninstalled": "მოდული წაიშალა",
|
||||||
"ProviderValue": "მომწოდებელი: {0}",
|
"ProviderValue": "მომწოდებელი: {0}",
|
||||||
"ScheduledTaskFailedWithName": "{0} ავარიულია",
|
"ScheduledTaskFailedWithName": "{0} ვერ შესრულდა",
|
||||||
"TvShows": "TV სერიალები",
|
"TvShows": "სატელევიზიო სერიალები",
|
||||||
"TaskRefreshPeople": "ხალხის განახლება",
|
"TaskRefreshPeople": "ხალხის განახლება",
|
||||||
"TaskUpdatePlugins": "დამატებების განახლება",
|
"TaskUpdatePlugins": "მოდულების განახლება",
|
||||||
"TaskRefreshChannels": "არხების განახლება",
|
"TaskRefreshChannels": "არხების განახლება",
|
||||||
"TaskOptimizeDatabase": "ბაზების ოპტიმიზაცია",
|
"TaskOptimizeDatabase": "მონაცემთა ბაზის ოპტიმიზაცია",
|
||||||
"TaskKeyframeExtractor": "საკვანძო კადრის გამომღები",
|
"TaskKeyframeExtractor": "საკვანძო კადრის გამომღები",
|
||||||
"DeviceOnlineWithName": "{0} შეერთებულია",
|
"DeviceOnlineWithName": "{0} დაკავშირდა",
|
||||||
"LabelIpAddressValue": "IP მისამართი: {0}",
|
"LabelIpAddressValue": "IP მისამართი: {0}",
|
||||||
"NameInstallFailed": "{0}-ის დაყენების შეცდომა",
|
"NameInstallFailed": "{0}-ის დაყენების შეცდომა",
|
||||||
"NotificationOptionApplicationUpdateAvailable": "ხელმისაწვდომია აპლიკაციის განახლება",
|
"NotificationOptionApplicationUpdateAvailable": "ხელმისაწვდომია აპლიკაციის განახლება",
|
||||||
"NotificationOptionAudioPlaybackStopped": "აუდიოს დაკვრა გაჩერებულია",
|
"NotificationOptionAudioPlaybackStopped": "აუდიოს დაკვრა გაჩერებულია",
|
||||||
"NotificationOptionNewLibraryContent": "ახალი შემცველობა დამატებულია",
|
"NotificationOptionNewLibraryContent": "ახალი შემცველობა დამატებულია",
|
||||||
"NotificationOptionPluginUpdateInstalled": "დამატების განახლება დაყენებულია",
|
"NotificationOptionPluginUpdateInstalled": "მოდულიs განახლება დაყენებულია",
|
||||||
"NotificationOptionServerRestartRequired": "სერვერის გადატვირთვა აუცილებელია",
|
"NotificationOptionServerRestartRequired": "საჭიროა სერვერის გადატვირთვა",
|
||||||
"NotificationOptionTaskFailed": "დაგეგმილი ამოცანის შეცდომა",
|
"NotificationOptionTaskFailed": "გეგმიური დავალების შეცდომა",
|
||||||
"NotificationOptionUserLockedOut": "მომხმარებელი დაიბლოკა",
|
"NotificationOptionUserLockedOut": "მომხმარებელი დაიბლოკა",
|
||||||
"NotificationOptionVideoPlayback": "ვიდეოს დაკვრა დაწყებულია",
|
"NotificationOptionVideoPlayback": "ვიდეოს დაკვრა დაწყებულია",
|
||||||
"PluginInstalledWithName": "{0} დაყენებულია",
|
"PluginInstalledWithName": "{0} დაყენებულია",
|
||||||
@@ -91,39 +91,49 @@
|
|||||||
"TaskRefreshLibrary": "მედიის ბიბლიოთეკის სკანირება",
|
"TaskRefreshLibrary": "მედიის ბიბლიოთეკის სკანირება",
|
||||||
"TaskCleanLogs": "ჟურნალის საქაღალდის გასუფთავება",
|
"TaskCleanLogs": "ჟურნალის საქაღალდის გასუფთავება",
|
||||||
"TaskCleanTranscode": "ტრანსკოდირების საქაღალდის გასუფთავება",
|
"TaskCleanTranscode": "ტრანსკოდირების საქაღალდის გასუფთავება",
|
||||||
"TaskDownloadMissingSubtitles": "ნაკლული სუბტიტრების გადმოწერა",
|
"TaskDownloadMissingSubtitles": "მიუწვდომელი სუბტიტრების გადმოწერა",
|
||||||
"UserDownloadingItemWithValues": "{0} -ი {0}-ს იწერს",
|
"UserDownloadingItemWithValues": "{0} -ი {1}-ს იწერს",
|
||||||
"FailedLoginAttemptWithUserName": "{0}-დან შემოსვლის შეცდომა",
|
"FailedLoginAttemptWithUserName": "შესვლის წარუმატებელი მცდელობა {0}-დან",
|
||||||
"MessageApplicationUpdated": "Jellyfin-ის სერვერი განახლდა",
|
"MessageApplicationUpdated": "Jellyfin-ის სერვერი განახლდა",
|
||||||
"MessageServerConfigurationUpdated": "სერვერის კონფიგურაცია განახლდა",
|
"MessageServerConfigurationUpdated": "სერვერის კონფიგურაცია განახლდა",
|
||||||
"ServerNameNeedsToBeRestarted": "საჭიროა {0}-ის გადატვირთვა",
|
"ServerNameNeedsToBeRestarted": "საჭიროა {0}-ის გადატვირთვა",
|
||||||
"UserCreatedWithName": "მომხმარებელი {0} შეიქმნა",
|
"UserCreatedWithName": "მომხმარებელი {0} შეიქმნა",
|
||||||
"UserDeletedWithName": "მომხმარებელი {0} წაშლილია",
|
"UserDeletedWithName": "მომხმარებელი {0} წაშლილია",
|
||||||
"UserOnlineFromDevice": "{0}-ი ხაზზეა {1}-დან",
|
"UserOnlineFromDevice": "{0}-ი დაკავშირდა {1}-დან",
|
||||||
"UserOfflineFromDevice": "{0}-ი {1}-დან გაითიშა",
|
"UserOfflineFromDevice": "{0}-ი {1}-დან გაეთიშა",
|
||||||
"ItemAddedWithName": "{0} ჩამატებულია ბიბლიოთეკაში",
|
"ItemAddedWithName": "{0} ჩამატებულია ბიბლიოთეკაში",
|
||||||
"ItemRemovedWithName": "{0} წაშლილია ბიბლიოთეკიდან",
|
"ItemRemovedWithName": "{0} წაშლილია ბიბლიოთეკიდან",
|
||||||
"UserLockedOutWithName": "მომხმარებელი {0} დაბლოკილია",
|
"UserLockedOutWithName": "მომხმარებელი {0} დაბლოკილია",
|
||||||
"UserStartedPlayingItemWithValues": "{0} თამაშობს {1}-ს {2}-ზე",
|
"UserStartedPlayingItemWithValues": "{0} უყურებს {1}-ს {2}-ზე",
|
||||||
"UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია",
|
"UserPasswordChangedWithName": "მომხმარებელი {0}-სთვის პაროლი შეიცვალა",
|
||||||
"UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა",
|
"UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა",
|
||||||
"UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე",
|
"UserStoppedPlayingItemWithValues": "{0}-მა დაასრულა {1}-ის ყურება {2}-ზე",
|
||||||
"TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.",
|
"TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.",
|
||||||
"TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.",
|
"TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.",
|
||||||
"NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.",
|
"NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.",
|
||||||
"CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან",
|
"CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.",
|
"StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.",
|
||||||
"SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა",
|
"SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერა ვერ შესრულდა",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას",
|
"ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას",
|
||||||
"TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველი ჟურნალის ჩანაწერების წაშლა.",
|
"TaskCleanActivityLogDescription": "შლის მითითებულ ასაკზე ძველ ჟურნალის ჩანაწერებს.",
|
||||||
"TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.",
|
"TaskCleanCacheDescription": "შლის სისტემისთვის არასაჭირო ქეშის ფაილებს.",
|
||||||
"TaskRefreshLibraryDescription": "თქვენი მედია ბიბლიოთეკაში ახალი ფაილების ძებნა და მეტამონაცემების განახლება.",
|
"TaskRefreshLibraryDescription": "ეძებს ახალ ფაილებს თქვენს მედიის ბიბლიოთეკაში და ანახლებს მეტამონაცემებს.",
|
||||||
"TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.",
|
"TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.",
|
||||||
"TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.",
|
"TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.",
|
||||||
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
|
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული მოდულების განახლებების გადმოწერა და დაყენება.",
|
||||||
"TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
|
"TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.",
|
"TaskDownloadMissingSubtitlesDescription": "ეძებს ბიბლიოთეკაში მიუწვდომელ სუბტიტრებს ინტერნეტში მეტამონაცემებზე დაყრდნობით.",
|
||||||
"TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
|
"TaskOptimizeDatabaseDescription": "კუმშავს მონაცემთა ბაზას ადგილის გათავისუფლებლად. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
|
||||||
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.",
|
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის დაშვებულ ბიბლიოთეკებში.",
|
||||||
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება"
|
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება",
|
||||||
|
"TaskAudioNormalization": "აუდიოს ნორმალიზება",
|
||||||
|
"TaskAudioNormalizationDescription": "აანალიზებს ფაილებს აუდიოს ნორმალიზაციისთვის.",
|
||||||
|
"TaskDownloadMissingLyrics": "მიუწვდომელი ლირიკების ჩამოტვირთვა",
|
||||||
|
"TaskDownloadMissingLyricsDescription": "ჩამოტვირთავს ამჟამად ბიბლიოთეკაში არარსებულ ლირიკებს სიმღერებისთვის",
|
||||||
|
"TaskExtractMediaSegments": "მედია სეგმენტების სკანირება",
|
||||||
|
"TaskExtractMediaSegmentsDescription": "მედია სეგმენტების სკანირება მხარდაჭერილი მოდულებისთვის.",
|
||||||
|
"TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია",
|
||||||
|
"TaskMoveTrickplayImagesDescription": "გადააქვს trickplay ფაილები ბიბლიოთეკის პარამეტრებზე დაყრდნობით.",
|
||||||
|
"CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავება",
|
||||||
|
"CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
"Shows": "Körsetımder",
|
"Shows": "Körsetımder",
|
||||||
"Songs": "Äuender",
|
"Songs": "Äuender",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalañyz.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalañyz.",
|
||||||
"SubtitleDownloadFailureForItem": "Субтитрлер {0} үшін жүктеліп алынуы сәтсіз",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "{1} üşın subtitrlerdı {0} közınen jüktep alu sätsız",
|
"SubtitleDownloadFailureFromForItem": "{1} üşın subtitrlerdı {0} közınen jüktep alu sätsız",
|
||||||
"Sync": "Ündestıru",
|
"Sync": "Ündestıru",
|
||||||
"System": "Jüie",
|
"System": "Jüie",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user