mirror of
https://github.com/Jellyfin2Samsung/Samsung-Jellyfin-Installer.git
synced 2026-03-01 11:21:12 +03:00
Compare commits
527 Commits
v1.8.4.3-b
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a198bb24f2 | ||
|
|
82142edf2e | ||
|
|
fccd1f2acb | ||
|
|
12a815a9ee | ||
|
|
fef471797d | ||
|
|
abf2dfda8f | ||
|
|
39fc2179d0 | ||
|
|
4321f08f0f | ||
|
|
0f9bd70a3d | ||
|
|
7ab26caa6a | ||
|
|
d65c1371bf | ||
|
|
2fc0739be9 | ||
|
|
ac4a8f6910 | ||
|
|
92e505c135 | ||
|
|
1850c487f0 | ||
|
|
89ac524f06 | ||
|
|
75cafea579 | ||
|
|
f4d61e36f8 | ||
|
|
ddfe2c4983 | ||
|
|
224c9391aa | ||
|
|
3c6cec56c9 | ||
|
|
6747fdfc35 | ||
|
|
8e678367f5 | ||
|
|
3223d73592 | ||
|
|
955418340d | ||
|
|
8a9ed0eb20 | ||
|
|
9f4c9a7907 | ||
|
|
9f90810b94 | ||
|
|
ef23c823df | ||
|
|
cbe0ce8b21 | ||
|
|
c381ecccd1 | ||
|
|
d1fa426815 | ||
|
|
377dca8528 | ||
|
|
932f0fa862 | ||
|
|
fc62f2c034 | ||
|
|
8c997ce7fd | ||
|
|
598ded836a | ||
|
|
9c34f8408e | ||
|
|
2a2ee67d52 | ||
|
|
2e5ee8d428 | ||
|
|
1ef3a31f13 | ||
|
|
8757689b91 | ||
|
|
37a7c414bd | ||
|
|
c3659394d5 | ||
|
|
3381869811 | ||
|
|
32265caf39 | ||
|
|
bfc759bfd8 | ||
|
|
245d47a9e0 | ||
|
|
095d19e8f9 | ||
|
|
527d5ca08a | ||
|
|
2da3ee4df2 | ||
|
|
1fe99bd974 | ||
|
|
d58ae3ded3 | ||
|
|
c002f88a7b | ||
|
|
29e746dd09 | ||
|
|
94307d58db | ||
|
|
b667132efe | ||
|
|
8ff66448ad | ||
|
|
6085e11046 | ||
|
|
97e684ea30 | ||
|
|
c19d411677 | ||
|
|
1786591380 | ||
|
|
2d1eb06048 | ||
|
|
ca18e54097 | ||
|
|
6ac842b2d0 | ||
|
|
23d7213bc2 | ||
|
|
fcdea48ba8 | ||
|
|
9f75dbfefe | ||
|
|
3ac9aa3bdf | ||
|
|
75ac9d6e2e | ||
|
|
a128e1be6b | ||
|
|
74dbde4276 | ||
|
|
912bc84687 | ||
|
|
598274ec08 | ||
|
|
3db936c767 | ||
|
|
ffc314e6c4 | ||
|
|
2d6f5492f8 | ||
|
|
bafcc8721b | ||
|
|
d7424138c6 | ||
|
|
fa19289e76 | ||
|
|
3f45b55f02 | ||
|
|
3916205d56 | ||
|
|
d822bd2e9f | ||
|
|
717674ed2f | ||
|
|
32b95a72cf | ||
|
|
404496e818 | ||
|
|
11ed846c1a | ||
|
|
d02526c1d6 | ||
|
|
56486c1ec8 | ||
|
|
a46a352359 | ||
|
|
2d0a6525da | ||
|
|
753fda3407 | ||
|
|
387c2b501f | ||
|
|
016f17ab04 | ||
|
|
8e971fc6aa | ||
|
|
ac423c7c04 | ||
|
|
ec2b9293ee | ||
|
|
8cee7d4b97 | ||
|
|
8ecf4a9b8a | ||
|
|
2e20e6f7b7 | ||
|
|
eb4ca257f1 | ||
|
|
e6845eebdd | ||
|
|
ebb4995748 | ||
|
|
505a2d3be4 | ||
|
|
e31beed93f | ||
|
|
dc682cfc5f | ||
|
|
56f77379c7 | ||
|
|
e09155e9d2 | ||
|
|
641c2d0f5f | ||
|
|
3a98bc6cf9 | ||
|
|
e8491798a5 | ||
|
|
bb1bda5051 | ||
|
|
d6a8d28fbb | ||
|
|
79b1d68643 | ||
|
|
e852cbc4f1 | ||
|
|
3dcf9a2878 | ||
|
|
6012413efb | ||
|
|
2aeb46c718 | ||
|
|
fe0b0b7bcb | ||
|
|
2232312f83 | ||
|
|
f66bfc69cb | ||
|
|
0ef964e535 | ||
|
|
6a3e93d4d6 | ||
|
|
84a4e964a3 | ||
|
|
7bdbc061cc | ||
|
|
f6c6ff074c | ||
|
|
ee1a9140cd | ||
|
|
4e27791dfe | ||
|
|
5518f7f20e | ||
|
|
cea2bda15b | ||
|
|
da91012e06 | ||
|
|
1e33f057bf | ||
|
|
eab08e271d | ||
|
|
b20803cc5b | ||
|
|
b3eb8d007b | ||
|
|
35990672d2 | ||
|
|
cc666e4b68 | ||
|
|
70bdf89b4a | ||
|
|
a6263a2468 | ||
|
|
eead46aeb6 | ||
|
|
b7d1c921d3 | ||
|
|
8dbb0de4a2 | ||
|
|
868df40a6b | ||
|
|
0b77c32b5e | ||
|
|
c0e1f0333f | ||
|
|
78d0ede171 | ||
|
|
03647d6360 | ||
|
|
f17a8404f3 | ||
|
|
3d0f89e6a8 | ||
|
|
116351376a | ||
|
|
7dc7ecc6e3 | ||
|
|
f663eae6d9 | ||
|
|
bfed3237e7 | ||
|
|
fbec97cfad | ||
|
|
a48b49e9fc | ||
|
|
3b4e31e09e | ||
|
|
9159ee38ac | ||
|
|
0bdd8cd6ce | ||
|
|
de7edefc9c | ||
|
|
35cb7cf2e6 | ||
|
|
ef7b2abb31 | ||
|
|
5ed25031ca | ||
|
|
37d61eed78 | ||
|
|
9aa4073995 | ||
|
|
ecf6937f55 | ||
|
|
23ffe2d046 | ||
|
|
1c6ac7a282 | ||
|
|
5f3f8a6895 | ||
|
|
4e9ab2dcb8 | ||
|
|
c87b7bef65 | ||
|
|
60d30c3f05 | ||
|
|
0cab66098e | ||
|
|
030cf24786 | ||
|
|
1c12ad6af7 | ||
|
|
3e5d3bfe74 | ||
|
|
d802bddf52 | ||
|
|
3f28090e9b | ||
|
|
faab2a68df | ||
|
|
6f37097e97 | ||
|
|
9d4f6b5630 | ||
|
|
2c2bb5de80 | ||
|
|
cd7d17e2b0 | ||
|
|
29381bca2e | ||
|
|
4f0232ab6a | ||
|
|
28b39c39f5 | ||
|
|
f278f938c0 | ||
|
|
7d4c94bd62 | ||
|
|
e36755de2f | ||
|
|
c15405cebb | ||
|
|
17153786c5 | ||
|
|
e5ab14c48e | ||
|
|
1643ea5758 | ||
|
|
7906bca1b2 | ||
|
|
9cf3de81ae | ||
|
|
eb10033d19 | ||
|
|
91c3aa9549 | ||
|
|
c63adfa618 | ||
|
|
4471e27855 | ||
|
|
7ef1c516e1 | ||
|
|
2daac548cf | ||
|
|
e377ddfd49 | ||
|
|
ae4f2ccb2f | ||
|
|
64d610d6b2 | ||
|
|
88f4755c3d | ||
|
|
d401e750d5 | ||
|
|
cd55172fb0 | ||
|
|
2e495db51d | ||
|
|
cdbc9959c5 | ||
|
|
60e0148ee2 | ||
|
|
b68114e6db | ||
|
|
51af88217e | ||
|
|
c4a4e947ba | ||
|
|
fd7058ca57 | ||
|
|
a41a36aad3 | ||
|
|
1c8bbb7251 | ||
|
|
7a1729530a | ||
|
|
efdb3406fa | ||
|
|
821fac94f8 | ||
|
|
3069290214 | ||
|
|
9065cf2249 | ||
|
|
e58a198711 | ||
|
|
4a3e54bd47 | ||
|
|
9e6eaa2316 | ||
|
|
a1cac4ae17 | ||
|
|
a48e3b5d24 | ||
|
|
cc231e39fd | ||
|
|
9879412518 | ||
|
|
0af0f2d1aa | ||
|
|
e91a0f23d6 | ||
|
|
d4fc9b6071 | ||
|
|
0979d05234 | ||
|
|
d1078087f5 | ||
|
|
63f37a5cfa | ||
|
|
867c7c2c6e | ||
|
|
c572e2b8ed | ||
|
|
0400671fc4 | ||
|
|
d58baa4012 | ||
|
|
2ca2f4c7f0 | ||
|
|
8d7ab3429a | ||
|
|
096d7a3e57 | ||
|
|
276be8cabb | ||
|
|
8eef524927 | ||
|
|
754a47b4dd | ||
|
|
581f1bb238 | ||
|
|
669d3896a0 | ||
|
|
a8def26087 | ||
|
|
3ea51370cd | ||
|
|
f280c76a27 | ||
|
|
d59c5e42c5 | ||
|
|
04d6f5c228 | ||
|
|
9cd6006841 | ||
|
|
14adc6e806 | ||
|
|
e1b9d5b03d | ||
|
|
71cf3d62a8 | ||
|
|
fed80d8f55 | ||
|
|
b36a83038f | ||
|
|
f17efcbefc | ||
|
|
fe16834604 | ||
|
|
ac5fb567d0 | ||
|
|
f38c345cc6 | ||
|
|
b6ac5b600c | ||
|
|
36a9a267bc | ||
|
|
7350a0c92d | ||
|
|
a575a4353b | ||
|
|
2adebebb74 | ||
|
|
9c8ae5c572 | ||
|
|
7af3f099cb | ||
|
|
ad0c3ff186 | ||
|
|
405573bc36 | ||
|
|
224fb37350 | ||
|
|
62fc93f10f | ||
|
|
dea4f4e6c7 | ||
|
|
6a0a6df965 | ||
|
|
9c38699702 | ||
|
|
36b112cd96 | ||
|
|
0f68d108b1 | ||
|
|
a97d4dccf3 | ||
|
|
2a054c761f | ||
|
|
faa67aa904 | ||
|
|
432e50abdf | ||
|
|
de86a50773 | ||
|
|
2e9f1fd350 | ||
|
|
a76b2cc3da | ||
|
|
b7e2f85251 | ||
|
|
95f166124c | ||
|
|
073cd3ebd2 | ||
|
|
81ed6dcb81 | ||
|
|
750f571cf4 | ||
|
|
66d15cc6a8 | ||
|
|
1fab1ae861 | ||
|
|
196fe4e89b | ||
|
|
9c4c802d6d | ||
|
|
0432935f34 | ||
|
|
8ee69ef322 | ||
|
|
dcbb4a0f4c | ||
|
|
37421af16c | ||
|
|
1f4032602e | ||
|
|
04992e262e | ||
|
|
00b78f4bbe | ||
|
|
d19ad9e600 | ||
|
|
62ea3023ef | ||
|
|
179a6b5785 | ||
|
|
fb2a7e59db | ||
|
|
2192fede6a | ||
|
|
ab61755609 | ||
|
|
f5c18ac044 | ||
|
|
22f684275c | ||
|
|
0ff3130107 | ||
|
|
4237d3a9db | ||
|
|
b8fd35b462 | ||
|
|
03808b636b | ||
|
|
9df468fdf2 | ||
|
|
16fa6f73e9 | ||
|
|
3f3709d4a9 | ||
|
|
c50464d44f | ||
|
|
c193ccedd2 | ||
|
|
7e63ceab37 | ||
|
|
b9979a9aec | ||
|
|
b348416041 | ||
|
|
820e6a19a7 | ||
|
|
206659e1d6 | ||
|
|
0cc084aef6 | ||
|
|
71fdbf8785 | ||
|
|
9cd31c48c4 | ||
|
|
d515779e17 | ||
|
|
fbe05e662f | ||
|
|
302a53f7b1 | ||
|
|
5316c4d609 | ||
|
|
83b5143031 | ||
|
|
f3571d6298 | ||
|
|
5f66f952aa | ||
|
|
17128965d3 | ||
|
|
2fd644bd53 | ||
|
|
54db8ba4ca | ||
|
|
108a4b0d4a | ||
|
|
063a7e2750 | ||
|
|
4fb92c2214 | ||
|
|
64fc60d543 | ||
|
|
7640a800d1 | ||
|
|
49dc6cde31 | ||
|
|
5530092a60 | ||
|
|
310c7a2fd1 | ||
|
|
3d962ff0b1 | ||
|
|
50144c0a9e | ||
|
|
58909dcd2e | ||
|
|
86bed3a425 | ||
|
|
ccc2bb170d | ||
|
|
f128790d70 | ||
|
|
4d6ce0770a | ||
|
|
a0813f387f | ||
|
|
7bbc7c7674 | ||
|
|
013a056d75 | ||
|
|
41e2eaaf7d | ||
|
|
9428364b99 | ||
|
|
a94e2debda | ||
|
|
2025affc89 | ||
|
|
b2020c301a | ||
|
|
d370bd9c9c | ||
|
|
8aa4f0eb41 | ||
|
|
437c9881ea | ||
|
|
8cb0f1d94f | ||
|
|
bb25f5ebb2 | ||
|
|
40038aa1f2 | ||
|
|
d38e6270cd | ||
|
|
0a1946f692 | ||
|
|
eb541ee586 | ||
|
|
60c2c8a7cf | ||
|
|
6eede596c0 | ||
|
|
ed87ff9be6 | ||
|
|
79e90e355f | ||
|
|
534975b559 | ||
|
|
c30656a0d7 | ||
|
|
a0bcc793a2 | ||
|
|
20079f5d6f | ||
|
|
47f09c547a | ||
|
|
3a0af16ad0 | ||
|
|
420f153e40 | ||
|
|
04eba3ce7c | ||
|
|
71b0d413a8 | ||
|
|
0f884fdcc8 | ||
|
|
cdfabac260 | ||
|
|
df55f0d456 | ||
|
|
f882a2b9a2 | ||
|
|
6871e88431 | ||
|
|
3232fe2a22 | ||
|
|
0ef39b7e8b | ||
|
|
2152ba24f9 | ||
|
|
6711907c4c | ||
|
|
764adce7a1 | ||
|
|
a7f8db2183 | ||
|
|
6efea5d414 | ||
|
|
961fa02ade | ||
|
|
5a93c3c08d | ||
|
|
7c03a513f1 | ||
|
|
70210550ed | ||
|
|
6c69337c01 | ||
|
|
5daaede009 | ||
|
|
cae30ed834 | ||
|
|
9801b6dfbf | ||
|
|
bf2f54dda7 | ||
|
|
60cf53c109 | ||
|
|
4864bd4025 | ||
|
|
3f5c8604e9 | ||
|
|
75b4cb3622 | ||
|
|
fc0b583b4f | ||
|
|
101b6781f0 | ||
|
|
c1a9ff7ce4 | ||
|
|
a934610642 | ||
|
|
ad4392f576 | ||
|
|
b441c2a5ee | ||
|
|
df8c651d56 | ||
|
|
c6c1e481ab | ||
|
|
cc91b798df | ||
|
|
30ffe70141 | ||
|
|
8c25f0366e | ||
|
|
617d3438b7 | ||
|
|
757d9d80a8 | ||
|
|
b8bf675e30 | ||
|
|
a93fb97aa4 | ||
|
|
4ee7c26d7e | ||
|
|
ecd42ff8df | ||
|
|
25cf8ff4de | ||
|
|
fdb42a3ff0 | ||
|
|
65192de7b6 | ||
|
|
4a92effb7c | ||
|
|
49e004cb83 | ||
|
|
9ce2b149c8 | ||
|
|
4eafb70246 | ||
|
|
442fbaae41 | ||
|
|
fbea16af8f | ||
|
|
296d52d75d | ||
|
|
a50dace372 | ||
|
|
0e55a90054 | ||
|
|
1ecc74c156 | ||
|
|
f5053671f8 | ||
|
|
cbf3248f8e | ||
|
|
2e7fb17433 | ||
|
|
5be9747b39 | ||
|
|
86849454c2 | ||
|
|
99b228fedc | ||
|
|
3dd3d0cf67 | ||
|
|
911a2108a7 | ||
|
|
c71a62e3ca | ||
|
|
720d9b36f4 | ||
|
|
b297822475 | ||
|
|
b8bc90a792 | ||
|
|
6f3686b99d | ||
|
|
08c0512952 | ||
|
|
f4ceb5f71d | ||
|
|
5f17674620 | ||
|
|
05074e12a6 | ||
|
|
689cd693b0 | ||
|
|
2cd4034197 | ||
|
|
9c27ae30d2 | ||
|
|
e5bde2680a | ||
|
|
5adf9fbff6 | ||
|
|
3e2f6bec91 | ||
|
|
d28331eba3 | ||
|
|
55284e76de | ||
|
|
6b8f23cba4 | ||
|
|
9ae0073354 | ||
|
|
3c22e7f6bf | ||
|
|
b3d371aa57 | ||
|
|
13c935d967 | ||
|
|
a3d236e5e0 | ||
|
|
bb8f170505 | ||
|
|
78ee3e1460 | ||
|
|
ad905bdb86 | ||
|
|
0f0347ee29 | ||
|
|
796268c518 | ||
|
|
95596c1153 | ||
|
|
1a1831bc43 | ||
|
|
980bb1af5f | ||
|
|
a8d97e31e7 | ||
|
|
42d3ea7d5a | ||
|
|
ef5c2c0b61 | ||
|
|
bcdee694cd | ||
|
|
de27559f0e | ||
|
|
6b9df89bc1 | ||
|
|
2fe4f59c93 | ||
|
|
8ed0a97f29 | ||
|
|
5e55368393 | ||
|
|
ab9d09fca5 | ||
|
|
9edc01a0fc | ||
|
|
c0c924c866 | ||
|
|
40e17be1e6 | ||
|
|
20a269ef6e | ||
|
|
06efc00bec | ||
|
|
40a4a0d6e7 | ||
|
|
6a762cb9c5 | ||
|
|
74e2eb0870 | ||
|
|
52210ca677 | ||
|
|
61d17db09d | ||
|
|
58c2228693 | ||
|
|
2b4ad0799f | ||
|
|
7a63a17b26 | ||
|
|
e755794239 | ||
|
|
032552a3af | ||
|
|
5acf1467ce | ||
|
|
868215a886 | ||
|
|
c4713f8201 | ||
|
|
f5e4c6893d | ||
|
|
70c34845b2 | ||
|
|
dfd8569550 | ||
|
|
2094870ef7 | ||
|
|
c0b365f479 | ||
|
|
4c92f5644a | ||
|
|
06c24fcedd | ||
|
|
6ebfd75260 | ||
|
|
6904770a4f | ||
|
|
a46290957c | ||
|
|
36b57fb1aa | ||
|
|
1c4bbd28ae | ||
|
|
29b04039ae | ||
|
|
26805e1de8 | ||
|
|
0107b9b75e | ||
|
|
609478bc54 | ||
|
|
9e18da0858 | ||
|
|
4b65e0e3f8 | ||
|
|
d45bc8a50b | ||
|
|
6df62eb4f0 | ||
|
|
d0a0de284d | ||
|
|
230f7d8c36 | ||
|
|
9ea3fee454 | ||
|
|
c6a1d130d9 | ||
|
|
912a9f84d7 | ||
|
|
4953efdb2e |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -84,7 +84,7 @@ We welcome pull requests! Please:
|
||||
```bash
|
||||
git remote add upstream https://github.com/PatrickSt1991/Samsung-Jellyfin-Installer.git
|
||||
git fetch upstream
|
||||
git rebase upstream/master
|
||||
git rebase upstream/beta
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -3,8 +3,7 @@ name: Feature Request
|
||||
about: Suggest a new feature or improvement for the application
|
||||
title: "[Feature] "
|
||||
labels: enhancement, keep-alive
|
||||
assignees: ''
|
||||
|
||||
assignees: PatrickSt1991
|
||||
---
|
||||
|
||||
**Feature Description**
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/generic-bug-report.md
vendored
10
.github/ISSUE_TEMPLATE/generic-bug-report.md
vendored
@@ -3,20 +3,22 @@ name: Generic bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[Bug] "
|
||||
labels: bug, keep-alive
|
||||
assignees: ''
|
||||
|
||||
assignees: PatrickSt1991
|
||||
---
|
||||
|
||||
**Have you checked the wiki?**
|
||||
[Samsung Jellyfin Installer Wiki](https://github.com/PatrickSt1991/Samsung-Jellyfin-Installer/wiki)
|
||||
[Samsung Jellyfin Installer Wiki](https://github.com/PatrickSt1991/Samsung-Jellyfin-Installer/wiki/FAQ)
|
||||
|
||||
**Have you included the log files in this issue?**
|
||||
- Yes
|
||||
- No
|
||||
|
||||
**Which version of the application did you use?**
|
||||
(e.g., v1.2.3.4)
|
||||
|
||||
**Is your Samsung TV in developer mode?**
|
||||
- Yes
|
||||
- No
|
||||
- Not sure
|
||||
|
||||
**What Tizen version is your TV running?**
|
||||
(e.g., Tizen 5.0, 6.5, etc.)
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/tizen-6--bug.md
vendored
7
.github/ISSUE_TEMPLATE/tizen-6--bug.md
vendored
@@ -8,7 +8,11 @@ assignees: PatrickSt1991
|
||||
---
|
||||
|
||||
**Have you checked the wiki?**
|
||||
[Samsung Jellyfin Installer Wiki](https://github.com/PatrickSt1991/Samsung-Jellyfin-Installer/wiki)
|
||||
[Samsung Jellyfin Installer Wiki](https://github.com/PatrickSt1991/Samsung-Jellyfin-Installer/wiki/FAQ)
|
||||
|
||||
**Have you included the log files in this issue?**
|
||||
- Yes
|
||||
- No
|
||||
|
||||
**Which version of the application did you use?**
|
||||
(e.g., v1.2.3.4)
|
||||
@@ -16,7 +20,6 @@ assignees: PatrickSt1991
|
||||
**Is your Samsung TV in developer mode?**
|
||||
- Yes
|
||||
- No
|
||||
- Not sure
|
||||
|
||||
**What Tizen version is your TV running?**
|
||||
(e.g., Tizen 5.0, 6.5, etc.)
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/tizen-6-5--bug.md
vendored
7
.github/ISSUE_TEMPLATE/tizen-6-5--bug.md
vendored
@@ -8,7 +8,11 @@ assignees: PatrickSt1991
|
||||
---
|
||||
|
||||
**Have you checked the wiki?**
|
||||
[Samsung Jellyfin Installer Wiki](https://github.com/PatrickSt1991/Samsung-Jellyfin-Installer/wiki)
|
||||
[Samsung Jellyfin Installer Wiki](https://github.com/PatrickSt1991/Samsung-Jellyfin-Installer/wiki/FAQ)
|
||||
|
||||
**Have you included the log files in this issue?**
|
||||
- Yes
|
||||
- No
|
||||
|
||||
**Which version of the application did you use?**
|
||||
(e.g., v1.2.3.4)
|
||||
@@ -16,7 +20,6 @@ assignees: PatrickSt1991
|
||||
**Is your Samsung TV in developer mode?**
|
||||
- Yes
|
||||
- No
|
||||
- Not sure
|
||||
|
||||
**What Tizen version is your TV running?**
|
||||
(e.g., Tizen 5.0, 6.5, etc.)
|
||||
|
||||
3
.github/pull_request_template.md
vendored
3
.github/pull_request_template.md
vendored
@@ -1,5 +1,8 @@
|
||||
# Pull Request Template
|
||||
|
||||
## Branch
|
||||
- [ ] I branched off beta (not master) to develop this feature/fix
|
||||
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. Also include relevant motivation and context.
|
||||
|
||||
304
.github/workflows/beta-prerelease.yml
vendored
Normal file
304
.github/workflows/beta-prerelease.yml
vendored
Normal file
@@ -0,0 +1,304 @@
|
||||
name: Beta Pre-Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [beta]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
PRODUCT_NAME: Jellyfin2Samsung
|
||||
PROJECT_DIR: Jellyfin2Samsung-CrossOS
|
||||
CONFIGURATION: Release
|
||||
|
||||
jobs:
|
||||
# ======================================================
|
||||
# RELEASE + WINDOWS + LINUX
|
||||
# ======================================================
|
||||
beta-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
VERSION: ${{ env.VERSION }}
|
||||
VERSION_TAG: ${{ env.VERSION_TAG }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# ---------------- VERSION ----------------
|
||||
- name: Extract version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(grep -oE 'v[0-9]+(\.[0-9]+){1,3}' \
|
||||
Jellyfin2Samsung-CrossOS/Helpers/AppSettings.cs)
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "❌ Version not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "VERSION=${VERSION#v}" >> $GITHUB_ENV
|
||||
echo "VERSION_TAG=${VERSION}-beta" >> $GITHUB_ENV
|
||||
|
||||
# ---------------- RELEASE NOTES ----------------
|
||||
- name: Generate changelog
|
||||
run: |
|
||||
LAST_TAG=$(git tag --sort=-creatordate | grep beta | head -n 1)
|
||||
RANGE="${LAST_TAG:+$LAST_TAG..HEAD}"
|
||||
|
||||
{
|
||||
echo "## ✅ What's New & Improved"
|
||||
git log ${RANGE:-HEAD} --pretty=format:"- %s"
|
||||
} > CHANGELOG.md
|
||||
|
||||
- name: Prepare release notes
|
||||
run: |
|
||||
DATE=$(date +'%Y-%m-%d')
|
||||
{
|
||||
echo "## 📦 [${VERSION_TAG}] – ${DATE}"
|
||||
echo ""
|
||||
cat CHANGELOG.md
|
||||
echo ""
|
||||
echo ""
|
||||
echo "| Platform | Status | Notes |"
|
||||
echo "|----------|--------|-------|"
|
||||
echo "| 🍎 macOS (.app + dmg) | ⚠️ Beta | ARM64 + Intel |"
|
||||
echo "| 🍎 macOS (CLI) | ⚠️ Beta | Per-arch tar.gz |"
|
||||
echo "| 🐧 Linux | ⚠️ Beta | tar.gz / .deb |"
|
||||
echo "| 🪟 Windows | ⚠️ Beta | CI-built |"
|
||||
} > RELEASE_NOTES.md
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
# ---------------- WINDOWS ----------------
|
||||
- name: Build Windows
|
||||
run: |
|
||||
dotnet publish Jellyfin2Samsung-CrossOS/Jellyfin2Samsung.csproj \
|
||||
-c Release -r win-x64 -p:SelfContained=true \
|
||||
-o publish/win-x64
|
||||
|
||||
mkdir -p dist
|
||||
(cd publish/win-x64 && zip -r "../../dist/${PRODUCT_NAME}-${VERSION_TAG}-win-x64.zip" .)
|
||||
|
||||
# ---------------- LINUX ----------------
|
||||
- name: Build Linux
|
||||
run: |
|
||||
dotnet publish Jellyfin2Samsung-CrossOS/Jellyfin2Samsung.csproj \
|
||||
-c Release -r linux-x64 -p:SelfContained=true \
|
||||
-o publish/linux-x64
|
||||
|
||||
mkdir -p dist
|
||||
tar -czf "dist/${PRODUCT_NAME}-${VERSION_TAG}-linux-x64.tar.gz" \
|
||||
-C publish/linux-x64 .
|
||||
|
||||
# ---------------- LINUX ICON ----------------
|
||||
- name: Linux icon
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y librsvg2-bin
|
||||
rsvg-convert -w 256 -h 256 \
|
||||
jellyfin-tizen-logo.svg -o jellyfin.png
|
||||
|
||||
# ---------------- DEB ----------------
|
||||
- name: Build .deb
|
||||
run: |
|
||||
sudo apt-get install -y dpkg-dev
|
||||
|
||||
PKG=deb
|
||||
mkdir -p $PKG/opt/jellyfin2samsung \
|
||||
$PKG/usr/share/{applications,icons/hicolor/256x256/apps}
|
||||
|
||||
cp -R publish/linux-x64/. $PKG/opt/jellyfin2samsung/
|
||||
chmod +x $PKG/opt/jellyfin2samsung/Jellyfin2Samsung
|
||||
|
||||
# 🔥 quick+dirty: allow writes where your app writes
|
||||
mkdir -p \
|
||||
$PKG/opt/jellyfin2samsung/Logs \
|
||||
$PKG/opt/jellyfin2samsung/Downloads \
|
||||
$PKG/opt/jellyfin2samsung/Assets/TizenSDB \
|
||||
$PKG/opt/jellyfin2samsung/Assets/Certificate
|
||||
chmod -R 777 \
|
||||
$PKG/opt/jellyfin2samsung/Logs \
|
||||
$PKG/opt/jellyfin2samsung/Downloads \
|
||||
$PKG/opt/jellyfin2samsung/Assets/TizenSDB \
|
||||
$PKG/opt/jellyfin2samsung/Assets/Certificate
|
||||
|
||||
cp jellyfin.png $PKG/usr/share/icons/hicolor/256x256/apps/jellyfin2samsung.png
|
||||
|
||||
cat > $PKG/usr/share/applications/jellyfin2samsung.desktop <<EOF
|
||||
[Desktop Entry]
|
||||
Name=Jellyfin2Samsung
|
||||
Exec=/opt/jellyfin2samsung/Jellyfin2Samsung
|
||||
Icon=jellyfin2samsung
|
||||
Type=Application
|
||||
Categories=Utility;
|
||||
EOF
|
||||
|
||||
mkdir -p $PKG/DEBIAN
|
||||
cat > $PKG/DEBIAN/control <<EOF
|
||||
Package: jellyfin2samsung
|
||||
Version: ${VERSION}
|
||||
Architecture: amd64
|
||||
Maintainer: MadeByPatrick
|
||||
Description: Jellyfin2Samsung
|
||||
EOF
|
||||
|
||||
dpkg-deb --build $PKG
|
||||
mv $PKG.deb dist/${PRODUCT_NAME}-${VERSION_TAG}-linux-x64.deb
|
||||
|
||||
# ---------------- RELEASE (UPDATE SAME TAG EVERY COMMIT) ----------------
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.VERSION_TAG }}
|
||||
name: ${{ env.VERSION_TAG }}
|
||||
prerelease: true
|
||||
overwrite_files: true
|
||||
files: dist/*
|
||||
body_path: RELEASE_NOTES.md
|
||||
|
||||
# ======================================================
|
||||
# MACOS (PER-ARCH CLI + APP + DMG)
|
||||
# ======================================================
|
||||
macos-app:
|
||||
runs-on: macos-latest
|
||||
needs: beta-release
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
# ---------------- ICON ----------------
|
||||
# ---------------- ICON ----------------
|
||||
- name: macOS icon (SVG -> ICNS)
|
||||
run: |
|
||||
# Reliable SVG -> PNG conversion on GitHub macOS runners
|
||||
brew install librsvg
|
||||
|
||||
# If the SVG is already in your repo, keep this as-is.
|
||||
# If not, uncomment the curl line below and remove the local reference.
|
||||
# curl -fsSL \
|
||||
# https://raw.githubusercontent.com/Jellyfin2Samsung/Samsung-Jellyfin-Installer/refs/heads/master/jellyfin-tizen-logo.svg \
|
||||
# -o jellyfin-tizen-logo.svg
|
||||
|
||||
rsvg-convert -w 1024 -h 1024 jellyfin-tizen-logo.svg -o jellyfin.png
|
||||
|
||||
rm -rf icon.iconset
|
||||
mkdir icon.iconset
|
||||
for s in 16 32 128 256 512; do
|
||||
sips -z $s $s jellyfin.png --out icon.iconset/icon_${s}x${s}.png
|
||||
sips -z $((s*2)) $((s*2)) jellyfin.png --out icon.iconset/icon_${s}x${s}@2x.png
|
||||
done
|
||||
|
||||
iconutil -c icns icon.iconset -o jellyfin.icns
|
||||
|
||||
# ---------------- ARM64 ----------------
|
||||
- name: macOS ARM64 publish
|
||||
run: |
|
||||
dotnet publish Jellyfin2Samsung-CrossOS/Jellyfin2Samsung.csproj \
|
||||
-c Release -r osx-arm64 -p:SelfContained=true \
|
||||
-o publish/osx-arm64
|
||||
|
||||
mkdir -p dist
|
||||
tar -czf \
|
||||
dist/${PRODUCT_NAME}-${{ needs.beta-release.outputs.VERSION_TAG }}-macos-arm64.tar.gz \
|
||||
-C publish/osx-arm64 .
|
||||
|
||||
APP=Jellyfin2Samsung-arm64.app/Contents
|
||||
mkdir -p $APP/MacOS $APP/Resources
|
||||
cp -R publish/osx-arm64/* $APP/MacOS/
|
||||
chmod +x $APP/MacOS/Jellyfin2Samsung
|
||||
cp jellyfin.icns $APP/Resources/
|
||||
|
||||
cat > $APP/Info.plist <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key><string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleExecutable</key><string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleIdentifier</key><string>org.madebypatrick.jellyfin2samsung</string>
|
||||
<key>CFBundleVersion</key><string>${{ needs.beta-release.outputs.VERSION }}</string>
|
||||
<key>CFBundleShortVersionString</key><string>${{ needs.beta-release.outputs.VERSION }}</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
|
||||
<key>CFBundleIconFile</key><string>jellyfin.icns</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
codesign --deep --force --sign - Jellyfin2Samsung-arm64.app
|
||||
|
||||
rm -rf dmg-arm64
|
||||
mkdir -p dmg-arm64
|
||||
cp -R Jellyfin2Samsung-arm64.app dmg-arm64/Jellyfin2Samsung.app
|
||||
ln -s /Applications dmg-arm64/Applications
|
||||
hdiutil create \
|
||||
-volname Jellyfin2Samsung \
|
||||
-srcfolder dmg-arm64 \
|
||||
-format UDZO \
|
||||
dist/${PRODUCT_NAME}-${{ needs.beta-release.outputs.VERSION_TAG }}-macos-arm64.dmg
|
||||
|
||||
# ---------------- INTEL ----------------
|
||||
- name: macOS Intel publish
|
||||
run: |
|
||||
dotnet publish Jellyfin2Samsung-CrossOS/Jellyfin2Samsung.csproj \
|
||||
-c Release -r osx-x64 -p:SelfContained=true \
|
||||
-o publish/osx-x64
|
||||
|
||||
tar -czf \
|
||||
dist/${PRODUCT_NAME}-${{ needs.beta-release.outputs.VERSION_TAG }}-macos-x64.tar.gz \
|
||||
-C publish/osx-x64 .
|
||||
|
||||
APP=Jellyfin2Samsung-x64.app/Contents
|
||||
mkdir -p $APP/MacOS $APP/Resources
|
||||
cp -R publish/osx-x64/* $APP/MacOS/
|
||||
chmod +x $APP/MacOS/Jellyfin2Samsung
|
||||
cp jellyfin.icns $APP/Resources/
|
||||
|
||||
cat > $APP/Info.plist <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key><string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleExecutable</key><string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleIdentifier</key><string>org.madebypatrick.jellyfin2samsung</string>
|
||||
<key>CFBundleVersion</key><string>${{ needs.beta-release.outputs.VERSION }}</string>
|
||||
<key>CFBundleShortVersionString</key><string>${{ needs.beta-release.outputs.VERSION }}</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
|
||||
<key>CFBundleIconFile</key><string>jellyfin.icns</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
codesign --deep --force --sign - Jellyfin2Samsung-x64.app
|
||||
|
||||
rm -rf dmg-x64
|
||||
mkdir -p dmg-x64
|
||||
cp -R Jellyfin2Samsung-x64.app dmg-x64/Jellyfin2Samsung.app
|
||||
ln -s /Applications dmg-x64/Applications
|
||||
hdiutil create \
|
||||
-volname Jellyfin2Samsung \
|
||||
-srcfolder dmg-x64 \
|
||||
-format UDZO \
|
||||
dist/${PRODUCT_NAME}-${{ needs.beta-release.outputs.VERSION_TAG }}-macos-x64.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.beta-release.outputs.VERSION_TAG }}
|
||||
name: ${{ needs.beta-release.outputs.VERSION_TAG }}
|
||||
prerelease: true
|
||||
overwrite_files: true
|
||||
files: dist/*
|
||||
373
.github/workflows/stable-release.yml
vendored
Normal file
373
.github/workflows/stable-release.yml
vendored
Normal file
@@ -0,0 +1,373 @@
|
||||
name: Stable Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
types: [closed]
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Git ref to build (branch/tag/SHA). Default: master"
|
||||
required: false
|
||||
default: "master"
|
||||
allow_existing_release:
|
||||
description: "If true, update assets even when the stable tag already exists"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
PRODUCT_NAME: Jellyfin2Samsung
|
||||
PROJECT_DIR: Jellyfin2Samsung-CrossOS
|
||||
CONFIGURATION: Release
|
||||
|
||||
jobs:
|
||||
# ======================================================
|
||||
# STABLE RELEASE + WINDOWS + LINUX (tar.gz + deb)
|
||||
# ======================================================
|
||||
stable-release:
|
||||
if: |
|
||||
(github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.base.ref == 'master' &&
|
||||
github.event.pull_request.head.ref == 'beta') ||
|
||||
(github.event_name == 'workflow_dispatch')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
VERSION: ${{ steps.version.outputs.VERSION }}
|
||||
VERSION_TAG: ${{ steps.version.outputs.VERSION_TAG }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
|
||||
|
||||
# ---------------- VERSION (NO -beta allowed) ----------------
|
||||
- name: Validate AppVersion (must be stable)
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
FILE="${PROJECT_DIR}/Helpers/AppSettings.cs"
|
||||
|
||||
VERSION_LINE=$(grep 'AppVersion' "$FILE" || true)
|
||||
|
||||
if echo "$VERSION_LINE" | grep -q '\-beta"'; then
|
||||
echo "❌ AppVersion still contains '-beta'"
|
||||
echo " Stable releases must use a clean version like v2.0.0.1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(echo "$VERSION_LINE" | grep -oE 'v[0-9]+(\.[0-9]+){1,3}')
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "❌ Failed to extract stable version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "VERSION=${VERSION#v}" >> "$GITHUB_ENV"
|
||||
echo "VERSION_TAG=${VERSION}" >> "$GITHUB_ENV"
|
||||
|
||||
echo "VERSION=${VERSION#v}" >> "$GITHUB_OUTPUT"
|
||||
echo "VERSION_TAG=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# --------------------------------------------------
|
||||
# Guard (log-only): create or update assets
|
||||
# --------------------------------------------------
|
||||
- name: Stable release exists? (allow updating assets)
|
||||
shell: bash
|
||||
run: |
|
||||
if git tag --list | grep -qx "$VERSION_TAG"; then
|
||||
echo "ℹ️ Stable release $VERSION_TAG already exists — will update assets"
|
||||
else
|
||||
echo "ℹ️ Stable release $VERSION_TAG does not exist — will create it"
|
||||
fi
|
||||
|
||||
# --------------------------------------------------
|
||||
# Optional: in manual runs, allow user to DISABLE updating an existing release
|
||||
# --------------------------------------------------
|
||||
- name: Stop if release exists and manual updating is disabled
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.allow_existing_release == false
|
||||
shell: bash
|
||||
run: |
|
||||
if git tag --list | grep -qx "$VERSION_TAG"; then
|
||||
echo "ℹ️ Release $VERSION_TAG already exists and allow_existing_release=false — exiting"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------- CHANGELOG (from last stable tag) ----------------
|
||||
- name: Generate changelog
|
||||
shell: bash
|
||||
run: |
|
||||
LAST_STABLE=$(git tag \
|
||||
| grep -E '^v[0-9]+(\.[0-9]+){1,3}$' \
|
||||
| sort -V \
|
||||
| tail -n 1)
|
||||
|
||||
if [ -z "$LAST_STABLE" ]; then
|
||||
RANGE="HEAD"
|
||||
else
|
||||
RANGE="$LAST_STABLE..HEAD"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "## ✅ What's New & Improved"
|
||||
git log $RANGE --pretty=format:"- %s"
|
||||
} > CHANGELOG.md
|
||||
|
||||
# ---------------- RELEASE NOTES ----------------
|
||||
- name: Prepare release notes
|
||||
shell: bash
|
||||
run: |
|
||||
DATE=$(date +'%Y-%m-%d')
|
||||
{
|
||||
echo "## 📦 [${VERSION_TAG}] – ${DATE}"
|
||||
echo ""
|
||||
cat CHANGELOG.md
|
||||
echo ""
|
||||
echo ""
|
||||
echo "| Platform | Status | Notes |"
|
||||
echo "|----------|--------|-------|"
|
||||
echo "| 🍎 macOS (.app + dmg) | ✅ Stable | ARM64 + Intel |"
|
||||
echo "| 🍎 macOS (CLI) | ✅ Stable | Per-arch tar.gz |"
|
||||
echo "| 🐧 Linux | ✅ Stable | tar.gz / .deb |"
|
||||
echo "| 🪟 Windows | ✅ Stable | CI-built |"
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "### 🛡️ Security Notice"
|
||||
echo "Antivirus warnings may occur and are likely **false positives**."
|
||||
} > RELEASE_NOTES.md
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
# ---------------- WINDOWS ----------------
|
||||
- name: Build Windows
|
||||
run: |
|
||||
dotnet publish ${PROJECT_DIR}/Jellyfin2Samsung.csproj \
|
||||
-c $CONFIGURATION -r win-x64 -p:SelfContained=true \
|
||||
-o publish/win-x64
|
||||
|
||||
mkdir -p dist
|
||||
(cd publish/win-x64 && zip -r "../../dist/${PRODUCT_NAME}-${VERSION_TAG}-win-x64.zip" .)
|
||||
|
||||
# ---------------- LINUX ----------------
|
||||
- name: Build Linux
|
||||
run: |
|
||||
dotnet publish ${PROJECT_DIR}/Jellyfin2Samsung.csproj \
|
||||
-c $CONFIGURATION -r linux-x64 -p:SelfContained=true \
|
||||
-o publish/linux-x64
|
||||
|
||||
tar -czf "dist/${PRODUCT_NAME}-${VERSION_TAG}-linux-x64.tar.gz" \
|
||||
-C publish/linux-x64 .
|
||||
|
||||
# ---------------- LINUX ICON ----------------
|
||||
- name: Linux icon
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y librsvg2-bin
|
||||
rsvg-convert -w 256 -h 256 \
|
||||
jellyfin-tizen-logo.svg -o jellyfin.png
|
||||
|
||||
# ---------------- DEB ----------------
|
||||
- name: Build .deb
|
||||
run: |
|
||||
sudo apt-get install -y dpkg-dev
|
||||
|
||||
PKG=deb
|
||||
mkdir -p $PKG/opt/jellyfin2samsung \
|
||||
$PKG/usr/share/{applications,icons/hicolor/256x256/apps}
|
||||
|
||||
cp -R publish/linux-x64/. $PKG/opt/jellyfin2samsung/
|
||||
chmod +x $PKG/opt/jellyfin2samsung/Jellyfin2Samsung
|
||||
|
||||
# 🔥 quick+dirty: allow writes where your app writes
|
||||
mkdir -p \
|
||||
$PKG/opt/jellyfin2samsung/Logs \
|
||||
$PKG/opt/jellyfin2samsung/Downloads \
|
||||
$PKG/opt/jellyfin2samsung/Assets/TizenSDB \
|
||||
$PKG/opt/jellyfin2samsung/Assets/Certificate
|
||||
chmod -R 777 \
|
||||
$PKG/opt/jellyfin2samsung/Logs \
|
||||
$PKG/opt/jellyfin2samsung/Downloads \
|
||||
$PKG/opt/jellyfin2samsung/Assets/TizenSDB \
|
||||
$PKG/opt/jellyfin2samsung/Assets/Certificate
|
||||
|
||||
cp jellyfin.png $PKG/usr/share/icons/hicolor/256x256/apps/jellyfin2samsung.png
|
||||
|
||||
cat > $PKG/usr/share/applications/jellyfin2samsung.desktop <<EOF
|
||||
[Desktop Entry]
|
||||
Name=Jellyfin2Samsung
|
||||
Exec=/opt/jellyfin2samsung/Jellyfin2Samsung
|
||||
Icon=jellyfin2samsung
|
||||
Type=Application
|
||||
Categories=Utility;
|
||||
EOF
|
||||
|
||||
mkdir -p $PKG/DEBIAN
|
||||
cat > $PKG/DEBIAN/control <<EOF
|
||||
Package: jellyfin2samsung
|
||||
Version: ${VERSION}
|
||||
Architecture: amd64
|
||||
Maintainer: MadeByPatrick
|
||||
Description: Jellyfin2Samsung
|
||||
EOF
|
||||
|
||||
dpkg-deb --build $PKG
|
||||
mv $PKG.deb dist/${PRODUCT_NAME}-${VERSION_TAG}-linux-x64.deb
|
||||
|
||||
# ---------------- CREATE/UPDATE STABLE RELEASE ----------------
|
||||
- name: Create/Update GitHub Release (stable)
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.VERSION_TAG }}
|
||||
name: ${{ env.VERSION_TAG }}
|
||||
prerelease: false
|
||||
overwrite_files: true
|
||||
files: dist/*
|
||||
body_path: RELEASE_NOTES.md
|
||||
|
||||
# ======================================================
|
||||
# MACOS (PER-ARCH CLI + APP + DMG)
|
||||
# ======================================================
|
||||
macos-app:
|
||||
runs-on: macos-latest
|
||||
needs: stable-release
|
||||
if: needs.stable-release.result == 'success'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
# ---------------- ICON ----------------
|
||||
- name: macOS icon
|
||||
run: |
|
||||
sips -s format png jellyfin-tizen-logo.svg --out jellyfin.png
|
||||
sips -z 1024 1024 jellyfin.png --out jellyfin.png
|
||||
|
||||
mkdir icon.iconset
|
||||
for s in 16 32 128 256 512; do
|
||||
sips -z $s $s jellyfin.png --out icon.iconset/icon_${s}x${s}.png
|
||||
sips -z $((s*2)) $((s*2)) jellyfin.png --out icon.iconset/icon_${s}x${s}@2x.png
|
||||
done
|
||||
iconutil -c icns icon.iconset -o jellyfin.icns
|
||||
|
||||
# ---------------- ARM64 ----------------
|
||||
- name: macOS ARM64 publish
|
||||
run: |
|
||||
dotnet publish ${PROJECT_DIR}/Jellyfin2Samsung.csproj \
|
||||
-c $CONFIGURATION -r osx-arm64 -p:SelfContained=true \
|
||||
-o publish/osx-arm64
|
||||
|
||||
mkdir -p dist
|
||||
tar -czf \
|
||||
dist/${PRODUCT_NAME}-${{ needs.stable-release.outputs.VERSION_TAG }}-macos-arm64.tar.gz \
|
||||
-C publish/osx-arm64 .
|
||||
|
||||
APP=Jellyfin2Samsung-arm64.app/Contents
|
||||
mkdir -p $APP/MacOS $APP/Resources
|
||||
cp -R publish/osx-arm64/* $APP/MacOS/
|
||||
chmod +x $APP/MacOS/Jellyfin2Samsung
|
||||
cp jellyfin.icns $APP/Resources/
|
||||
|
||||
cat > $APP/Info.plist <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key><string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleExecutable</key><string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleIdentifier</key><string>org.madebypatrick.jellyfin2samsung</string>
|
||||
<key>CFBundleVersion</key><string>${{ needs.stable-release.outputs.VERSION }}</string>
|
||||
<key>CFBundleShortVersionString</key><string>${{ needs.stable-release.outputs.VERSION }}</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
|
||||
<key>CFBundleIconFile</key><string>jellyfin.icns</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
codesign --deep --force --sign - Jellyfin2Samsung-arm64.app
|
||||
|
||||
rm -rf dmg-arm64
|
||||
mkdir -p dmg-arm64
|
||||
cp -R Jellyfin2Samsung-arm64.app dmg-arm64/Jellyfin2Samsung.app
|
||||
ln -s /Applications dmg-arm64/Applications
|
||||
hdiutil create \
|
||||
-volname Jellyfin2Samsung \
|
||||
-srcfolder dmg-arm64 \
|
||||
-format UDZO \
|
||||
dist/${PRODUCT_NAME}-${{ needs.stable-release.outputs.VERSION_TAG }}-macos-arm64.dmg
|
||||
|
||||
# ---------------- INTEL ----------------
|
||||
- name: macOS Intel publish
|
||||
run: |
|
||||
dotnet publish ${PROJECT_DIR}/Jellyfin2Samsung.csproj \
|
||||
-c $CONFIGURATION -r osx-x64 -p:SelfContained=true \
|
||||
-o publish/osx-x64
|
||||
|
||||
tar -czf \
|
||||
dist/${PRODUCT_NAME}-${{ needs.stable-release.outputs.VERSION_TAG }}-macos-x64.tar.gz \
|
||||
-C publish/osx-x64 .
|
||||
|
||||
APP=Jellyfin2Samsung-x64.app/Contents
|
||||
mkdir -p $APP/MacOS $APP/Resources
|
||||
cp -R publish/osx-x64/* $APP/MacOS/
|
||||
chmod +x $APP/MacOS/Jellyfin2Samsung
|
||||
cp jellyfin.icns $APP/Resources/
|
||||
|
||||
cat > $APP/Info.plist <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key><string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleExecutable</key><string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleIdentifier</key><string>org.madebypatrick.jellyfin2samsung</string>
|
||||
<key>CFBundleVersion</key><string>${{ needs.stable-release.outputs.VERSION }}</string>
|
||||
<key>CFBundleShortVersionString</key><string>${{ needs.stable-release.outputs.VERSION }}</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
|
||||
<key>CFBundleIconFile</key><string>jellyfin.icns</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
codesign --deep --force --sign - Jellyfin2Samsung-x64.app
|
||||
|
||||
rm -rf dmg-x64
|
||||
mkdir -p dmg-x64
|
||||
cp -R Jellyfin2Samsung-x64.app dmg-x64/Jellyfin2Samsung.app
|
||||
ln -s /Applications dmg-x64/Applications
|
||||
hdiutil create \
|
||||
-volname Jellyfin2Samsung \
|
||||
-srcfolder dmg-x64 \
|
||||
-format UDZO \
|
||||
dist/${PRODUCT_NAME}-${{ needs.stable-release.outputs.VERSION_TAG }}-macos-x64.dmg
|
||||
|
||||
# ---------------- UPLOAD MACOS ASSETS TO SAME STABLE RELEASE ----------------
|
||||
- name: Upload macOS assets to GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.stable-release.outputs.VERSION_TAG }}
|
||||
name: ${{ needs.stable-release.outputs.VERSION_TAG }}
|
||||
prerelease: false
|
||||
overwrite_files: true
|
||||
files: dist/*
|
||||
11
.github/workflows/update-version-table.yml
vendored
11
.github/workflows/update-version-table.yml
vendored
@@ -3,8 +3,18 @@ on:
|
||||
schedule:
|
||||
- cron: "0 */6 * * *"
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows: ["Beta Pre-Release"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
update-readme:
|
||||
if: |
|
||||
github.event_name != 'workflow_run' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_branch == 'beta'
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -12,6 +22,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: beta
|
||||
- name: Fetch latest releases
|
||||
id: releases
|
||||
uses: actions/github-script@v7
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -178,7 +178,6 @@ DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
|
||||
@@ -5,13 +5,18 @@ using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Styling;
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Helpers.API;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Plugins;
|
||||
using Jellyfin2Samsung.Helpers.Tizen.Certificate;
|
||||
using Jellyfin2Samsung.Helpers.Tizen.Devices;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Services;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
using Jellyfin2Samsung.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -21,7 +26,6 @@ namespace Jellyfin2Samsung
|
||||
{
|
||||
private IServiceProvider _serviceProvider;
|
||||
|
||||
// Static property to access services from anywhere
|
||||
public static IServiceProvider Services { get; private set; }
|
||||
|
||||
public override void Initialize()
|
||||
@@ -37,7 +41,6 @@ namespace Jellyfin2Samsung
|
||||
{
|
||||
DisableAvaloniaDataAnnotationValidation();
|
||||
|
||||
// Always use Dispatcher.Post for cross-platform safety
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
@@ -46,7 +49,10 @@ namespace Jellyfin2Samsung
|
||||
});
|
||||
}
|
||||
|
||||
RequestedThemeVariant = ThemeVariant.Light;
|
||||
// Apply saved theme on startup
|
||||
var themeService = _serviceProvider.GetRequiredService<IThemeService>();
|
||||
themeService.ApplyTheme();
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
@@ -56,44 +62,82 @@ namespace Jellyfin2Samsung
|
||||
|
||||
var settings = AppSettings.Load();
|
||||
|
||||
// Services
|
||||
// --------------------
|
||||
// Core services
|
||||
// --------------------
|
||||
services.AddSingleton(settings);
|
||||
services.AddSingleton<IDialogService, DialogService>();
|
||||
services.AddSingleton<ILocalizationService, LocalizationService>();
|
||||
services.AddSingleton<INetworkService, NetworkService>();
|
||||
services.AddSingleton<ITizenCertificateService, TizenCertificateService>();
|
||||
services.AddSingleton<ITizenInstallerService, TizenInstallerService>();
|
||||
services.AddSingleton<SamsungLoginService>();
|
||||
services.AddSingleton<HttpClient>();
|
||||
services.AddSingleton<IThemeService, ThemeService>();
|
||||
services.AddSingleton<IUpdaterService, UpdaterService>();
|
||||
services.AddSingleton<IUpdateDialogService, UpdateDialogService>();
|
||||
|
||||
// HttpClient (configured ONCE, with GitHub auth if available)
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var appSettings = sp.GetRequiredService<AppSettings>();
|
||||
var token = Helpers.Core.GitHubAuthHandler.ResolveToken(appSettings);
|
||||
var handler = new Helpers.Core.GitHubAuthHandler(token);
|
||||
|
||||
var client = new HttpClient(handler)
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("SamsungJellyfinInstaller/1.1");
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
|
||||
|
||||
return client;
|
||||
});
|
||||
|
||||
services.AddSingleton<SamsungLoginService>();
|
||||
services.AddSingleton<JellyfinApiClient>();
|
||||
services.AddSingleton<TizenApiClient>();
|
||||
services.AddSingleton<PluginManager>();
|
||||
services.AddSingleton<JellyfinPackagePatcher>();
|
||||
|
||||
// --------------------
|
||||
// Helpers
|
||||
// --------------------
|
||||
services.AddSingleton<DeviceHelper>();
|
||||
services.AddSingleton<PackageHelper>();
|
||||
services.AddSingleton<JellyfinHelper>();
|
||||
services.AddSingleton<CertificateHelper>();
|
||||
services.AddSingleton<FileHelper>();
|
||||
services.AddSingleton<ProcessHelper>();
|
||||
services.AddSingleton<TvLogService>();
|
||||
|
||||
// --------------------
|
||||
// ViewModels
|
||||
services.AddSingleton<MainWindowViewModel>();
|
||||
services.AddSingleton<SettingsViewModel>();
|
||||
// --------------------
|
||||
services.AddTransient<MainWindowViewModel>();
|
||||
services.AddTransient<InstallationCompleteViewModel>();
|
||||
services.AddTransient<InstallingWindowViewModel>();
|
||||
services.AddTransient<TvLogsViewModel>();
|
||||
services.AddSingleton<JellyfinConfigViewModel>();
|
||||
|
||||
// JellyfinConfigViewModel requires JellyfinHelper
|
||||
services.AddTransient(provider =>
|
||||
{
|
||||
var helper = provider.GetRequiredService<JellyfinHelper>();
|
||||
var localization = provider.GetRequiredService<ILocalizationService>();
|
||||
return new JellyfinConfigViewModel(helper, localization);
|
||||
});
|
||||
|
||||
// --------------------
|
||||
// Views
|
||||
// --------------------
|
||||
services.AddSingleton(provider =>
|
||||
{
|
||||
return new MainWindow
|
||||
var vm = provider.GetRequiredService<MainWindowViewModel>();
|
||||
|
||||
var window = new MainWindow
|
||||
{
|
||||
DataContext = provider.GetRequiredService<MainWindowViewModel>()
|
||||
DataContext = vm
|
||||
};
|
||||
|
||||
// IMPORTANT: prevent memory leak
|
||||
window.Closed += (_, _) =>
|
||||
{
|
||||
if (vm is IDisposable d)
|
||||
d.Dispose();
|
||||
};
|
||||
|
||||
return window;
|
||||
});
|
||||
|
||||
services.AddTransient(provider =>
|
||||
@@ -117,11 +161,13 @@ namespace Jellyfin2Samsung
|
||||
return new InstallationCompleteWindow(vm);
|
||||
});
|
||||
|
||||
// Build and assign service provider
|
||||
// --------------------
|
||||
// Build provider
|
||||
// --------------------
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
Services = _serviceProvider;
|
||||
|
||||
// Set localization service globally
|
||||
// Localization bootstrap
|
||||
var localizationService = _serviceProvider.GetRequiredService<ILocalizationService>();
|
||||
LocalizationExtensions.SetLocalizationService(localizationService);
|
||||
}
|
||||
@@ -129,7 +175,9 @@ namespace Jellyfin2Samsung
|
||||
private void DisableAvaloniaDataAnnotationValidation()
|
||||
{
|
||||
var dataValidationPluginsToRemove =
|
||||
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
|
||||
BindingPlugins.DataValidators
|
||||
.OfType<DataAnnotationsValidationPlugin>()
|
||||
.ToArray();
|
||||
|
||||
foreach (var plugin in dataValidationPluginsToRemove)
|
||||
BindingPlugins.DataValidators.Remove(plugin);
|
||||
|
||||
BIN
Jellyfin2Samsung-CrossOS/Assets/Certificate/Jellyfin/author.p12
Normal file
BIN
Jellyfin2Samsung-CrossOS/Assets/Certificate/Jellyfin/author.p12
Normal file
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
tizenpkcs12passfordsigner
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/af.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/af.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ar.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ar.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ca.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ca.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/cs.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/cs.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
@@ -1,57 +1,37 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Installer",
|
||||
"InstallationFailed": "Installation mislykkedes",
|
||||
"InstallationSuccessfulOn": "Jellyfin er blevet installeret!",
|
||||
"InstallationSuccessfulOn": "{0} er blevet installeret!",
|
||||
"DownloadFailed": "Download mislykkedes:",
|
||||
"FailedLoadingReleases": "Kunne ikke indlæse udgivelser:",
|
||||
"InstallTizenSdb": "Tizen SDB er påkrævet, men ikke fundet. Prøv at downloade igen.",
|
||||
"FailedTizenSdb": "Tizen SDB er påkrævet, men kunne ikke findes og downloades.",
|
||||
"DownloadingPackage": "Downloader pakke...",
|
||||
"ConnectingToDevice": "Opretter forbindelse til enhed...",
|
||||
"RetrievingDeviceAddress": "Henter enhedsadresse...",
|
||||
"TvNameNotFound": "TV-navn",
|
||||
"TvDuidNotFound": "TV-duid kunne ikke findes...",
|
||||
"UpdatingCertificateProfile": "Opdaterer certifikatprofil...",
|
||||
"CreatingCertificateProfile": "Opretter ny certifikatprofil...",
|
||||
"PackagingWgtWithCertificate": "Pakker wgt-filen med certifikat...",
|
||||
"InstallationSuccessful": "Installation gennemført",
|
||||
"InstallingPackage": "Installerer pakke på enhed...",
|
||||
"InstallationMaybeFailed": "Installationen kan være mislykkedes",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Indlæser udgivelser...",
|
||||
"ScanningNetwork": "Søger i netværk efter Samsung TV...",
|
||||
"CheckTizenOS": "Checker Tizen OS-version...",
|
||||
"SuccessAuthCode": "Autorisationskode modtaget",
|
||||
"Ready": "Klar til brug...",
|
||||
"SamsungLogin": "Logerer ind på Samsung...",
|
||||
"FailedAuthCode": "Login blev annulleret eller mislykkedes...",
|
||||
"NoDevicesFound": "Ingen enheder fundet",
|
||||
"OutputDir": "Sikrer at outputmappen eksisterer",
|
||||
"GenPassword": "Genererer tilfældig adgangskode...",
|
||||
"GenKeyPair": "Genererer nøglepar",
|
||||
"CreateAuthorCsr": "Opretter Author CSR...",
|
||||
"CreateDistributorCSR": "Genererer Distributor CSR med DUID...",
|
||||
"PostAuthorCSR": "Sender til Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Sender til Samsung distributor...",
|
||||
"CreateNewCertificates": "Opretter de signerede P12-filer...",
|
||||
"ExportPfxCertificates": "Eksporterer PFX-certifikater...",
|
||||
"SettingCertificateManager": "Retter Tizen certificate manager...",
|
||||
"SettingsCaCerts": "Indstiller CA-certifikater...",
|
||||
"ChooseRelease": "Vælg udgivelse...",
|
||||
"ChooseVersion": "Vælg version...",
|
||||
"ChooseTV": "Vælg TV...",
|
||||
"SelectLanguage": "Vælg sprog",
|
||||
"SelectCertificate": "Vælg certifikat",
|
||||
"SelectWGT": "Vælg WGT og/eller TPK-fil(er)",
|
||||
"lblRelease": "Udgivelse",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Vælg TV",
|
||||
"lblLanguage": "Sprog",
|
||||
"lblCustomWgt": "Brugerdefineret WGT",
|
||||
"lblSettings": "Indstillinger",
|
||||
"lblRememberIp": "Husk IP'er",
|
||||
"lblCustomWgt": "WGT-fil",
|
||||
"lblSettings": "Applikationsindstillinger",
|
||||
"lblDeletePrevious": "Fjern gamle Jellyfin",
|
||||
"lblexpires": "Udløber",
|
||||
"IpWindowTitle": "Indtast TV IP",
|
||||
"IpWindowDescription": "Indtast venligst enhedens IP-adresse:",
|
||||
"InvalidDeviceIp": "Ugyldig enheds-IP eller enhed ikke fundet.",
|
||||
@@ -62,33 +42,15 @@
|
||||
"NoPackageToInstall": "Ingen pakke at installere",
|
||||
"NoDeviceSelected": "Ingen enhed valgt",
|
||||
"UsingCustomWGT": "Bruger brugerdefineret WGT-fil",
|
||||
"FailedRemoveOld": "Kunne ikke fjerne gammel app-version",
|
||||
"FailedRemoveOldExtra": "Fjern venligst den gamle Jellyfin-app manuelt fra TV'et",
|
||||
"CheckingTizenSdb": "Checker Tizen SDB...",
|
||||
"InitializationFailed": "Initialisering mislykkedes...",
|
||||
"DownloadingSetupFile": "Downloader installer...",
|
||||
"InstallingSetupFile": "Installerer Tizen Studio CLI...",
|
||||
"InstallingCertificateManager": "Installerer Certificate Manager...",
|
||||
"ReInstallingCertificateManager": "Installation af Certifikathåndtering-tilføjelsen mislykkedes. Vil du prøve igen?",
|
||||
"InstallingCertificateAddOn": "Installerer Certificate Manager add-on...",
|
||||
"CheckingPackageManagerList": "Checker Tizen Package manager",
|
||||
"PermissionDenied": "Adgang nægtet",
|
||||
"AdminPrivRequired": "Administratorrettigheder er påkrævet for at oprette mappen. Opret den manuelt eller kør programmet som Administrator.",
|
||||
"PathLengthWarning": "Windows-stilængde overskredet",
|
||||
"PathLengthExceeded": "Standardstien overskrider Windows-stilængden. Vil du ændre den til C:\\Tizen Studio Cli?",
|
||||
"lblForceLogin": "Tving Samsung Login",
|
||||
"lblForceLogin": "Tving Samsung-certifikat",
|
||||
"DeveloperModeRequired": "Samsung TV er ikke i developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP matcher ikke denne enheds lokale IP, vil du fortsætte?",
|
||||
"lblRTL": "Højre-til-venstre (RTL) læsning",
|
||||
"FailedToStopSdbServer": "Der kører allerede en SDB Server, kunne ikke stoppe...",
|
||||
"FailedToStopSdbServerTitle": "SDB Server kunne ikke stoppe!",
|
||||
"lblJellyfinServer": "Adresse",
|
||||
"lblModifyConfig": "Indstil Jellyfin Config",
|
||||
"ConfigFailure": "config.json ikke fundet i den udpakkede pakke.",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Omvendt IP (for arabisk/hebraisk)",
|
||||
"ServerIP": "Server IP",
|
||||
"ServerPort": "Port",
|
||||
"Theme": "Tema",
|
||||
"SelectTheme": "Vælg tema",
|
||||
"lblEnableBackdrops": "Aktiver baggrunde",
|
||||
"lblEnableThemeSongs": "Aktiver temasange",
|
||||
"lblEnableThemeVideos": "Aktiver temavideoer",
|
||||
@@ -98,31 +60,13 @@
|
||||
"lblNextUpEnabled": "Næste aktiveret",
|
||||
"lblEnableExternalVideoPlayers": "Aktiver eksterne videomedspillere",
|
||||
"lblSkipIntros": "Spring intros over",
|
||||
"lblAutoPlayNextEpisode": "Automatisk afspil næste afsnit",
|
||||
"lblRememberAudioSelections": "Husk lydvalg",
|
||||
"lblRememberSubtitleSelections": "Husk undertekstvalg",
|
||||
"lblPlayDefaultAudioTrack": "Afspil standard lydspor",
|
||||
"lblSubtitleMode": "Underteksttilstand",
|
||||
"lblAudioLanguagePreference": "Foretrukket lydsprog",
|
||||
"lblSubtitleLanguagePreference": "Foretrukket undertekstsprog",
|
||||
"lblJellyfinConfig": "Jellyfin Config",
|
||||
"SelectSubtitleMode": "Vælg underteksttilstand",
|
||||
"lblJellyfinConfig": "Jellyfin-indstillinger",
|
||||
"AudioLanguage": "f.eks. da | en | de",
|
||||
"lblOpenConfig": "Åbn config",
|
||||
"lblValidateLogin": "Valider",
|
||||
"lblValidateJellyfin": "Valider Jellyfin Server",
|
||||
"NoJellyfinServer": "Udfyld venligst Jellyfin serveren først...",
|
||||
"JellyfinLoginFailed": "Login mislykkedes: Ugyldige legitimationsoplysninger eller serverfejl.",
|
||||
"lblJellyfinServerApi": "API-nøgle",
|
||||
"UpdateMode": "Opdateringstilstand",
|
||||
"lblServerSettings": "Serverindstillinger",
|
||||
"lblBrowserSettings": "Browserindstillinger",
|
||||
"lblUserSettings": "Brugerindstillinger",
|
||||
"lbluserAutoLogin": "Aktiver automatisk login",
|
||||
"lblJellyfinUser": "Jellyfin bruger(e)",
|
||||
"lblFailedUsers": "Kunne ikke hente nogen Jellyfin-brugere, ingen Config-indstillinger vil blive ændret!",
|
||||
"lblUpdateMode": "Vælg hvad der skal opdateres",
|
||||
"lblJellyfinAPIKey": "Jellyfin API-nøgle",
|
||||
"lblSelectUsers": "Vælg bruger(e) der skal opdateres",
|
||||
"lblValidation": "🍺 Køb mig en øl",
|
||||
"btn_Close": "Luk",
|
||||
@@ -134,14 +78,78 @@
|
||||
"keyContinue": "Fortsætte",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Bekræfte",
|
||||
"minimalCliTitle": "Tizen CLI og certifikatadministrator kræves",
|
||||
"minimalCliMessage": "Tizen CLI og certifikatadministrator er nødvendige for at fortsætte.\n\nVi downloader og installerer nu Tizen CLI efterfulgt af certifikatadministrator.\nDette kan tage et par minutter. Vær tålmodig under installationsprocessen.",
|
||||
"minimalCliStop": "Brugeren afviste at installere Tizen CLI",
|
||||
"packageAndSign": "Emballering og signering...",
|
||||
"certRetryFailed": "Gentagelse mislykkedes, opret venligst en fejl",
|
||||
"lblPermitInstall": "Tillad installation (Tizen =< 4.0)",
|
||||
"alreadyInstalled": "Jellyfin kunne ikke installeres, da den allerede er installeret. Fjern den først, og prøv igen.",
|
||||
"diagnoseTv": "Diagnosticér tv-funktioner",
|
||||
"searchApp": "Søger efter om appen allerede er installeret",
|
||||
"modiyConfigRequired": "Jellyfin-appen har brug for et nyt app-id!"
|
||||
"modiyConfigRequired": "Jellyfin-appen har brug for et nyt app-id!",
|
||||
"deleteExistingVersion": "Sletter en eksisterende version...",
|
||||
"deleteExistingFailed": "Sletning af den eksisterende version mislykkedes. Slet den manuelt...",
|
||||
"deleteExistingSuccess": "Eksisterende version slettet...",
|
||||
"deleteExistingNotAllowed": "Sletning er ikke tilladt. Aktivér indstillingen for at tillade værktøjet at fjerne den eksisterende app...",
|
||||
"lblLocalIP": "Lokal IP:",
|
||||
"lblTryOverwrite": "Overskriv eksisterende version",
|
||||
"lblUseServerScripts": "Download Jellyfin-plugins",
|
||||
"lblEnableDevLogs": "Aktivér tv-fejlfinding",
|
||||
"lblOpenDebugWindow": "Åbn TV-log",
|
||||
"lblStartLog": "Starte",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Spare",
|
||||
"lblLaunchOnInstall": "Åbn efter installation",
|
||||
"insufficientSpace": "Din enhed har ikke nok plads. Fjern nogle apps og prøv igen...",
|
||||
"lblKeepWGTFile": "Bevar WGT-fil",
|
||||
"AuthorMismatch": "Forfattercertifikat stemmer ikke overens. Underskriv venligst pakken igen med det korrekte certifikat.",
|
||||
"FixYouTube153": "Ret YouTube-plugin (fejl 153)",
|
||||
"lblBasePath": "Basissti",
|
||||
"lblJellyfinUsername": "Brugernavn",
|
||||
"lblJellyfinPassword": "Adgangskode",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-login indstillinger",
|
||||
"lblBasePathHint": "Tip: Indsæt fuld URL (f.eks. https://host.com/sti/jellyfin) for at udfylde alle felter automatisk",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Log ud",
|
||||
"lblEnableAutoLoginConfig": "Aktivér config-patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Afspilning",
|
||||
"lblServerInputMode": "Inputtilstand",
|
||||
"lblServerUrl": "Server-URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Avancerede indstillinger",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Stil",
|
||||
"lblCssSettings": "Brugerdefinerede CSS-indstillinger",
|
||||
"lblCustomCss": "Brugerdefineret CSS-kode",
|
||||
"lblCssHint": "Indtast CSS eller brug @import til at indlæse eksterne temaer",
|
||||
"lblValidateCss": "Valider CSS",
|
||||
"lblCssValidationStatus": "Valideringsstatus",
|
||||
"lblCssEmpty": "Ingen CSS at validere",
|
||||
"lblClearCss": "Ryd",
|
||||
"lblCssValidating": "Validerer...",
|
||||
"lblCssUrlFailed": "{0} URL(er) ikke tilgængelige",
|
||||
"lblCssUrlsValid": "{0} URL(er) valideret med succes",
|
||||
"lblCssSyntaxValid": "CSS-syntaks gyldig",
|
||||
"lblCssUnmatchedBrace": "Ulige klammer { }",
|
||||
"lblCssUnmatchedParen": "Ulige parenteser ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Klik på et tema for at indsætte det og automatisk validere. Besøg GitHub-repo for forhåndsvisninger.",
|
||||
"lblTabMainSettings": "Hoved",
|
||||
"lblMainSettings": "Programindstillinger",
|
||||
"lblRefreshUsers": "Opdater brugere",
|
||||
"lblUserSelectionHint": "Valgte brugere konfigureres under installationen",
|
||||
"subnetMismatch": "Enhederne er i forskellige undernetværk (netværk)",
|
||||
"UpdateAvailable": "Opdatering tilgængelig",
|
||||
"UpdateCurrentVersion": "Nuværende version:",
|
||||
"UpdateLatestVersion": "Nyeste version:",
|
||||
"UpdateReleaseNotes": "Udgivelsesnoter:",
|
||||
"UpdateManual": "Åbn udgivelser",
|
||||
"UpdateAutomatic": "Opdater nu",
|
||||
"UpdateSkip": "Spring denne version over",
|
||||
"UpdateDownloading": "Downloader opdatering...",
|
||||
"UpdateApplying": "Anvender opdatering",
|
||||
"UpdateApplyingMessage": "Applikationen genstarter for at fuldføre opdateringen.",
|
||||
"UpdateError": "Opdatering mislykkedes",
|
||||
"UpdateCheckFailed": "Kunne ikke tjekke for opdateringer",
|
||||
"IncompatiblePackage": "Pakkeversionen er ikke kompatibel med enheden.",
|
||||
"IncompatiblePackageDetailed": "Pakkeversion {0} er ikke kompatibel med Tizen OS {1}",
|
||||
"lblMdnsWarning": "Advarsel: Du bruger et mDNS-hostnavn (.local). Samsung TV'er kan ikke pålideligt opløse .local-adresser, hvilket kan få serveren til at vises som \"undefined\" på dit TV — især efter netværksafbrydelser. Brug en direkte IP-adresse (f.eks. 192.168.1.100) i stedet for en stabil forbindelse."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/de.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/de.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Herunterladen & Installieren",
|
||||
"InstallationFailed": "Installation fehlgeschlagen",
|
||||
"InstallationSuccessfulOn": "{0} wurde erfolgreich installiert!",
|
||||
"DownloadFailed": "Download fehlgeschlagen:",
|
||||
"FailedLoadingReleases": "Laden der Versionen fehlgeschlagen:",
|
||||
"InstallTizenSdb": "Das Tizen SDB ist erforderlich, wurde aber nicht gefunden. Download wird erneut versucht.",
|
||||
"FailedTizenSdb": "Tizen SDB ist erforderlich, konnte aber nicht gefunden und heruntergeladen werden.",
|
||||
"ConnectingToDevice": "Verbindung zum Gerät wird hergestellt...",
|
||||
"TvNameNotFound": "TV-Name konnte nicht gefunden werden...",
|
||||
"TvDuidNotFound": "TV-DUID konnte nicht gefunden werden...",
|
||||
"CreatingCertificateProfile": "Neues Zertifikatsprofil wird erstellt...",
|
||||
"InstallationSuccessful": "Installation erfolgreich",
|
||||
"InstallingPackage": "Paket wird auf Gerät installiert...",
|
||||
"Output": "Ausgabe",
|
||||
"LoadingReleases": "Versionen werden geladen...",
|
||||
"ScanningNetwork": "Netzwerk wird nach Samsung TV durchsucht...",
|
||||
"Ready": "Bereit zur Nutzung...",
|
||||
"NoDevicesFound": "Keine Geräte gefunden",
|
||||
"GenPassword": "Zufälliges Passwort wird generiert...",
|
||||
"GenKeyPair": "Schlüsselpaar wird generiert",
|
||||
"CreateAuthorCsr": "Autor-CSR wird erstellt...",
|
||||
"CreateDistributorCSR": "Distributor-CSR wird mit DUID generiert...",
|
||||
"PostAuthorCSR": "An Samsung-Autor-Endpunkt wird gesendet...",
|
||||
"PostDistributorCSR": "An Samsung-Distributor wird gesendet...",
|
||||
"ExportPfxCertificates": "PFX-Zertifikate werden exportiert...",
|
||||
"SelectWGT": "WGT- und/oder TPK-Datei(en) auswählen",
|
||||
"lblRelease": "Version",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "TV auswählen",
|
||||
"lblLanguage": "Sprache",
|
||||
"lblCustomWgt": "WGT-Datei",
|
||||
"lblSettings": "Anwendungseinstellungen",
|
||||
"lblDeletePrevious": "Altes Jellyfin entfernen",
|
||||
"IpWindowTitle": "TV-IP eingeben",
|
||||
"IpWindowDescription": "Bitte die IP-Adresse des Geräts eingeben:",
|
||||
"InvalidDeviceIp": "Ungültige Geräte-IP oder Gerät nicht gefunden.",
|
||||
"IpNotListed": "Meine IP ist nicht aufgeführt...",
|
||||
"lblCertifcate": "Zertifikat",
|
||||
"lblOther": "Andere",
|
||||
"DownloadCompleted": "Download abgeschlossen...",
|
||||
"NoPackageToInstall": "Kein Paket zu installieren",
|
||||
"NoDeviceSelected": "Kein Gerät ausgewählt",
|
||||
"UsingCustomWGT": "Benutzerdefinierte WGT-Datei wird verwendet",
|
||||
"CheckingTizenSdb": "Tizen SDB wird überprüft...",
|
||||
"InitializationFailed": "Initialisierung fehlgeschlagen...",
|
||||
"lblForceLogin": "Samsung-Zertifikat erzwingen",
|
||||
"DeveloperModeRequired": "Samsung TV ist nicht im Entwicklermodus...",
|
||||
"DeveloperIPMismatch": "Samsung Developer Mode IP stimmt nicht mit der lokalen IP dieses Geräts überein, möchten Sie fortfahren?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (für Arabisch/Hebräisch)",
|
||||
"ServerIP": "Server-IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Hintergrundbilder aktivieren",
|
||||
"lblEnableThemeSongs": "Theme-Songs aktivieren",
|
||||
"lblEnableThemeVideos": "Theme-Videos aktivieren",
|
||||
"lblBackdropScreensaver": "Hintergrund-Bildschirmschoner",
|
||||
"lblDetailsBanner": "Details-Banner",
|
||||
"lblCinemaMode": "Kino-Modus",
|
||||
"lblNextUpEnabled": "Als Nächstes aktiviert",
|
||||
"lblEnableExternalVideoPlayers": "Externe Videoplayer aktivieren",
|
||||
"lblSkipIntros": "Intros überspringen",
|
||||
"lblSubtitleMode": "Untertitelmodus",
|
||||
"lblAudioLanguagePreference": "Audiosprache-Präferenz",
|
||||
"lblSubtitleLanguagePreference": "Untertitelsprache-Präferenz",
|
||||
"lblJellyfinConfig": "Jellyfin-Einstellungen",
|
||||
"AudioLanguage": "z.B. de | en | fr",
|
||||
"lblServerSettings": "Servereinstellungen",
|
||||
"lblBrowserSettings": "Browser-Einstellungen",
|
||||
"lblSelectUsers": "Benutzer zum Aktualisieren auswählen",
|
||||
"lblValidation": "🍺 Kauf mir ein Bier",
|
||||
"btn_Close": "Schließen",
|
||||
"lbleasyRight": "Das war einfach, oder?",
|
||||
"NoDevicesFoundRetry": "Keine Geräte gefunden, mit virtuellem NIC erneut versuchen?",
|
||||
"RetySearchMsg": "Keine TVs gefunden, möchten Sie erneut suchen, einschließlich virtueller Netzwerkkarten?",
|
||||
"keyYes": "Ja",
|
||||
"keyNo": "Nein",
|
||||
"keyContinue": "Weiter",
|
||||
"keyStop": "Stopp",
|
||||
"keyConfirm": "Bestätigen",
|
||||
"packageAndSign": "Verpacken und Signieren...",
|
||||
"alreadyInstalled": "Jellyfin konnte nicht installiert werden, da es bereits installiert ist. Bitte entfernen Sie es zuerst und versuchen Sie es erneut...",
|
||||
"diagnoseTv": "TV-Fähigkeiten diagnostizieren",
|
||||
"modiyConfigRequired": "Jellyfin-App benötigt eine neue App-ID!",
|
||||
"deleteExistingVersion": "Existierende Version wird gelöscht...",
|
||||
"deleteExistingFailed": "Löschen der existierenden Version fehlgeschlagen. Bitte löschen Sie sie manuell...",
|
||||
"deleteExistingSuccess": "Existierende Version erfolgreich gelöscht...",
|
||||
"deleteExistingNotAllowed": "Löschen nicht erlaubt. Aktivieren Sie die Einstellung, um dem Tool zu erlauben, die existierende App zu entfernen...",
|
||||
"lblLocalIP": "Lokale IP:",
|
||||
"lblTryOverwrite": "Vorhandene Version überschreiben",
|
||||
"lblUseServerScripts": "Jellyfin-Plugins herunterladen",
|
||||
"lblEnableDevLogs": "TV-Debugging aktivieren",
|
||||
"lblOpenDebugWindow": "TV-Log öffnen",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stopp",
|
||||
"lblSaveLogs": "Speichern",
|
||||
"lblLaunchOnInstall": "Nach der Installation öffnen",
|
||||
"insufficientSpace": "Auf Ihrem Gerät ist nicht genügend Speicherplatz vorhanden. Bitte entfernen Sie einige Apps und versuchen Sie es erneut ...",
|
||||
"lblKeepWGTFile": "WGT-Datei beibehalten",
|
||||
"AuthorMismatch": "Das Autorenzertifikat stimmt nicht überein. Bitte signieren Sie das Paket erneut mit dem richtigen Zertifikat.",
|
||||
"FixYouTube153": "YouTube-Plugin reparieren (Fehler 153)",
|
||||
"lblBasePath": "Basispfad",
|
||||
"lblJellyfinUsername": "Benutzername",
|
||||
"lblJellyfinPassword": "Passwort",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login-Einstellungen",
|
||||
"lblBasePathHint": "Tipp: Vollständige URL einfügen (z.B. https://host.com/pfad/jellyfin) um alle Felder automatisch auszufüllen",
|
||||
"lblTestServer": "Server testen",
|
||||
"lblLogout": "Abmelden",
|
||||
"lblEnableAutoLoginConfig": "Config-Patching für Auto-Login aktivieren",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Wiedergabe",
|
||||
"lblServerInputMode": "Eingabemodus",
|
||||
"lblServerUrl": "Server-URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Erweiterte Einstellungen",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Benutzerdefinierte CSS-Einstellungen",
|
||||
"lblCustomCss": "Benutzerdefinierter CSS-Code",
|
||||
"lblCssHint": "CSS eingeben oder @import für externe Themes verwenden",
|
||||
"lblValidateCss": "CSS validieren",
|
||||
"lblCssValidationStatus": "Validierungsstatus",
|
||||
"lblCssEmpty": "Kein CSS zum Validieren",
|
||||
"lblClearCss": "Leeren",
|
||||
"lblCssValidating": "Wird validiert...",
|
||||
"lblCssUrlFailed": "{0} URL(s) nicht erreichbar",
|
||||
"lblCssUrlsValid": "{0} URL(s) erfolgreich validiert",
|
||||
"lblCssSyntaxValid": "CSS-Syntax gültig",
|
||||
"lblCssUnmatchedBrace": "Ungleiche Klammern { }",
|
||||
"lblCssUnmatchedParen": "Ungleiche Klammern ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Klicken Sie auf ein Theme um es einzufügen und automatisch zu validieren. Besuchen Sie das GitHub-Repo für Vorschauen.",
|
||||
"lblTabMainSettings": "Haupt",
|
||||
"lblMainSettings": "Anwendungseinstellungen",
|
||||
"lblRefreshUsers": "Benutzer aktualisieren",
|
||||
"lblUserSelectionHint": "Ausgewählte Benutzer werden bei der Installation konfiguriert",
|
||||
"subnetMismatch": "Die Geräte befinden sich in unterschiedlichen Subnetzen (Netzwerken).",
|
||||
"UpdateAvailable": "Update verfügbar",
|
||||
"UpdateCurrentVersion": "Aktuelle Version:",
|
||||
"UpdateLatestVersion": "Neueste Version:",
|
||||
"UpdateReleaseNotes": "Versionshinweise:",
|
||||
"UpdateManual": "Releases öffnen",
|
||||
"UpdateAutomatic": "Jetzt aktualisieren",
|
||||
"UpdateSkip": "Diese Version überspringen",
|
||||
"UpdateDownloading": "Update wird heruntergeladen...",
|
||||
"UpdateApplying": "Update wird angewendet",
|
||||
"UpdateApplyingMessage": "Die Anwendung wird neu gestartet, um das Update abzuschließen.",
|
||||
"UpdateError": "Update fehlgeschlagen",
|
||||
"UpdateCheckFailed": "Update-Überprüfung fehlgeschlagen",
|
||||
"IncompatiblePackage": "Paketversion ist nicht mit dem Gerät kompatibel.",
|
||||
"IncompatiblePackageDetailed": "Paketversion {0} nicht kompatibel mit Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warnung: Du verwendest einen mDNS-Hostnamen (.local). Samsung TVs können .local-Adressen nicht zuverlässig auflösen, was dazu führen kann, dass der Server auf deinem TV als \"undefined\" angezeigt wird — besonders nach Netzwerkunterbrechungen. Verwende stattdessen eine direkte IP-Adresse (z.B. 192.168.1.100) für eine stabile Verbindung."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/el.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/el.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
@@ -1,57 +1,37 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "Jellyfin has been successfully installed!",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"DownloadingPackage": "Downloading package...",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"RetrievingDeviceAddress": "Retrieving device address...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"UpdatingCertificateProfile": "Updating certificate profile...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"PackagingWgtWithCertificate": "Packaging the wgt file with certificate...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"InstallationMaybeFailed": "Installation may have failed",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"CheckTizenOS": "Checking Tizen OS version...",
|
||||
"SuccessAuthCode": "Successfully obtained authorization code",
|
||||
"Ready": "Ready for use...",
|
||||
"SamsungLogin": "Signing into Samsung...",
|
||||
"FailedAuthCode": "Login was canceled or failed...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"OutputDir": "Ensuring that the output directory exists",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"CreateNewCertificates": "Creating the signed P12 files...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SettingCertificateManager": "Fixing Tizen certificate manager...",
|
||||
"SettingsCaCerts": "Setting CA certificates...",
|
||||
"ChooseRelease": "Choose release...",
|
||||
"ChooseVersion": "Choose version...",
|
||||
"ChooseTV": "Choose TV...",
|
||||
"SelectLanguage": "Select language",
|
||||
"SelectCertificate": "Select certificate",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "Custom WGT",
|
||||
"lblSettings": "Settings",
|
||||
"lblRememberIp": "Remember IPs",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"lblexpires": "Expires",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
@@ -62,33 +42,15 @@
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"FailedRemoveOld": "Failed to remove old app version",
|
||||
"FailedRemoveOldExtra": "Please remove the old Jellyfin app by hand from the TV",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"DownloadingSetupFile": "Downloading installer...",
|
||||
"InstallingSetupFile": "Installing Tizen Studio CLI...",
|
||||
"InstallingCertificateManager": "Installing Certificate Manager...",
|
||||
"ReInstallingCertificateManager": "Installing Certificate Manager add-on failed, want to try again?",
|
||||
"InstallingCertificateAddOn": "Installing Certificate Manager add-on...",
|
||||
"CheckingPackageManagerList": "Checking Tizen Package manager",
|
||||
"PermissionDenied": "Permission Denied",
|
||||
"AdminPrivRequired": "Administrator privileges are required to create the folder. Please create it manually or rerun the application as Administrator.",
|
||||
"PathLengthWarning": "Windows Path Length Exceeded",
|
||||
"PathLengthExceeded": "The default path exceeds Windows Path length. Would you like to change it to C:\\Tizen Studio Cli?",
|
||||
"lblForceLogin": "Force Samsung Login",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"lblRTL": "Right-to-left (RTL) reading",
|
||||
"FailedToStopSdbServer": "There is a SDB Server already running, failed to stop...",
|
||||
"FailedToStopSdbServerTitle": "SDB Server failed to stop!",
|
||||
"lblJellyfinServer": "Address",
|
||||
"lblModifyConfig": "Set Jellyfin Config",
|
||||
"ConfigFailure": "config.json not found in the extracted package.",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"ServerPort": "Port",
|
||||
"Theme": "Theme",
|
||||
"SelectTheme": "Select theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
@@ -98,31 +60,13 @@
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblAutoPlayNextEpisode": "Auto play next episode",
|
||||
"lblRememberAudioSelections": "Remember audio selections",
|
||||
"lblRememberSubtitleSelections": "Remember subtitle selections",
|
||||
"lblPlayDefaultAudioTrack": "Play default audio track",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin Config",
|
||||
"SelectSubtitleMode": "Select Subtitle Mode",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblOpenConfig": "Open config",
|
||||
"lblValidateLogin": "Validate",
|
||||
"lblValidateJellyfin": "Validate Jellyfin Server",
|
||||
"NoJellyfinServer": "Please fill in the Jellyfin server first...",
|
||||
"JellyfinLoginFailed": "Login failed: Invalid credentials or server error.",
|
||||
"lblJellyfinServerApi": "API Key",
|
||||
"UpdateMode": "Update mode",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblUserSettings": "User settings",
|
||||
"lbluserAutoLogin": "Enable auto login",
|
||||
"lblJellyfinUser": "Jellyfin user(s)",
|
||||
"lblFailedUsers": "Failed to retrieve any Jellyfin Users, no Config settings will be changed!",
|
||||
"lblUpdateMode": "Choose what to update",
|
||||
"lblJellyfinAPIKey": "Jellyfin API Key",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
@@ -134,14 +78,78 @@
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"minimalCliTitle": "Tizen CLI & Certificate manager required",
|
||||
"minimalCliMessage": "Tizen CLI & Certificate manager are required to continue.\n\nWe will now download and install Tizen CLI followed by Certificate manager.\nThis may take a few minutes. Please be patient during the installation process.",
|
||||
"minimalCliStop": "User declined to install Tizen CLI",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"certRetryFailed": "Retry failed, please create an issue",
|
||||
"lblPermitInstall": "Permit Install (Tizen =< 4.0)",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"searchApp": "Searching if app is already installed",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!"
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/es.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/es.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/fi.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/fi.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/fr.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/fr.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Télécharger et installer",
|
||||
"InstallationFailed": "Échec de l'installation",
|
||||
"InstallationSuccessfulOn": "{0} a été installé avec succès !",
|
||||
"DownloadFailed": "Échec du téléchargement :",
|
||||
"FailedLoadingReleases": "Échec du chargement des versions :",
|
||||
"InstallTizenSdb": "Le Tizen SDB est requis mais introuvable. Nouvelle tentative de téléchargement.",
|
||||
"FailedTizenSdb": "Tizen SDB est requis mais n'a pas pu être trouvé ni téléchargé.",
|
||||
"ConnectingToDevice": "Connexion à l'appareil...",
|
||||
"TvNameNotFound": "Le nom du téléviseur est introuvable...",
|
||||
"TvDuidNotFound": "Le DUID du téléviseur est introuvable...",
|
||||
"CreatingCertificateProfile": "Création d'un nouveau profil de certificat...",
|
||||
"InstallationSuccessful": "Installation réussie",
|
||||
"InstallingPackage": "Installation du paquet sur l'appareil...",
|
||||
"Output": "Sortie",
|
||||
"LoadingReleases": "Chargement des versions...",
|
||||
"ScanningNetwork": "Recherche de téléviseurs Samsung sur le réseau...",
|
||||
"Ready": "Prêt à l'emploi...",
|
||||
"NoDevicesFound": "Aucun appareil trouvé",
|
||||
"GenPassword": "Génération d'un mot de passe aléatoire...",
|
||||
"GenKeyPair": "Génération de la paire de clés",
|
||||
"CreateAuthorCsr": "Création du CSR d'auteur...",
|
||||
"CreateDistributorCSR": "Génération du CSR de distributeur avec DUID...",
|
||||
"PostAuthorCSR": "Envoi au point de terminaison auteur Samsung...",
|
||||
"PostDistributorCSR": "Envoi au distributeur Samsung...",
|
||||
"ExportPfxCertificates": "Exportation des certificats PFX...",
|
||||
"SelectWGT": "Sélectionner le(s) fichier(s) WGT et/ou TPK",
|
||||
"lblRelease": "Version",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Sélectionner le téléviseur",
|
||||
"lblLanguage": "Langue",
|
||||
"lblCustomWgt": "Fichier WGT",
|
||||
"lblSettings": "Paramètres de l'application",
|
||||
"lblDeletePrevious": "Supprimer l'ancien Jellyfin",
|
||||
"IpWindowTitle": "Saisir l'adresse IP du téléviseur",
|
||||
"IpWindowDescription": "Veuillez saisir l'adresse IP de l'appareil :",
|
||||
"InvalidDeviceIp": "Adresse IP de l'appareil invalide ou appareil introuvable.",
|
||||
"IpNotListed": "Mon adresse IP n'est pas dans la liste...",
|
||||
"lblCertifcate": "Certificat",
|
||||
"lblOther": "Autre",
|
||||
"DownloadCompleted": "Téléchargement terminé...",
|
||||
"NoPackageToInstall": "Aucun paquet à installer",
|
||||
"NoDeviceSelected": "Aucun appareil sélectionné",
|
||||
"UsingCustomWGT": "Utilisation d'un fichier WGT personnalisé",
|
||||
"CheckingTizenSdb": "Vérification de Tizen SDB...",
|
||||
"InitializationFailed": "Échec de l'initialisation...",
|
||||
"lblForceLogin": "Certificat Samsung obligatoire",
|
||||
"DeveloperModeRequired": "Le téléviseur Samsung n'est pas en mode développeur...",
|
||||
"DeveloperIPMismatch": "L'adresse IP du mode développeur Samsung ne correspond pas à l'adresse IP locale de cet appareil. Souhaitez-vous continuer ?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Adresse IP inversée (pour l'arabe/l'hébreu)",
|
||||
"ServerIP": "IP du serveur",
|
||||
"Theme": "Thème",
|
||||
"lblEnableBackdrops": "Activer les arrière-plans",
|
||||
"lblEnableThemeSongs": "Activer les musiques de thème",
|
||||
"lblEnableThemeVideos": "Activer les vidéos de thème",
|
||||
"lblBackdropScreensaver": "Économiseur d'écran avec arrière-plan",
|
||||
"lblDetailsBanner": "Bannière de détails",
|
||||
"lblCinemaMode": "Mode cinéma",
|
||||
"lblNextUpEnabled": "Activer « À suivre »",
|
||||
"lblEnableExternalVideoPlayers": "Activer les lecteurs vidéo externes",
|
||||
"lblSkipIntros": "Ignorer les génériques",
|
||||
"lblSubtitleMode": "Mode sous-titres",
|
||||
"lblAudioLanguagePreference": "Préférence de langue audio",
|
||||
"lblSubtitleLanguagePreference": "Préférence de langue des sous-titres",
|
||||
"lblJellyfinConfig": "Paramètres Jellyfin",
|
||||
"AudioLanguage": "ex. fr | en | es",
|
||||
"lblServerSettings": "Paramètres du serveur",
|
||||
"lblBrowserSettings": "Paramètres du navigateur",
|
||||
"lblSelectUsers": "Sélectionner le(s) utilisateur(s) à mettre à jour",
|
||||
"lblValidation": "🍺 Offrez-moi une bière",
|
||||
"btn_Close": "Fermer",
|
||||
"lbleasyRight": "C'était facile, non ?",
|
||||
"NoDevicesFoundRetry": "Aucun appareil trouvé, réessayer avec la carte réseau virtuelle ?",
|
||||
"RetySearchMsg": "Aucun téléviseur trouvé. Voulez-vous relancer la recherche en incluant les cartes réseau virtuelles ?",
|
||||
"keyYes": "Oui",
|
||||
"keyNo": "Non",
|
||||
"keyContinue": "Continuer",
|
||||
"keyStop": "Arrêter",
|
||||
"keyConfirm": "Confirmer",
|
||||
"packageAndSign": "Empaquetage et signature...",
|
||||
"alreadyInstalled": "Jellyfin n'a pas pu être installé car il est déjà installé. Veuillez d'abord le supprimer puis réessayer...",
|
||||
"diagnoseTv": "Diagnostiquer les capacités du téléviseur",
|
||||
"modiyConfigRequired": "L'application Jellyfin nécessite un nouvel identifiant d'application !",
|
||||
"deleteExistingVersion": "Suppression d'une version existante...",
|
||||
"deleteExistingFailed": "La suppression de la version existante a échoué. Veuillez la supprimer manuellement...",
|
||||
"deleteExistingSuccess": "Version existante supprimée avec succès...",
|
||||
"deleteExistingNotAllowed": "Suppression impossible. Activez l'option pour autoriser l'outil à supprimer l'application existante...",
|
||||
"lblLocalIP": "IP locale :",
|
||||
"lblTryOverwrite": "Écraser la version existante",
|
||||
"lblUseServerScripts": "Télécharger les plugins Jellyfin",
|
||||
"lblEnableDevLogs": "Activer le débogage TV",
|
||||
"lblOpenDebugWindow": "Ouvrir le journal TV",
|
||||
"lblStartLog": "Commencer",
|
||||
"lblStopLog": "Arrêter",
|
||||
"lblSaveLogs": "Sauvegarder",
|
||||
"lblLaunchOnInstall": "Ouvrir après l'installation",
|
||||
"insufficientSpace": "Votre appareil ne dispose pas d'espace suffisant, veuillez supprimer certaines applications et réessayer...",
|
||||
"lblKeepWGTFile": "Conserver le fichier WGT",
|
||||
"AuthorMismatch": "Incompatibilité du certificat d'auteur, veuillez signer à nouveau le package avec le certificat correct.",
|
||||
"FixYouTube153": "Correction du plugin YouTube (Erreur 153)",
|
||||
"lblBasePath": "Chemin de base",
|
||||
"lblJellyfinUsername": "Nom d'utilisateur",
|
||||
"lblJellyfinPassword": "Mot de passe",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Paramètres de connexion automatique",
|
||||
"lblBasePathHint": "Astuce: Collez l'URL complète (ex: https://host.com/chemin/jellyfin) pour remplir automatiquement",
|
||||
"lblTestServer": "Tester serveur",
|
||||
"lblLogout": "Déconnexion",
|
||||
"lblEnableAutoLoginConfig": "Activer le patch config pour auto-login",
|
||||
"lblTabServer": "Serveur",
|
||||
"lblTabPlayback": "Lecture",
|
||||
"lblServerInputMode": "Mode de saisie",
|
||||
"lblServerUrl": "URL du serveur",
|
||||
"lblConnectionStatus": "Statut serveur",
|
||||
"lblAdvancedSettings": "Paramètres avancés",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "Style CSS",
|
||||
"lblCssSettings": "Paramètres CSS personnalisés",
|
||||
"lblCustomCss": "Code CSS personnalisé",
|
||||
"lblCssHint": "Entrez du CSS ou utilisez @import pour charger des thèmes externes",
|
||||
"lblValidateCss": "Valider CSS",
|
||||
"lblCssValidationStatus": "Statut de validation",
|
||||
"lblCssEmpty": "Aucun CSS à valider",
|
||||
"lblClearCss": "Effacer",
|
||||
"lblCssValidating": "Validation en cours...",
|
||||
"lblCssUrlFailed": "{0} URL(s) inaccessible(s)",
|
||||
"lblCssUrlsValid": "{0} URL(s) validée(s) avec succès",
|
||||
"lblCssSyntaxValid": "Syntaxe CSS valide",
|
||||
"lblCssUnmatchedBrace": "Accolades non appariées { }",
|
||||
"lblCssUnmatchedParen": "Parenthèses non appariées ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Cliquez sur un thème pour l'insérer et le valider automatiquement. Visitez le dépôt GitHub pour les aperçus.",
|
||||
"lblTabMainSettings": "Principal",
|
||||
"lblMainSettings": "Paramètres de l'application",
|
||||
"lblRefreshUsers": "Actualiser utilisateurs",
|
||||
"lblUserSelectionHint": "Les utilisateurs sélectionnés seront configurés lors de l'installation",
|
||||
"subnetMismatch": "Les appareils se trouvent dans différents sous-réseaux (réseau).",
|
||||
"UpdateAvailable": "Mise à jour disponible",
|
||||
"UpdateCurrentVersion": "Version actuelle:",
|
||||
"UpdateLatestVersion": "Dernière version:",
|
||||
"UpdateReleaseNotes": "Notes de version:",
|
||||
"UpdateManual": "Ouvrir les versions",
|
||||
"UpdateAutomatic": "Mettre à jour",
|
||||
"UpdateSkip": "Ignorer cette version",
|
||||
"UpdateDownloading": "Téléchargement...",
|
||||
"UpdateApplying": "Application de la mise à jour",
|
||||
"UpdateApplyingMessage": "L'application va redémarrer pour terminer la mise à jour.",
|
||||
"UpdateError": "Échec de la mise à jour",
|
||||
"UpdateCheckFailed": "Échec de la vérification des mises à jour",
|
||||
"IncompatiblePackage": "Version du package non compatible avec l'appareil.",
|
||||
"IncompatiblePackageDetailed": "La version du package {0} n'est pas compatible avec Tizen OS {1}",
|
||||
"lblMdnsWarning": "Attention : Vous utilisez un nom d'hôte mDNS (.local). Les TV Samsung ne peuvent pas résoudre de manière fiable les adresses .local, ce qui peut faire apparaître le serveur comme \"undefined\" sur votre TV — surtout après des interruptions réseau. Utilisez plutôt une adresse IP directe (ex. 192.168.1.100) pour une connexion stable."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/he.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/he.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/hu.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/hu.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/it.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/it.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ja.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ja.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ko.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ko.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
@@ -1,57 +1,37 @@
|
||||
{
|
||||
"DownloadAndInstall": "Downloaden & Installeren",
|
||||
"InstallationFailed": "Installatie mislukt",
|
||||
"InstallationSuccessfulOn": "Jellyfin is succesvol geïnstalleerd!",
|
||||
"InstallationSuccessfulOn": "{0} is succesvol geïnstalleerd!",
|
||||
"DownloadFailed": "Download mislukt:",
|
||||
"FailedLoadingReleases": "Laden van releases mislukt:",
|
||||
"InstallTizenSdb": "Tizen SDB is vereist maar niet gevonden. opnieuw proberen te downloaden.",
|
||||
"FailedTizenSdb": "Tizen SDB is vereist maar kon niet gevonden en gedownload worden.",
|
||||
"DownloadingPackage": "Pakket downloaden...",
|
||||
"ConnectingToDevice": "Verbinden met apparaat...",
|
||||
"RetrievingDeviceAddress": "Apparaatadres ophalen...",
|
||||
"TvNameNotFound": "TV-naam kon niet worden gevonden...",
|
||||
"TvDuidNotFound": "TV-duid kon niet worden gevonden...",
|
||||
"UpdatingCertificateProfile": "Certificaatsprofiel bijwerken...",
|
||||
"CreatingCertificateProfile": "Nieuw certificaatprofiel maken...",
|
||||
"PackagingWgtWithCertificate": "Wgt-bestand verpakken met certificaat...",
|
||||
"InstallationSuccessful": "Installatie succesvol",
|
||||
"InstallingPackage": "Pakket installeren op apparaat...",
|
||||
"InstallationMaybeFailed": "Installatie is mogelijk mislukt",
|
||||
"Output": "Uitvoer",
|
||||
"LoadingReleases": "Releases laden...",
|
||||
"ScanningNetwork": "Netwerk scannen voor Samsung TV...",
|
||||
"CheckTizenOS": "Tizen OS-versie controleren...",
|
||||
"SuccessAuthCode": "Succesvol autorisatiecode verkregen",
|
||||
"Ready": "Klaar voor gebruik...",
|
||||
"SamsungLogin": "Aanmelden bij Samsung...",
|
||||
"FailedAuthCode": "Login is geannuleerd of mislukt...",
|
||||
"NoDevicesFound": "Geen apparaten gevonden",
|
||||
"OutputDir": "Zorgen dat de uitvoermap bestaat",
|
||||
"GenPassword": "Willekeurig wachtwoord genereren...",
|
||||
"GenKeyPair": "Sleutelpaar genereren",
|
||||
"CreateAuthorCsr": "Author CSR aanmaken...",
|
||||
"CreateDistributorCSR": "Distributor CSR genereren met DUID...",
|
||||
"PostAuthorCSR": "Posten naar Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posten naar Samsung distributor...",
|
||||
"CreateNewCertificates": "Ondertekende P12-bestanden aanmaken...",
|
||||
"ExportPfxCertificates": "PFX-certificaten exporteren...",
|
||||
"SettingCertificateManager": "Tizen certificate manager repareren...",
|
||||
"SettingsCaCerts": "CA-certificaten instellen...",
|
||||
"ChooseRelease": "Release kiezen...",
|
||||
"ChooseVersion": "Versie kiezen...",
|
||||
"ChooseTV": "TV kiezen...",
|
||||
"SelectLanguage": "Taal selecteren",
|
||||
"SelectCertificate": "Certificaat selecteren",
|
||||
"SelectWGT": "WGT- en/of TPK-bestand(en) selecteren",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Versie",
|
||||
"lblSelectTv": "Kies TV",
|
||||
"lblLanguage": "Taal",
|
||||
"lblCustomWgt": "Aangepaste WGT",
|
||||
"lblSettings": "Instellingen",
|
||||
"lblRememberIp": "IP-adressen onthouden",
|
||||
"lblCustomWgt": "WGT Bestand",
|
||||
"lblSettings": "Applicatie-instellingen",
|
||||
"lblDeletePrevious": "Oude Jellyfin verwijderen",
|
||||
"lblexpires": "Verloopt",
|
||||
"IpWindowTitle": "TV IP invoeren",
|
||||
"IpWindowDescription": "Voer het IP-adres van het apparaat in:",
|
||||
"InvalidDeviceIp": "Ongeldig apparaat-IP of apparaat niet gevonden.",
|
||||
@@ -62,33 +42,15 @@
|
||||
"NoPackageToInstall": "Geen pakket om te installeren",
|
||||
"NoDeviceSelected": "Geen apparaat geselecteerd",
|
||||
"UsingCustomWGT": "Aangepast WGT-bestand gebruiken",
|
||||
"FailedRemoveOld": "Verwijderen oude app-versie mislukt",
|
||||
"FailedRemoveOldExtra": "Verwijder de oude Jellyfin-app handmatig van de TV",
|
||||
"CheckingTizenSdb": "Tizen SDB controleren...",
|
||||
"InitializationFailed": "Initialisatie mislukt...",
|
||||
"DownloadingSetupFile": "Installer downloaden...",
|
||||
"InstallingSetupFile": "Tizen Studio CLI installeren...",
|
||||
"InstallingCertificateManager": "Certificate Manager installeren...",
|
||||
"ReInstallingCertificateManager": "Het installeren van de Certificate Manager-invoegtoepassing is mislukt. Wilt u het opnieuw proberen?",
|
||||
"InstallingCertificateAddOn": "Certificate Manager add-on installeren...",
|
||||
"CheckingPackageManagerList": "Tizen Package manager controleren",
|
||||
"PermissionDenied": "Toestemming geweigerd",
|
||||
"AdminPrivRequired": "Beheerdersrechten zijn vereist om de map aan te maken. Maak deze handmatig aan of voer de applicatie uit als Beheerder.",
|
||||
"PathLengthWarning": "Windows-padlengte overschreden",
|
||||
"PathLengthExceeded": "Het standaardpad overschrijdt de Windows-padlengte. Wilt u dit wijzigen naar C:\\Tizen Studio Cli?",
|
||||
"lblForceLogin": "Samsung Login forceren",
|
||||
"lblForceLogin": "Forceer Samsung-certificaat",
|
||||
"DeveloperModeRequired": "Samsung TV staat niet in ontwikkelaarsmodus...",
|
||||
"DeveloperIPMismatch": "Samsung ontwikkelaarsmodus IP komt niet overeen met het lokale IP van dit apparaat, wilt u doorgaan?",
|
||||
"lblRTL": "Rechts-naar-links (RTL) lezen",
|
||||
"FailedToStopSdbServer": "Er is al een SDB Server actief, stoppen mislukt...",
|
||||
"FailedToStopSdbServerTitle": "SDB Server kon niet stoppen!",
|
||||
"lblJellyfinServer": "Adres",
|
||||
"lblModifyConfig": "Jellyfin Config instellen",
|
||||
"ConfigFailure": "config.json niet gevonden in het uitgepakte pakket.",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Omgekeerd IP-adres (voor Arabisch/Hebreeuws)",
|
||||
"ServerIP": "Server IP",
|
||||
"ServerPort": "Poort",
|
||||
"Theme": "Thema",
|
||||
"SelectTheme": "Thema selecteren",
|
||||
"lblEnableBackdrops": "Achtergronden inschakelen",
|
||||
"lblEnableThemeSongs": "Themaliedjes inschakelen",
|
||||
"lblEnableThemeVideos": "Themavideo's inschakelen",
|
||||
@@ -98,31 +60,13 @@
|
||||
"lblNextUpEnabled": "Volgende ingeschakeld",
|
||||
"lblEnableExternalVideoPlayers": "Externe videospelers inschakelen",
|
||||
"lblSkipIntros": "Intro's overslaan",
|
||||
"lblAutoPlayNextEpisode": "Automatisch volgende aflevering afspelen",
|
||||
"lblRememberAudioSelections": "Audioselecties onthouden",
|
||||
"lblRememberSubtitleSelections": "Ondertitelingselecties onthouden",
|
||||
"lblPlayDefaultAudioTrack": "Standaard audiospoor afspelen",
|
||||
"lblSubtitleMode": "Ondertitelingmodus",
|
||||
"lblAudioLanguagePreference": "Voorkeur audiotaal",
|
||||
"lblSubtitleLanguagePreference": "Voorkeur ondertitelingtaal",
|
||||
"lblJellyfinConfig": "Jellyfin Config",
|
||||
"SelectSubtitleMode": "Ondertitelingmodus selecteren",
|
||||
"lblJellyfinConfig": "Jellyfin-instellingen",
|
||||
"AudioLanguage": "bijv. nl | en | de",
|
||||
"lblOpenConfig": "Config openen",
|
||||
"lblValidateLogin": "Valideren",
|
||||
"lblValidateJellyfin": "Jellyfin Server valideren",
|
||||
"NoJellyfinServer": "Vul eerst de Jellyfin server in...",
|
||||
"JellyfinLoginFailed": "Login mislukt: Ongeldige inloggegevens of serverfout.",
|
||||
"lblJellyfinServerApi": "API-sleutel",
|
||||
"UpdateMode": "Updatemodus",
|
||||
"lblServerSettings": "Serverinstellingen",
|
||||
"lblBrowserSettings": "Browserinstellingen",
|
||||
"lblUserSettings": "Gebruikersinstellingen",
|
||||
"lbluserAutoLogin": "Automatisch inloggen inschakelen",
|
||||
"lblJellyfinUser": "Jellyfin gebruiker(s)",
|
||||
"lblFailedUsers": "Kon geen Jellyfin-gebruikers ophalen, geen Config-instellingen zullen worden gewijzigd!",
|
||||
"lblUpdateMode": "Kies wat moet worden bijgewerkt",
|
||||
"lblJellyfinAPIKey": "Jellyfin API-sleutel",
|
||||
"lblSelectUsers": "Selecteer gebruiker(s) om bij te werken",
|
||||
"lblValidation": "🍺 Trakteer me op een biertje",
|
||||
"btn_Close": "Sluiten",
|
||||
@@ -134,14 +78,78 @@
|
||||
"keyContinue": "Doorgaan",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Bevestig",
|
||||
"minimalCliTitle": "Tizen CLI & Certificaatbeheerder vereist",
|
||||
"minimalCliMessage": "Tizen CLI en Certificaatbeheer zijn vereist om door te gaan.\n\nWe gaan nu Tizen CLI downloaden en installeren, gevolgd door Certificaatbeheer.\nDit kan enkele minuten duren. Wees geduldig tijdens het installatieproces.",
|
||||
"minimalCliStop": "Gebruiker weigerde Tizen CLI te installeren",
|
||||
"packageAndSign": "Inpakken en ondertekenen...",
|
||||
"certRetryFailed": "Opnieuw proberen mislukt, maak een probleem aan",
|
||||
"lblPermitInstall": "Installatie toestaan (Tizen =< 4.0)",
|
||||
"alreadyInstalled": "Jellyfin kon niet worden geïnstalleerd omdat het al is geïnstalleerd. Verwijder het eerst en probeer het opnieuw.",
|
||||
"diagnoseTv": "Diagnose van tv-mogelijkheden",
|
||||
"searchApp": "Zoeken of de app al is geïnstalleerd",
|
||||
"modiyConfigRequired": "De Jellyfin-app heeft een nieuwe app-ID nodig!"
|
||||
"modiyConfigRequired": "De Jellyfin-app heeft een nieuwe app-ID nodig!",
|
||||
"deleteExistingVersion": "Bestaande versie verwijderen...",
|
||||
"deleteExistingFailed": "Het verwijderen van de bestaande versie is mislukt. Verwijder de versie handmatig....",
|
||||
"deleteExistingSuccess": "Bestaande versie succesvol verwijderd...",
|
||||
"deleteExistingNotAllowed": "Verwijderen niet toegestaan. Schakel de instelling in zodat de tool de bestaande app kan verwijderen...",
|
||||
"lblLocalIP": "Lokaal IP-adres:",
|
||||
"lblTryOverwrite": "Bestaande versie overschrijven",
|
||||
"lblUseServerScripts": "Jellyfin plugins downloaden",
|
||||
"lblEnableDevLogs": "TV-foutopsporing inschakelen",
|
||||
"lblOpenDebugWindow": "Open TV-logboek",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Opslaan",
|
||||
"lblLaunchOnInstall": "Openen na installeren",
|
||||
"insufficientSpace": "Er is onvoldoende ruimte op uw apparaat. Verwijder enkele apps en probeer het opnieuw...",
|
||||
"lblKeepWGTFile": "WGT-bestand behouden",
|
||||
"AuthorMismatch": "Auteurscertificaat komt niet overeen. Onderteken het pakket opnieuw met het juiste certificaat.",
|
||||
"FixYouTube153": "YouTube-plug-in repareren (fout 153)",
|
||||
"lblBasePath": "Basispad",
|
||||
"lblJellyfinUsername": "Gebruikersnaam",
|
||||
"lblJellyfinPassword": "Wachtwoord",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-login instellingen",
|
||||
"lblBasePathHint": "Tip: Plak volledige URL (bijv. https://host.com/pad/jellyfin) om alle velden automatisch in te vullen",
|
||||
"lblTestServer": "Server testen",
|
||||
"lblLogout": "Uitloggen",
|
||||
"lblEnableAutoLoginConfig": "Config-patching voor auto-login inschakelen",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Afspelen",
|
||||
"lblServerInputMode": "Invoermodus",
|
||||
"lblServerUrl": "Server-URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Geavanceerde instellingen",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Stijl",
|
||||
"lblCssSettings": "Aangepaste CSS-instellingen",
|
||||
"lblCustomCss": "Aangepaste CSS-code",
|
||||
"lblCssHint": "Voer CSS in of gebruik @import voor externe thema's",
|
||||
"lblValidateCss": "CSS valideren",
|
||||
"lblCssValidationStatus": "Validatiestatus",
|
||||
"lblCssEmpty": "Geen CSS om te valideren",
|
||||
"lblClearCss": "Wissen",
|
||||
"lblCssValidating": "Valideren...",
|
||||
"lblCssUrlFailed": "{0} URL('s) onbereikbaar",
|
||||
"lblCssUrlsValid": "{0} URL('s) succesvol gevalideerd",
|
||||
"lblCssSyntaxValid": "CSS-syntaxis geldig",
|
||||
"lblCssUnmatchedBrace": "Ongelijke accolades { }",
|
||||
"lblCssUnmatchedParen": "Ongelijke haakjes ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Klik op een thema om het in te voegen en automatisch te valideren. Bezoek de GitHub-repo voor previews.",
|
||||
"lblTabMainSettings": "Hoofd",
|
||||
"lblMainSettings": "Toepassingsinstellingen",
|
||||
"lblRefreshUsers": "Gebruikers vernieuwen",
|
||||
"lblUserSelectionHint": "Geselecteerde gebruikers worden geconfigureerd tijdens de installatie",
|
||||
"subnetMismatch": "De apparaten bevinden zich in verschillende subnetten (netwerken).",
|
||||
"UpdateAvailable": "Update beschikbaar",
|
||||
"UpdateCurrentVersion": "Huidige versie:",
|
||||
"UpdateLatestVersion": "Nieuwste versie:",
|
||||
"UpdateReleaseNotes": "Release-opmerkingen:",
|
||||
"UpdateManual": "Releases openen",
|
||||
"UpdateAutomatic": "Nu bijwerken",
|
||||
"UpdateSkip": "Deze versie overslaan",
|
||||
"UpdateDownloading": "Update downloaden...",
|
||||
"UpdateApplying": "Update toepassen",
|
||||
"UpdateApplyingMessage": "De applicatie wordt opnieuw gestart om de update te voltooien.",
|
||||
"UpdateError": "Update mislukt",
|
||||
"UpdateCheckFailed": "Controle op updates mislukt",
|
||||
"IncompatiblePackage": "Pakketversie niet compatibel met apparaat.",
|
||||
"IncompatiblePackageDetailed": "Pakketversie {0} niet compatibel met Tizen OS {1}",
|
||||
"lblMdnsWarning": "Waarschuwing: Je gebruikt een mDNS-hostnaam (.local). Samsung TV's kunnen .local-adressen niet betrouwbaar omzetten, waardoor de server als \"undefined\" op je TV kan verschijnen — vooral na netwerkonderbrekingen. Gebruik in plaats daarvan een direct IP-adres (bijv. 192.168.1.100) voor een stabiele verbinding."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/no.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/no.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/pl.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/pl.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/pt.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/pt.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Aviso: Você está usando um nome de host mDNS (.local). TVs Samsung não conseguem resolver endereços .local de forma confiável, o que pode fazer o servidor aparecer como \"undefined\" na sua TV — especialmente após interrupções de rede. Use um endereço IP direto (ex. 192.168.1.100) para uma conexão estável."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ro.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ro.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ru.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/ru.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/sr.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/sr.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/sv.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/sv.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/tr.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/tr.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "İndir ve Kur",
|
||||
"InstallationFailed": "Kurulum başarısız",
|
||||
"InstallationSuccessfulOn": "{0} başarıyla kuruldu!",
|
||||
"DownloadFailed": "İndirme başarısız:",
|
||||
"FailedLoadingReleases": "Sürümler yüklenemedi:",
|
||||
"InstallTizenSdb": "Tizen SDB gerekli ancak bulunamadı. İndirme yeniden deneniyor.",
|
||||
"FailedTizenSdb": "Tizen SDB gerekli ancak bulunamadı ve indirilemedi.",
|
||||
"ConnectingToDevice": "Cihaza bağlanılıyor...",
|
||||
"TvNameNotFound": "TV Adı bulunamadı...",
|
||||
"TvDuidNotFound": "TV duid bulunamadı...",
|
||||
"CreatingCertificateProfile": "Yeni sertifika profili oluşturuluyor...",
|
||||
"InstallationSuccessful": "Kurulum başarılı",
|
||||
"InstallingPackage": "Paket cihaza kuruluyor...",
|
||||
"Output": "Çıktı",
|
||||
"LoadingReleases": "Sürümler yükleniliyor...",
|
||||
"ScanningNetwork": "Samsung TV için ağ taranıyor...",
|
||||
"Ready": "Kullanıma hazır...",
|
||||
"NoDevicesFound": "Cihaz bulunamadı",
|
||||
"GenPassword": "Rastgele şifre oluşturuluyor...",
|
||||
"GenKeyPair": "Anahtar çifti oluşturuluyor",
|
||||
"CreateAuthorCsr": "Yazar CSR oluşturuluyor...",
|
||||
"CreateDistributorCSR": "DUID ile Dağıtıcı CSR oluşturuluyor...",
|
||||
"PostAuthorCSR": "Samsung yazar uç noktasına gönderiliyor...",
|
||||
"PostDistributorCSR": "Samsung dağıtıcısına gönderiliyor...",
|
||||
"ExportPfxCertificates": "PFX sertifikaları dışa aktarılıyor...",
|
||||
"SelectWGT": "WGT ve/veya TPK dosyası seç",
|
||||
"lblRelease": "Sürüm",
|
||||
"lblVersion": "Versiyon",
|
||||
"lblSelectTv": "TV Seç",
|
||||
"lblLanguage": "Dil",
|
||||
"lblCustomWgt": "WGT Dosyası",
|
||||
"lblSettings": "Uygulama ayarları",
|
||||
"lblDeletePrevious": "Eski Jellyfin'i kaldır",
|
||||
"IpWindowTitle": "TV IP Adresini Girin",
|
||||
"IpWindowDescription": "Lütfen cihazın IP adresini girin:",
|
||||
"InvalidDeviceIp": "Geçersiz cihaz IP'si veya cihaz bulunamadı.",
|
||||
"IpNotListed": "Benim IP'm listede yok...",
|
||||
"lblCertifcate": "Sertifika",
|
||||
"lblOther": "Diğer",
|
||||
"DownloadCompleted": "İndirme tamamlandı...",
|
||||
"NoPackageToInstall": "Kurulacak paket yok",
|
||||
"NoDeviceSelected": "Cihaz seçilmedi",
|
||||
"UsingCustomWGT": "Özel WGT dosyası kullanılıyor",
|
||||
"CheckingTizenSdb": "Tizen SDB kontrol ediliyor...",
|
||||
"InitializationFailed": "Başlatma başarısız...",
|
||||
"lblForceLogin": "Samsung sertifikasını zorla",
|
||||
"DeveloperModeRequired": "Samsung TV geliştirici modunda değil...",
|
||||
"DeveloperIPMismatch": "Samsung Geliştirici modu IP'si bu cihazın yerel IP'siyle eşleşmiyor, devam etmek istiyor musunuz?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Ters IP (Arapça/İbranice için)",
|
||||
"ServerIP": "Sunucu IP",
|
||||
"Theme": "Tema",
|
||||
"lblEnableBackdrops": "Arka planları etkinleştir",
|
||||
"lblEnableThemeSongs": "Tema şarkılarını etkinleştir",
|
||||
"lblEnableThemeVideos": "Tema videolarını etkinleştir",
|
||||
"lblBackdropScreensaver": "Arka plan ekran koruyucu",
|
||||
"lblDetailsBanner": "Detay bannerı",
|
||||
"lblCinemaMode": "Sinema modu",
|
||||
"lblNextUpEnabled": "Sıradaki etkin",
|
||||
"lblEnableExternalVideoPlayers": "Harici video oynatıcıları etkinleştir",
|
||||
"lblSkipIntros": "Intro'ları atla",
|
||||
"lblSubtitleMode": "Alt yazı modu",
|
||||
"lblAudioLanguagePreference": "Ses dili tercihi",
|
||||
"lblSubtitleLanguagePreference": "Alt yazı dili tercihi",
|
||||
"lblJellyfinConfig": "Jellyfin ayarları",
|
||||
"AudioLanguage": "örn. tr | en | da",
|
||||
"lblServerSettings": "Sunucu ayarları",
|
||||
"lblBrowserSettings": "Tarayıcı ayarları",
|
||||
"lblSelectUsers": "Güncellenecek kullanıcı(lar)ı seç",
|
||||
"lblValidation": "🍺 Bana bir bira ısmarla",
|
||||
"btn_Close": "Kapat",
|
||||
"lbleasyRight": "Kolaydı, değil mi?",
|
||||
"NoDevicesFoundRetry": "Cihaz bulunamadı, sanal NIC ile yeniden denensin mi?",
|
||||
"RetySearchMsg": "TV bulunamadı, arama sanal ağ kartları dahil edilerek tekrar yapılsın mı?",
|
||||
"keyYes": "Evet",
|
||||
"keyNo": "Hayır",
|
||||
"keyContinue": "Devam",
|
||||
"keyStop": "Durdur",
|
||||
"keyConfirm": "Onayla",
|
||||
"packageAndSign": "Paketleniyor ve imzalanıyor...",
|
||||
"alreadyInstalled": "Jellyfin zaten kurulu olduğu için kurulamadı, lütfen önce kaldırın ve tekrar deneyin...",
|
||||
"diagnoseTv": "TV yeteneklerini tani",
|
||||
"modiyConfigRequired": "Jellyfin uygulamasının yeni bir uygulama kimliğine ihtiyacı var!",
|
||||
"deleteExistingVersion": "Mevcut sürüm siliniyor...",
|
||||
"deleteExistingFailed": "Mevcut sürümü silme başarısız. Lütfen manuel olarak silin...",
|
||||
"deleteExistingSuccess": "Mevcut sürüm başarıyla silindi...",
|
||||
"deleteExistingNotAllowed": "Silmeye izin verilmiyor. Aracın mevcut uygulamayı kaldırmasına izin vermek için ayarı etkinleştirin...",
|
||||
"lblLocalIP": "Yerel IP:",
|
||||
"lblTryOverwrite": "Mevcut sürümü üstüne yaz",
|
||||
"lblUseServerScripts": "Jellyfin eklentilerini indir",
|
||||
"lblEnableDevLogs": "TV Hata Ayıklamasını Etkinleştir",
|
||||
"lblOpenDebugWindow": "TV Logunu Aç",
|
||||
"lblStartLog": "Başlat",
|
||||
"lblStopLog": "Durdur",
|
||||
"lblSaveLogs": "Kaydet",
|
||||
"lblLaunchOnInstall": "Kurulumdan sonra aç",
|
||||
"insufficientSpace": "Cihazınızda yetersiz alan var, lütfen bazı uygulamaları kaldırın ve tekrar deneyin...",
|
||||
"lblKeepWGTFile": "WGT dosyasını koru",
|
||||
"AuthorMismatch": "Yazar Sertifikası uyumsuzluğu, lütfen paketi doğru sertifika ile yeniden imzalayın.",
|
||||
"FixYouTube153": "YouTube Eklentisini Düzelt (Hata 153)",
|
||||
"lblBasePath": "Temel Yol",
|
||||
"lblJellyfinUsername": "Kullanıcı Adı",
|
||||
"lblJellyfinPassword": "Şifre",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Otomatik Giriş Ayarları",
|
||||
"lblBasePathHint": "İpucu: Tüm alanları otomatik doldurmak için tam URL yapıştırın",
|
||||
"lblTestServer": "Sunucuyu Test Et",
|
||||
"lblLogout": "Çıkış Yap",
|
||||
"lblEnableAutoLoginConfig": "Otomatik giriş için config yaması etkinleştir",
|
||||
"lblTabServer": "Sunucu",
|
||||
"lblTabPlayback": "Oynatma",
|
||||
"lblServerInputMode": "Giriş Modu",
|
||||
"lblServerUrl": "Sunucu URL",
|
||||
"lblConnectionStatus": "Sunucu Durumu",
|
||||
"lblAdvancedSettings": "Gelişmiş Ayarlar",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Stili",
|
||||
"lblCssSettings": "Özel CSS Ayarları",
|
||||
"lblCustomCss": "Özel CSS Kodu",
|
||||
"lblCssHint": "CSS girin veya harici temalar için @import kullanın",
|
||||
"lblValidateCss": "CSS Doğrula",
|
||||
"lblCssValidationStatus": "Doğrulama Durumu",
|
||||
"lblCssEmpty": "Doğrulanacak CSS yok",
|
||||
"lblClearCss": "Temizle",
|
||||
"lblCssValidating": "Doğrulanıyor...",
|
||||
"lblCssUrlFailed": "{0} URL(ler) ulaşılamıyor",
|
||||
"lblCssUrlsValid": "{0} URL(ler) başarıyla doğrulandı",
|
||||
"lblCssSyntaxValid": "CSS söz dizimi geçerli",
|
||||
"lblCssUnmatchedBrace": "Eşleşmeyen süslü parantezler { }",
|
||||
"lblCssUnmatchedParen": "Eşleşmeyen parantezler ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Bir temaya tıklayarak ekleyin ve otomatik olarak doğrulayın. Önizlemeler için GitHub deposunu ziyaret edin.",
|
||||
"lblTabMainSettings": "Ana",
|
||||
"lblMainSettings": "Uygulama Ayarları",
|
||||
"lblRefreshUsers": "Kullanıcıları Yenile",
|
||||
"lblUserSelectionHint": "Seçilen kullanıcılar kurulum sırasında yapılandırılacak",
|
||||
"subnetMismatch": "Cihazlar farklı alt ağlarda (ağda) bulunmaktadır.",
|
||||
"UpdateAvailable": "Güncelleme mevcut",
|
||||
"UpdateCurrentVersion": "Mevcut sürüm:",
|
||||
"UpdateLatestVersion": "Son sürüm:",
|
||||
"UpdateReleaseNotes": "Sürüm notları:",
|
||||
"UpdateManual": "Sürümleri aç",
|
||||
"UpdateAutomatic": "Şimdi güncelle",
|
||||
"UpdateSkip": "Bu sürümü atla",
|
||||
"UpdateDownloading": "Güncelleme indiriliyor...",
|
||||
"UpdateApplying": "Güncelleme uygulanıyor",
|
||||
"UpdateApplyingMessage": "Güncellemeyi tamamlamak için uygulama yeniden başlatılacak.",
|
||||
"UpdateError": "Güncelleme başarısız",
|
||||
"UpdateCheckFailed": "Güncelleme kontrolü başarısız",
|
||||
"IncompatiblePackage": "Paket sürümü cihazla uyumlu değil.",
|
||||
"IncompatiblePackageDetailed": "Paket sürümü {0}, Tizen OS {1} ile uyumlu değil",
|
||||
"lblMdnsWarning": "Uyarı: Bir mDNS ana bilgisayar adı (.local) kullanıyorsunuz. Samsung TV'ler .local adreslerini güvenilir şekilde çözümleyemez, bu da sunucunun TV'nizde \"undefined\" olarak görünmesine neden olabilir — özellikle ağ kesintilerinden sonra. Kararlı bir bağlantı için bunun yerine doğrudan bir IP adresi (ör. 192.168.1.100) kullanın."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/uk.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/uk.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/vi.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/vi.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
155
Jellyfin2Samsung-CrossOS/Assets/Localization/zh.json
Normal file
155
Jellyfin2Samsung-CrossOS/Assets/Localization/zh.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"DownloadAndInstall": "Download & Install",
|
||||
"InstallationFailed": "Installation failed",
|
||||
"InstallationSuccessfulOn": "{0} has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"TvNameNotFound": "TV Name could not be found...",
|
||||
"TvDuidNotFound": "TV duid could not be found...",
|
||||
"CreatingCertificateProfile": "Creating new certificate profile...",
|
||||
"InstallationSuccessful": "Installation successful",
|
||||
"InstallingPackage": "Installing package on device...",
|
||||
"Output": "Output",
|
||||
"LoadingReleases": "Loading releases...",
|
||||
"ScanningNetwork": "Scanning network for Samsung TV...",
|
||||
"Ready": "Ready for use...",
|
||||
"NoDevicesFound": "No devices found",
|
||||
"GenPassword": "Generating random password...",
|
||||
"GenKeyPair": "Generating keypair",
|
||||
"CreateAuthorCsr": "Creating Author CSR...",
|
||||
"CreateDistributorCSR": "Generating Distributor CSR with DUID...",
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"SelectWGT": "Select WGT and/or TPK file(s)",
|
||||
"lblRelease": "Release",
|
||||
"lblVersion": "Version",
|
||||
"lblSelectTv": "Select TV",
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblDeletePrevious": "Remove old Jellyfin",
|
||||
"IpWindowTitle": "Enter TV IP",
|
||||
"IpWindowDescription": "Please enter the device's IP address:",
|
||||
"InvalidDeviceIp": "Invalid device IP or device not found.",
|
||||
"IpNotListed": "My IP is not listed...",
|
||||
"lblCertifcate": "Certificate",
|
||||
"lblOther": "Other",
|
||||
"DownloadCompleted": "Download completed...",
|
||||
"NoPackageToInstall": "No package to install",
|
||||
"NoDeviceSelected": "No device selected",
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"lblForceLogin": "Force Samsung certificate",
|
||||
"DeveloperModeRequired": "Samsung TV is not in developer mode...",
|
||||
"DeveloperIPMismatch": "Samsung Developer mode IP doesn't match this devices local IP, do you wish to continue?",
|
||||
"DeveloperIPReversed": "IP is in reversed order on the TV, please re-enable developer mode and type your local IP in reversed order. (ex: 192.168.1.2 → 2.1.168.192)",
|
||||
"lblRTL": "Reverse IP (for Arabic/Hebrew)",
|
||||
"ServerIP": "Server IP",
|
||||
"Theme": "Theme",
|
||||
"lblEnableBackdrops": "Enable backdrops",
|
||||
"lblEnableThemeSongs": "Enable theme songs",
|
||||
"lblEnableThemeVideos": "Enable theme videos",
|
||||
"lblBackdropScreensaver": "Backdrop screensaver",
|
||||
"lblDetailsBanner": "Details banner",
|
||||
"lblCinemaMode": "Cinema mode",
|
||||
"lblNextUpEnabled": "Next up enabled",
|
||||
"lblEnableExternalVideoPlayers": "Enable external video players",
|
||||
"lblSkipIntros": "Skip intros",
|
||||
"lblSubtitleMode": "Subtitle mode",
|
||||
"lblAudioLanguagePreference": "Audio language preference",
|
||||
"lblSubtitleLanguagePreference": "Subtitle language preference",
|
||||
"lblJellyfinConfig": "Jellyfin settings",
|
||||
"AudioLanguage": "e.g. en | da | nl",
|
||||
"lblServerSettings": "Server settings",
|
||||
"lblBrowserSettings": "Browser settings",
|
||||
"lblSelectUsers": "Select user(s) to update",
|
||||
"lblValidation": "🍺 Buy me a beer",
|
||||
"btn_Close": "Close",
|
||||
"lbleasyRight": "That was easy, right?",
|
||||
"NoDevicesFoundRetry": "No devices found, retry with virtual NIC?",
|
||||
"RetySearchMsg": "No TV's found, want to search again with virtual network cards included in the search?",
|
||||
"keyYes": "Yes",
|
||||
"keyNo": "No",
|
||||
"keyContinue": "Continue",
|
||||
"keyStop": "Stop",
|
||||
"keyConfirm": "Confirm",
|
||||
"packageAndSign": "Packaging and signing...",
|
||||
"alreadyInstalled": "Jellyfin couldn't not be installed because its already installed, please remove first and try again...",
|
||||
"diagnoseTv": "Diagnose TV capabilities",
|
||||
"modiyConfigRequired": "Jellyfin app needs a new app id!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
"deleteExistingSuccess": "Existing version successfully deleted...",
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stop",
|
||||
"lblSaveLogs": "Save",
|
||||
"lblLaunchOnInstall": "Open after installation",
|
||||
"insufficientSpace": "Your device has insufficient space, please remove some apps and try again...",
|
||||
"lblKeepWGTFile": "Preserve WGT file",
|
||||
"AuthorMismatch": "Author Certificate mismatch, please re-sign the package with correct certificate.",
|
||||
"FixYouTube153": "Fix YouTube Plugin (Error 153)",
|
||||
"lblBasePath": "Base Path",
|
||||
"lblJellyfinUsername": "Username",
|
||||
"lblJellyfinPassword": "Password",
|
||||
"lblAuthenticate": "Test Login",
|
||||
"lblAutoLoginSettings": "Auto-Login Settings",
|
||||
"lblBasePathHint": "Tip: Paste full URL (e.g., https://host.com/path/jellyfin) to auto-fill all fields",
|
||||
"lblTestServer": "Test Server",
|
||||
"lblLogout": "Logout",
|
||||
"lblEnableAutoLoginConfig": "Enable config patching for auto-login",
|
||||
"lblTabServer": "Server",
|
||||
"lblTabPlayback": "Playback",
|
||||
"lblServerInputMode": "Input Mode",
|
||||
"lblServerUrl": "Server URL",
|
||||
"lblConnectionStatus": "Server Status",
|
||||
"lblAdvancedSettings": "Advanced Settings",
|
||||
"lblGitHubToken": "GitHub Token (PAT)",
|
||||
"lblGitHubTokenHint": "Optional. Prevents API rate limiting when fetching releases. Create one at GitHub > Settings > Developer settings > Personal access tokens.",
|
||||
"lblTabCss": "CSS Style",
|
||||
"lblCssSettings": "Custom CSS Settings",
|
||||
"lblCustomCss": "Custom CSS Code",
|
||||
"lblCssHint": "Enter custom CSS or use @import to load external themes",
|
||||
"lblValidateCss": "Validate CSS",
|
||||
"lblCssValidationStatus": "Validation Status",
|
||||
"lblCssEmpty": "No CSS to validate",
|
||||
"lblClearCss": "Clear",
|
||||
"lblCssValidating": "Validating...",
|
||||
"lblCssUrlFailed": "{0} URL(s) unreachable",
|
||||
"lblCssUrlsValid": "{0} URL(s) validated successfully",
|
||||
"lblCssSyntaxValid": "CSS syntax valid",
|
||||
"lblCssUnmatchedBrace": "Unmatched braces { }",
|
||||
"lblCssUnmatchedParen": "Unmatched parentheses ( )",
|
||||
"lblJellyThemes": "JellyThemes",
|
||||
"lblJellyThemesHint": "Click a theme to insert it and auto-validate. Visit the GitHub repo for previews.",
|
||||
"lblTabMainSettings": "Main",
|
||||
"lblMainSettings": "Application Settings",
|
||||
"lblRefreshUsers": "Refresh Users",
|
||||
"lblUserSelectionHint": "Selected users will be configured during installation",
|
||||
"subnetMismatch": "Devices are in different subnets (network)",
|
||||
"UpdateAvailable": "Update Available",
|
||||
"UpdateCurrentVersion": "Current version:",
|
||||
"UpdateLatestVersion": "Latest version:",
|
||||
"UpdateReleaseNotes": "Release notes:",
|
||||
"UpdateManual": "Open Releases",
|
||||
"UpdateAutomatic": "Update Now",
|
||||
"UpdateSkip": "Skip this version",
|
||||
"UpdateDownloading": "Downloading update...",
|
||||
"UpdateApplying": "Applying Update",
|
||||
"UpdateApplyingMessage": "The application will restart to complete the update.",
|
||||
"UpdateError": "Update failed",
|
||||
"UpdateCheckFailed": "Failed to check for updates",
|
||||
"IncompatiblePackage": "Package version not compatible with device.",
|
||||
"IncompatiblePackageDetailed": "Package version {0} not compatible with Tizen OS {1}",
|
||||
"lblMdnsWarning": "Warning: You are using an mDNS hostname (.local). Samsung TVs cannot reliably resolve .local addresses, which may cause the server to appear as \"undefined\" on your TV — especially after network interruptions. Use a direct IP address (e.g. 192.168.1.100) instead for a stable connection."
|
||||
}
|
||||
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/linux-x64/esbuild
Normal file
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/linux-x64/esbuild
Normal file
Binary file not shown.
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/macos-x64/esbuild
Normal file
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/macos-x64/esbuild
Normal file
Binary file not shown.
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/win-x64/esbuild.exe
Normal file
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/win-x64/esbuild.exe
Normal file
Binary file not shown.
32
Jellyfin2Samsung-CrossOS/Extensions/TraceListener.cs
Normal file
32
Jellyfin2Samsung-CrossOS/Extensions/TraceListener.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace Jellyfin2Samsung.Extensions
|
||||
{
|
||||
public sealed class FileTraceListener : TraceListener
|
||||
{
|
||||
private readonly string _filePath;
|
||||
|
||||
public FileTraceListener(string filePath)
|
||||
{
|
||||
_filePath = filePath;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
|
||||
}
|
||||
|
||||
public override void Write(string? message)
|
||||
{
|
||||
if (message == null) return;
|
||||
File.AppendAllText(_filePath, message);
|
||||
}
|
||||
|
||||
public override void WriteLine(string? message)
|
||||
{
|
||||
if (message == null) return;
|
||||
File.AppendAllText(
|
||||
_filePath,
|
||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}{Environment.NewLine}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
215
Jellyfin2Samsung-CrossOS/Helpers/API/JellyfinApiClient.cs
Normal file
215
Jellyfin2Samsung-CrossOS/Helpers/API/JellyfinApiClient.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.API
|
||||
{
|
||||
public class JellyfinApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public JellyfinApiClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a valid Jellyfin configuration exists with an authenticated user.
|
||||
/// Uses AccessToken from username/password authentication.
|
||||
/// </summary>
|
||||
public static bool IsValidJellyfinConfiguration()
|
||||
{
|
||||
return !string.IsNullOrEmpty(AppSettings.Default.JellyfinFullUrl) &&
|
||||
!string.IsNullOrEmpty(AppSettings.Default.JellyfinAccessToken) &&
|
||||
!string.IsNullOrEmpty(AppSettings.Default.JellyfinUserId) &&
|
||||
UrlHelper.IsValidHttpUrl($"{AppSettings.Default.JellyfinFullUrl}/Users");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the user has a valid authentication (AccessToken + UserId).
|
||||
/// </summary>
|
||||
public static bool HasValidAuthentication()
|
||||
{
|
||||
return !string.IsNullOrEmpty(AppSettings.Default.JellyfinAccessToken) &&
|
||||
!string.IsNullOrEmpty(AppSettings.Default.JellyfinUserId);
|
||||
}
|
||||
|
||||
public async Task<List<JellyfinPluginInfo>> GetInstalledPluginsAsync(string serverUrl)
|
||||
{
|
||||
var list = new List<JellyfinPluginInfo>();
|
||||
try
|
||||
{
|
||||
string url = UrlHelper.CombineUrl(serverUrl, "/Plugins");
|
||||
Trace.WriteLine("Fetching installed plugins from: " + url);
|
||||
var json = await _httpClient.GetStringAsync(url);
|
||||
var parsed = JsonSerializer.Deserialize<List<JellyfinPluginInfo>>(json, JsonSerializerOptionsProvider.Default);
|
||||
|
||||
if (parsed != null)
|
||||
list.AddRange(parsed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine("Failed to fetch /Plugins: " + ex);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<JellyfinPublicSystemInfo?> GetPublicSystemInfoAsync(string serverUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = UrlHelper.CombineUrl(serverUrl, "/System/Info/Public");
|
||||
Trace.WriteLine("Fetching Jellyfin public system info from: " + url);
|
||||
|
||||
var json = await _httpClient.GetStringAsync(url);
|
||||
return JsonSerializer.Deserialize<JellyfinPublicSystemInfo>(json, JsonSerializerOptionsProvider.Default);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine("Failed to fetch /System/Info/Public: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up HTTP headers for authenticated Jellyfin API requests.
|
||||
/// Uses the AccessToken obtained from username/password authentication.
|
||||
/// </summary>
|
||||
private void SetupHeaders()
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.Api.UserAgent);
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization",
|
||||
string.Format(Constants.Api.MediaBrowserAuthHeader, AppSettings.Default.JellyfinAccessToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates with Jellyfin using username and password.
|
||||
/// Returns the access token, user ID, and admin status on success.
|
||||
/// </summary>
|
||||
public async Task<(string? accessToken, string? userId, bool isAdmin, string? error)> AuthenticateAsync(string username, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
var serverUrl = UrlHelper.NormalizeServerUrl(AppSettings.Default.JellyfinFullUrl);
|
||||
var authUrl = $"{serverUrl}/Users/AuthenticateByName";
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Emby-Authorization", Constants.Api.EmbyAuthHeader);
|
||||
|
||||
var authPayload = new
|
||||
{
|
||||
Username = username,
|
||||
Pw = password
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(authPayload);
|
||||
using var content = new StringContent(json, Encoding.UTF8, Constants.Api.JsonContentType);
|
||||
|
||||
var response = await _httpClient.PostAsync(authUrl, content);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseJson = await response.Content.ReadAsStringAsync();
|
||||
var authResponse = JsonNode.Parse(responseJson);
|
||||
|
||||
var accessToken = authResponse?["AccessToken"]?.GetValue<string>();
|
||||
var userId = authResponse?["User"]?["Id"]?.GetValue<string>();
|
||||
var isAdmin = authResponse?["User"]?["Policy"]?["IsAdministrator"]?.GetValue<bool>() ?? false;
|
||||
|
||||
Trace.WriteLine($"[Auth] User authenticated. IsAdmin: {isAdmin}");
|
||||
|
||||
return (accessToken, userId, isAdmin, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
Trace.WriteLine($"Authentication failed: {response.StatusCode} - {errorContent}");
|
||||
return (null, null, false, $"Authentication failed: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Authentication error: {ex}");
|
||||
return (null, null, false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads all Jellyfin users. Requires admin authentication.
|
||||
/// </summary>
|
||||
public async Task<List<JellyfinUser>> LoadUsersAsync()
|
||||
{
|
||||
var users = new List<JellyfinUser>();
|
||||
try
|
||||
{
|
||||
SetupHeaders();
|
||||
var serverUrl = UrlHelper.NormalizeServerUrl(AppSettings.Default.JellyfinFullUrl);
|
||||
var usersUrl = $"{serverUrl}/Users";
|
||||
|
||||
Trace.WriteLine($"[LoadUsers] Fetching users from: {usersUrl}");
|
||||
|
||||
var response = await _httpClient.GetAsync(usersUrl);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseJson = await response.Content.ReadAsStringAsync();
|
||||
var usersArray = JsonNode.Parse(responseJson)?.AsArray();
|
||||
|
||||
if (usersArray != null)
|
||||
{
|
||||
foreach (var userNode in usersArray)
|
||||
{
|
||||
var user = new JellyfinUser
|
||||
{
|
||||
Id = userNode?["Id"]?.GetValue<string>() ?? "",
|
||||
Name = userNode?["Name"]?.GetValue<string>() ?? ""
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(user.Id) && !string.IsNullOrEmpty(user.Name))
|
||||
{
|
||||
users.Add(user);
|
||||
}
|
||||
}
|
||||
|
||||
Trace.WriteLine($"[LoadUsers] Loaded {users.Count} users");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine($"[LoadUsers] Failed to load users: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"[LoadUsers] Error loading users: {ex}");
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the current server URL is reachable by checking the parameter url endpoint.
|
||||
/// </summary>
|
||||
public async Task<bool> TestServerConnectionAsync(string testUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
var response = await _httpClient.GetAsync(testUrl);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
Jellyfin2Samsung-CrossOS/Helpers/API/TizenApiClient.cs
Normal file
90
Jellyfin2Samsung-CrossOS/Helpers/API/TizenApiClient.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.API
|
||||
{
|
||||
public class TizenApiClient
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public TizenApiClient(
|
||||
HttpClient httpClient,
|
||||
IDialogService dialogService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<NetworkDevice> GetDeveloperInfoAsync(NetworkDevice device)
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = $"http://{device.IpAddress}:{Constants.Ports.SamsungTvApiPort}/api/v2/";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
string jsonContent = await response.Content.ReadAsStringAsync();
|
||||
var jsonObject = JsonNode.Parse(jsonContent);
|
||||
|
||||
var logFilePath = Path.Combine(AppContext.BaseDirectory, "Logs", $"debug_tv_api_{DateTime.Now:yyyy-MM-dd_HH-mm-ss-fff}.log");
|
||||
await File.WriteAllTextAsync(logFilePath, jsonContent);
|
||||
|
||||
|
||||
var deviceNode = jsonObject?["device"];
|
||||
if (deviceNode == null)
|
||||
{
|
||||
return CreateFallbackDevice(device);
|
||||
}
|
||||
|
||||
return new NetworkDevice
|
||||
{
|
||||
IpAddress = deviceNode["ip"]?.GetValue<string>() ?? device.IpAddress,
|
||||
DeviceName = WebUtility.HtmlDecode(deviceNode["name"]?.GetValue<string>() ?? string.Empty),
|
||||
ModelName = deviceNode["modelName"]?.GetValue<string>() ?? string.Empty,
|
||||
Manufacturer = deviceNode["type"]?.GetValue<string>() ?? string.Empty,
|
||||
DeveloperMode = deviceNode["developerMode"]?.GetValue<string>() ?? string.Empty,
|
||||
DeveloperIP = deviceNode["developerIP"]?.GetValue<string>() ?? string.Empty
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Error connecting to Samsung TV at {device.IpAddress}: {ex.Message}");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Error parsing JSON response: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Unexpected error: {ex.Message}");
|
||||
}
|
||||
|
||||
return CreateFallbackDevice(device);
|
||||
}
|
||||
|
||||
private static NetworkDevice CreateFallbackDevice(NetworkDevice device)
|
||||
{
|
||||
return new NetworkDevice
|
||||
{
|
||||
IpAddress = device.IpAddress,
|
||||
DeviceName = device.DeviceName,
|
||||
Manufacturer = device.Manufacturer,
|
||||
DeveloperMode = string.Empty,
|
||||
DeveloperIP = string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,12 @@ namespace Jellyfin2Samsung.Helpers
|
||||
public class AppSettings
|
||||
{
|
||||
private const string FileName = "settings.json";
|
||||
public static readonly string FolderPath = Environment.CurrentDirectory;
|
||||
public static readonly string FolderPath = AppContext.BaseDirectory;
|
||||
public static readonly string FilePath = Path.Combine(FolderPath, FileName);
|
||||
public static readonly string TizenSdbPath = Path.Combine(FolderPath, "Assets", "TizenSDB");
|
||||
public static readonly string CertificatePath = Path.Combine(FolderPath, "Assets", "Certificate");
|
||||
public static readonly string ProfilePath = Path.Combine(FolderPath, "Assets", "TizenProfile");
|
||||
public static readonly string EsbuildPath = Path.Combine(FolderPath, "Assets", "esbuild");
|
||||
public static readonly string DownloadPath = Path.Combine(FolderPath, "Downloads");
|
||||
|
||||
private static AppSettings? _instance;
|
||||
@@ -23,18 +24,29 @@ namespace Jellyfin2Samsung.Helpers
|
||||
public ExistingCertificates? ChosenCertificates { get; set; }
|
||||
[JsonIgnore]
|
||||
public string CustomWgtPath { get; set; } = "";
|
||||
|
||||
[JsonIgnore]
|
||||
public string LocalIp { get; set; } = "";
|
||||
[JsonIgnore]
|
||||
public string TvIp { get; set; } = "";
|
||||
public static AppSettings Default => _instance ??= Load();
|
||||
|
||||
// ----- User-scoped settings -----
|
||||
public string Language { get; set; } = "en";
|
||||
public string Certificate { get; set; } = "Jelly2Sams";
|
||||
public bool RememberCustomIP { get; set; } = false;
|
||||
public bool DeletePreviousInstall { get; set; } = false;
|
||||
public string UserCustomIP { get; set; } = "";
|
||||
public string SavedNetworkInterfaceName { get; set; } = "";
|
||||
public bool ForceSamsungLogin { get; set; } = false;
|
||||
public bool RTLReading { get; set; } = false;
|
||||
public string JellyfinIP { get; set; } = "";
|
||||
public string JellyfinBasePath { get; set; } = "";
|
||||
public string ServerInputMode { get; set; } = "IP : Port";
|
||||
public string JellyfinUsername { get; set; } = "";
|
||||
public string JellyfinPassword { get; set; } = "";
|
||||
public string JellyfinAccessToken { get; set; } = "";
|
||||
public string JellyfinServerId { get; set; } = "";
|
||||
public string JellyfinServerLocalAddress { get; set; } = "";
|
||||
public string JellyfinServerName { get; set; } = "";
|
||||
public string AudioLanguagePreference { get; set; } = "";
|
||||
public string SubtitleLanguagePreference { get; set; } = "";
|
||||
public bool EnableBackdrops { get; set; } = false;
|
||||
@@ -46,32 +58,64 @@ namespace Jellyfin2Samsung.Helpers
|
||||
public bool NextUpEnabled { get; set; } = false;
|
||||
public bool EnableExternalVideoPlayers { get; set; } = false;
|
||||
public bool SkipIntros { get; set; } = false;
|
||||
public bool AutoPlayNextEpisode { get; set; } = true;
|
||||
public bool RememberAudioSelections { get; set; } = true;
|
||||
public bool RememberSubtitleSelections { get; set; } = true;
|
||||
public bool PlayDefaultAudioTrack { get; set; } = true;
|
||||
public bool PermitInstall { get; set; } = false;
|
||||
public string SelectedTheme { get; set; } = "dark";
|
||||
public string SelectedSubtitleMode { get; set; } = "Default";
|
||||
public string ConfigUpdateMode { get; set; } = "None";
|
||||
public string JellyfinApiKey { get; set; } = "";
|
||||
public string JellyfinUserId { get; set; } = "";
|
||||
public bool UserAutoLogin { get; set; } = true;
|
||||
public bool IsJellyfinAdmin { get; set; } = false;
|
||||
public string SelectedUserIds { get; set; } = ""; // Comma-separated list of selected user IDs for multi-user config
|
||||
public string DistributorsEndpoint_V1 { get; set; } = "https://svdca.samsungqbe.com/apis/v1/distributors";
|
||||
public string DistributorsEndpoint_V3 { get; set; } = "https://svdca.samsungqbe.com/apis/v3/distributors";
|
||||
public string AuthorEndpoint_V3 { get; set; } = "https://svdca.samsungqbe.com/apis/v3/authors";
|
||||
public bool TryOverwrite { get; set; } = true;
|
||||
public bool UseServerScripts { get; set; } = false;
|
||||
public bool OpenAfterInstall { get; set; } = false;
|
||||
public bool EnableDevLogs { get; set; } = false;
|
||||
public bool KeepWGTFile { get; set; } = false;
|
||||
public bool PatchYoutubePlugin { get; set; } = false;
|
||||
public string CustomCss { get; set; } = "";
|
||||
public bool DarkMode { get; set; } = false;
|
||||
public string GitHubToken { get; set; } = "";
|
||||
public string LocalYoutubeServer { get; set; } = string.Empty;
|
||||
|
||||
// ----- Updater settings -----
|
||||
public bool CheckForUpdatesOnStartup { get; set; } = true;
|
||||
public string SkippedUpdateVersion { get; set; } = string.Empty;
|
||||
public DateTime? LastUpdateCheck { get; set; } = null;
|
||||
|
||||
// ----- Application-scoped settings (readonly at runtime) -----
|
||||
public string ReleasesUrl { get; set; } = "https://api.github.com/repos/jeppevinkel/jellyfin-tizen-builds/releases";
|
||||
public string AuthorEndpoint { get; set; } = "https://dev.tizen.samsung.com/apis/v2/authors";
|
||||
public string AppVersion { get; set; } = "v1.8.4.2";
|
||||
public string AppVersion { get; set; } = "v2.2.0.9";
|
||||
public string TizenSdb { get; set; } = "https://api.github.com/repos/PatrickSt1991/tizen-sdb/releases";
|
||||
public string JellyfinAvRelease { get; set; } = "https://api.github.com/repos/PatrickSt1991/tizen-jellyfin-avplay/releases";
|
||||
public string JellyfinAvReleaseFork { get; set; } = "https://api.github.com/repos/asamahy/tizen-jellyfin-avplay/releases";
|
||||
public string JellyfinLegacy { get; set; } = "https://api.github.com/repos/jeppevinkel/jellyfin-tizen-builds/releases/tags/2024-10-27-1821";
|
||||
public string CommunityRelease { get; set; } = "https://api.github.com/repos/PatrickSt1991/tizen-community-packages/releases";
|
||||
public string MoonfinRelease { get; set; } = "https://api.github.com/repos/Moonfin-Client/Smart-TV/releases";
|
||||
public string LiteFinRelease { get; set; } = "https://api.github.com/repos/MoazSalem/litefin/releases";
|
||||
public string ReleaseInfo { get; set; } = "https://raw.githubusercontent.com/jeppevinkel/jellyfin-tizen-builds/refs/heads/master/README.md";
|
||||
|
||||
public string CommunityInfo { get; set; } = "https://raw.githubusercontent.com/PatrickSt1991/tizen-community-packages/refs/heads/main/README.md";
|
||||
public AppSettings() { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full Jellyfin URL including base path for reverse proxy setups.
|
||||
/// Example: https://xxx.seedhost.eu/xxx/jellyfin
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string JellyfinFullUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
var baseUrl = Core.UrlHelper.NormalizeServerUrl(JellyfinIP);
|
||||
var basePath = JellyfinBasePath?.Trim('/') ?? "";
|
||||
|
||||
if (string.IsNullOrEmpty(basePath))
|
||||
return baseUrl;
|
||||
|
||||
return $"{baseUrl}/{basePath}";
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
|
||||
54
Jellyfin2Samsung-CrossOS/Helpers/Converters/Converters.cs
Normal file
54
Jellyfin2Samsung-CrossOS/Helpers/Converters/Converters.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Converters
|
||||
{
|
||||
public class TvLogStatusToBrushConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> value switch
|
||||
{
|
||||
TvLogConnectionStatus.Connected => new SolidColorBrush(Color.Parse("#27AE60")),
|
||||
TvLogConnectionStatus.Listening => new SolidColorBrush(Color.Parse("#2980B9")),
|
||||
TvLogConnectionStatus.NoConnections => new SolidColorBrush(Color.Parse("#E67E22")),
|
||||
TvLogConnectionStatus.Stopped => new SolidColorBrush(Color.Parse("#7F8C8D")),
|
||||
_ => Brushes.Gray
|
||||
};
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public class StringEqualsConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not GitHubRelease release)
|
||||
return false;
|
||||
|
||||
return release.Name == parameter?.ToString();
|
||||
}
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public class StringNotEqualsConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not GitHubRelease release)
|
||||
return true;
|
||||
|
||||
return release.Name != parameter?.ToString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
103
Jellyfin2Samsung-CrossOS/Helpers/Core/AddLatestRelease.cs
Normal file
103
Jellyfin2Samsung-CrossOS/Helpers/Core/AddLatestRelease.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public class AddLatestRelease
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public AddLatestRelease(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<GitHubRelease>> GetReleasesAsync(string url, string prefix, string displayName, int take = 1)
|
||||
{
|
||||
if (take < 1) take = 1;
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var releases = JsonSerializer.Deserialize<List<GitHubRelease>>(
|
||||
json,
|
||||
JsonSerializerOptionsProvider.Default);
|
||||
|
||||
if (releases == null || releases.Count == 0)
|
||||
return new List<GitHubRelease>();
|
||||
|
||||
releases = releases
|
||||
.Select(r =>
|
||||
{
|
||||
r.Assets = r.Assets?
|
||||
.Where(a =>
|
||||
!string.IsNullOrWhiteSpace(a.FileName) &&
|
||||
(a.FileName.EndsWith(".wgt", StringComparison.OrdinalIgnoreCase) ||
|
||||
a.FileName.EndsWith(".tpk", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList() ?? new List<Asset>();
|
||||
|
||||
return r;
|
||||
})
|
||||
.Where(r => r.Assets.Count > 0)
|
||||
.ToList();
|
||||
|
||||
if (releases.Count == 0)
|
||||
return new List<GitHubRelease>();
|
||||
|
||||
var result = releases.Count > take ? releases.GetRange(0, take) : releases;
|
||||
|
||||
foreach (var r in result)
|
||||
{
|
||||
r.Name = string.IsNullOrWhiteSpace(displayName)
|
||||
? $"{prefix}{r.Name}"
|
||||
: displayName;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
var latest = JsonSerializer.Deserialize<GitHubRelease>(
|
||||
json,
|
||||
JsonSerializerOptionsProvider.Default);
|
||||
|
||||
if (latest == null)
|
||||
return new List<GitHubRelease>();
|
||||
|
||||
latest.Assets = latest.Assets?
|
||||
.Where(a =>
|
||||
!string.IsNullOrWhiteSpace(a.FileName) &&
|
||||
(a.FileName.EndsWith(".wgt", StringComparison.OrdinalIgnoreCase) ||
|
||||
a.FileName.EndsWith(".tpk", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList() ?? new List<Asset>();
|
||||
|
||||
if (latest.Assets.Count == 0)
|
||||
return new List<GitHubRelease>();
|
||||
|
||||
latest.Name = string.IsNullOrWhiteSpace(displayName)
|
||||
? $"{prefix}{latest.Name}"
|
||||
: displayName;
|
||||
|
||||
return new List<GitHubRelease> { latest };
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Trace.WriteLine($"Failed to fetch release from {url}: {ex}");
|
||||
return new List<GitHubRelease>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
270
Jellyfin2Samsung-CrossOS/Helpers/Core/Constants.cs
Normal file
270
Jellyfin2Samsung-CrossOS/Helpers/Core/Constants.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Centralized constants for the application.
|
||||
/// Eliminates magic strings and numbers scattered throughout the codebase.
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// Application identifiers and names.
|
||||
/// </summary>
|
||||
public static class AppIdentifiers
|
||||
{
|
||||
public const string JellyfinAppName = "Jellyfin";
|
||||
public const string Jelly2SamsDefault = "Jelly2Sams (default)";
|
||||
public const string Jelly2Sams = "Jelly2Sams";
|
||||
public const string CustomWgtFile = "Custom WGT File";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// preview image URLS for different apps.
|
||||
/// </summary>
|
||||
public static class PreviewImages
|
||||
{
|
||||
public const string Jellyfin = "https://jellyfin.org/assets/images/10.8-home-4a73a92bf90d1eeffa5081201ca9c7bb.png";
|
||||
public const string Moonfin = "https://iili.io/fs8W4Re.png";
|
||||
public const string Moonlight = "https://iili.io/fsvn6mJ.png";
|
||||
public const string Fireplace = "https://raw.githubusercontent.com/thonythony/fireplace/refs/heads/master/icon.jpg";
|
||||
public const string TVApp = "https://iili.io/fsvaHsn.png";
|
||||
public const string Twitch = "https://iili.io/fsvUNu2.md.gif";
|
||||
public const string ClubInfoBoard = "https://iili.io/fsviHQV.png";
|
||||
public const string Doom = "https://iili.io/fyofVqu.png";
|
||||
public const string TTD = "https://iili.io/qFBP2vn.png";
|
||||
public const string Litefin = "https://iili.io/qerI0la.png";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tizen installation error codes returned by the SDB tool.
|
||||
/// </summary>
|
||||
public static class TizenErrorCodes
|
||||
{
|
||||
public const string DownloadFailed116 = "download failed[116]";
|
||||
public const string InstallFailed118012 = "install failed[118012]";
|
||||
public const string InstallFailed118Minus12 = "install failed[118, -12]";
|
||||
public const string InstallFailed118 = "install failed[118]";
|
||||
public const string Installing100 = "installing[100]";
|
||||
public const string InstallCompleted = "install completed";
|
||||
public const string ResignFailed = "Re-sign failed";
|
||||
public const string Failed = "failed";
|
||||
public const string NotInstalled = "failed[132]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default values used throughout the application.
|
||||
/// </summary>
|
||||
public static class Defaults
|
||||
{
|
||||
public const string TizenOsVersion = "7.0";
|
||||
public const string SdkToolPath = "/opt/usr/apps/tmp";
|
||||
public const string HomeDeveloperPath = "/home/developer";
|
||||
public const string TizenSdbDefaultVersion = "v1.0.0";
|
||||
public const int SamsungLoginTimeoutMinutes = 5;
|
||||
public const int NetworkScanTimeoutMs = 1000;
|
||||
public const int HttpRequestTimeoutSeconds = 15;
|
||||
public const int WebSocketMonitorDelaySeconds = 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tizen version thresholds for feature compatibility.
|
||||
/// </summary>
|
||||
public static class TizenVersions
|
||||
{
|
||||
public const string CertificateRequired = "7.0";
|
||||
public const string PushInstallMax = "4.0";
|
||||
public const string IntermediateVersion = "3.0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Network ports used by the application.
|
||||
/// </summary>
|
||||
public static class Ports
|
||||
{
|
||||
public const int TizenDevPort = 26101;
|
||||
public const int SamsungTvApiPort = 8001;
|
||||
public const int SamsungLoginCallbackPort = 4794;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// File extensions and patterns.
|
||||
/// </summary>
|
||||
public static class FilePatterns
|
||||
{
|
||||
public const string WgtExtension = ".wgt";
|
||||
public const string TpkExtension = ".tpk";
|
||||
public const string P12Extension = ".p12";
|
||||
public const string CsrExtension = ".csr";
|
||||
public const string CerExtension = ".cer";
|
||||
public const string CrtExtension = ".crt";
|
||||
public const string JsExtension = ".js";
|
||||
public const string CssExtension = ".css";
|
||||
public const string WgtPattern = "*.wgt";
|
||||
public const string TpkPattern = "*.tpk";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Platform-specific binary names.
|
||||
/// </summary>
|
||||
public static class PlatformBinaries
|
||||
{
|
||||
public const string TizenSdbWindowsPattern = "TizenSdb*.exe";
|
||||
public const string TizenSdbLinuxPattern = "TizenSdb*_linux";
|
||||
public const string TizenSdbMacOsPattern = "TizenSdb*_macos";
|
||||
public const string WindowsExtension = ".exe";
|
||||
public const string LinuxSuffix = "_linux";
|
||||
public const string MacOsSuffix = "_macos";
|
||||
public const string EsbuildWindows = "win-x64";
|
||||
public const string EsbuildLinux = "linux-x64";
|
||||
public const string EsbuildMacOs = "osx-universal";
|
||||
public const string EsbuildExecutable = "esbuild";
|
||||
public const string EsbuildExecutableWindows = "esbuild.exe";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP and API related constants.
|
||||
/// </summary>
|
||||
public static class Api
|
||||
{
|
||||
public const string UserAgent = "SamsungJellyfinInstaller/1.0";
|
||||
public const string MediaBrowserAuthHeader = "MediaBrowser Token=\"{0}\"";
|
||||
public const string EmbyAuthHeader = "MediaBrowser Client=\"Samsung Jellyfin Installer\", Device=\"PC\", DeviceId=\"samsungjellyfin\", Version=\"1.0.0\"";
|
||||
public const string JsonContentType = "application/json";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Samsung API endpoints and OAuth constants.
|
||||
/// </summary>
|
||||
public static class Samsung
|
||||
{
|
||||
public const string LoopbackHost = "localhost";
|
||||
public const string CallbackPath = "/signin/callback";
|
||||
public const string OAuthClientId = "v285zxnl3h";
|
||||
public const string OAuthState = "accountcheckdogeneratedstatetext";
|
||||
public const string TokenType = "TOKEN";
|
||||
public const string SignInGateUrl = "https://account.samsung.com/accounts/be1dce529476c1a6d407c4c7578c31bd/signInGate";
|
||||
public const string PlatformVd = "VD";
|
||||
public const string PrivilegeLevelPublic = "Public";
|
||||
public const string DeveloperTypeIndividual = "Individual";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Certificate related constants.
|
||||
/// </summary>
|
||||
public static class Certificate
|
||||
{
|
||||
public const string AuthorFileName = "author.p12";
|
||||
public const string DistributorFileName = "distributor.p12";
|
||||
public const string PasswordFileName = "password.txt";
|
||||
public const string DeviceProfileFileName = "device-profile.xml";
|
||||
public const string AuthorCsrFileName = "author.csr";
|
||||
public const string DistributorCsrFileName = "distributor.csr";
|
||||
public const string SignedAuthorCerFileName = "signed_author.cer";
|
||||
public const string SignedDistributorCerFileName = "signed_distributor.cer";
|
||||
public const string AuthorCaFileName = "vd_tizen_dev_author_ca.cer";
|
||||
public const string DistributorCaFileName = "vd_tizen_dev_public2.crt";
|
||||
public const string KeyAlias = "usercertificate";
|
||||
public const string CsrSubjectAuthor = "C=, ST=, L=, O=, OU=, CN=Jelly2Sams";
|
||||
public const string CsrSubjectDistributorTemplate = "CN=TizenSDK, OU=, O=, L=, ST=, C=, emailAddress={0}";
|
||||
public const string SigningAlgorithm = "SHA256withRSA";
|
||||
public const int RsaKeySize = 2048;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Jellyfin web app paths and file names.
|
||||
/// </summary>
|
||||
public static class JellyfinWeb
|
||||
{
|
||||
public const string IndexHtml = "index.html";
|
||||
public const string ConfigJson = "config.json";
|
||||
public const string WwwFolder = "www";
|
||||
public const string PluginCacheFolder = "plugin_cache";
|
||||
public const string CredentialsStorageKey = "jellyfin_credentials";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Random string generation character sets.
|
||||
/// </summary>
|
||||
public static class CharacterSets
|
||||
{
|
||||
public const string AlphaLower = "abcdefghijklmnopqrstuvwxyz";
|
||||
public const string AlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
public const string Alpha = AlphaLower + AlphaUpper;
|
||||
public const string AlphaNumeric = Alpha + "0123456789";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updater related constants.
|
||||
/// </summary>
|
||||
public static class Updater
|
||||
{
|
||||
public const string RepoOwner = "Jellyfin2Samsung";
|
||||
public const string RepoName = "Samsung-Jellyfin-Installer";
|
||||
public const string AtomFeedUrl = "https://github.com/Jellyfin2Samsung/Samsung-Jellyfin-Installer/releases.atom";
|
||||
public const string ReleasesPageUrl = "https://github.com/Jellyfin2Samsung/Samsung-Jellyfin-Installer/releases";
|
||||
public const string LatestReleaseApiUrl = "https://api.github.com/repos/Jellyfin2Samsung/Samsung-Jellyfin-Installer/releases/latest";
|
||||
public const int UpdateCheckTimeoutSeconds = 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Localization keys used for status messages.
|
||||
/// </summary>
|
||||
public static class LocalizationKeys
|
||||
{
|
||||
public const string InstallTizenSdb = "InstallTizenSdb";
|
||||
public const string DiagnoseTv = "diagnoseTv";
|
||||
public const string AlreadyInstalled = "alreadyInstalled";
|
||||
public const string DeleteExistingVersion = "deleteExistingVersion";
|
||||
public const string DeleteExistingFailed = "deleteExistingFailed";
|
||||
public const string DeleteExistingSuccess = "deleteExistingSuccess";
|
||||
public const string DeleteExistingNotAllowed = "deleteExistingNotAllowed";
|
||||
public const string ConnectingToDevice = "ConnectingToDevice";
|
||||
public const string TvNameNotFound = "TvNameNotFound";
|
||||
public const string TvDuidNotFound = "TvDuidNotFound";
|
||||
public const string SamsungLogin = "SamsungLogin";
|
||||
public const string CreatingCertificateProfile = "CreatingCertificateProfile";
|
||||
public const string PackageAndSign = "packageAndSign";
|
||||
public const string InstallingPackage = "InstallingPackage";
|
||||
public const string InstallationFailed = "InstallationFailed";
|
||||
public const string InstallationSuccessful = "InstallationSuccessful";
|
||||
public const string InsufficientSpace = "insufficientSpace";
|
||||
public const string AuthorMismatch = "AuthorMismatch";
|
||||
public const string ModifyConfigRequired = "modiyConfigRequired";
|
||||
public const string FailedTizenSdb = "FailedTizenSdb";
|
||||
public const string CheckingTizenSdb = "CheckingTizenSdb";
|
||||
public const string ScanningNetwork = "ScanningNetwork";
|
||||
public const string InitializationFailed = "InitializationFailed";
|
||||
public const string NoDevicesFound = "NoDevicesFound";
|
||||
public const string NoDevicesFoundRetry = "NoDevicesFoundRetry";
|
||||
public const string Ready = "Ready";
|
||||
public const string FailedLoadingReleases = "FailedLoadingReleases";
|
||||
public const string InvalidDeviceIp = "InvalidDeviceIp";
|
||||
public const string LblOther = "lblOther";
|
||||
public const string IpNotListed = "IpNotListed";
|
||||
public const string IncompatiblePackage = "IncompatiblePackage";
|
||||
public const string IncompatiblePackageDetailed = "IncompatiblePackageDetailed";
|
||||
|
||||
// Updater localization keys
|
||||
public const string UpdateAvailable = "UpdateAvailable";
|
||||
public const string UpdateCurrentVersion = "UpdateCurrentVersion";
|
||||
public const string UpdateLatestVersion = "UpdateLatestVersion";
|
||||
public const string UpdateReleaseNotes = "UpdateReleaseNotes";
|
||||
public const string UpdateManual = "UpdateManual";
|
||||
public const string UpdateAutomatic = "UpdateAutomatic";
|
||||
public const string UpdateSkip = "UpdateSkip";
|
||||
public const string UpdateDownloading = "UpdateDownloading";
|
||||
public const string UpdateApplying = "UpdateApplying";
|
||||
public const string UpdateApplyingMessage = "UpdateApplyingMessage";
|
||||
public const string UpdateError = "UpdateError";
|
||||
public const string UpdateCheckFailed = "UpdateCheckFailed";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Esbuild transpilation settings.
|
||||
/// </summary>
|
||||
public static class Esbuild
|
||||
{
|
||||
public const string TempFolderName = "J2S_Esbuild";
|
||||
public const string TargetEs2015 = "es2015";
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Jellyfin2Samsung-CrossOS/Helpers/Core/EsbuildHelper.cs
Normal file
93
Jellyfin2Samsung-CrossOS/Helpers/Core/EsbuildHelper.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public static class EsbuildHelper
|
||||
{
|
||||
public static string? GetEsbuildPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
string baseDir = AppContext.BaseDirectory;
|
||||
return PlatformService.GetEsbuildPath(Path.Combine(baseDir, AppSettings.EsbuildPath));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transpiles ES2015+ JavaScript to ES5 using esbuild.
|
||||
/// If esbuild is missing or fails, returns the original JS.
|
||||
/// </summary>
|
||||
public static async Task<string> TranspileAsync(string js, string? relPathForLog = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? esbuildPath = GetEsbuildPath();
|
||||
if (string.IsNullOrEmpty(esbuildPath))
|
||||
{
|
||||
Trace.WriteLine($"esbuild binary not found, skipping transpile for {relPathForLog ?? "unknown"}");
|
||||
return js;
|
||||
}
|
||||
|
||||
string tempRoot = Path.Combine(Path.GetTempPath(), Constants.Esbuild.TempFolderName);
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
|
||||
string inputPath = Path.Combine(tempRoot, Guid.NewGuid().ToString("N") + Constants.FilePatterns.JsExtension);
|
||||
string outputPath = Path.Combine(tempRoot, Guid.NewGuid().ToString("N") + Constants.FilePatterns.JsExtension);
|
||||
|
||||
await File.WriteAllTextAsync(inputPath, js, Encoding.UTF8);
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = esbuildPath,
|
||||
Arguments = $"\"{inputPath}\" --outfile=\"{outputPath}\" --target={Constants.Esbuild.TargetEs2015}",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var proc = new Process { StartInfo = psi };
|
||||
proc.Start();
|
||||
|
||||
string stdout = await proc.StandardOutput.ReadToEndAsync();
|
||||
string stderr = await proc.StandardError.ReadToEndAsync();
|
||||
|
||||
proc.WaitForExit();
|
||||
|
||||
if (proc.ExitCode != 0 || !File.Exists(outputPath))
|
||||
{
|
||||
Trace.WriteLine($"esbuild failed for {relPathForLog ?? "unknown"} (exit {proc.ExitCode}): {stderr}");
|
||||
return js;
|
||||
}
|
||||
|
||||
string transpiled = await File.ReadAllTextAsync(outputPath, Encoding.UTF8);
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(inputPath);
|
||||
File.Delete(outputPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore cleanup errors
|
||||
}
|
||||
|
||||
Trace.WriteLine($"Transpiled {relPathForLog ?? "unknown"} via esbuild");
|
||||
return transpiled;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"esbuild transpile error for {relPathForLog ?? "unknown"}: {ex}");
|
||||
return js;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
212
Jellyfin2Samsung-CrossOS/Helpers/Core/FileHelper.cs
Normal file
212
Jellyfin2Samsung-CrossOS/Helpers/Core/FileHelper.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using Avalonia.Platform.Storage;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public class FileHelper
|
||||
{
|
||||
private static readonly string[] wgtItem = ["*.wgt"];
|
||||
private static readonly string[] tpkItem = ["*.tpk"];
|
||||
private static readonly string[] allItem = ["*.wgt", "*.tpk"];
|
||||
|
||||
|
||||
public async Task<string?> BrowseWgtFilesAsync(IStorageProvider storageProvider)
|
||||
{
|
||||
var fileTypes = new List<FilePickerFileType>
|
||||
{
|
||||
new("WGT Files")
|
||||
{
|
||||
Patterns = wgtItem
|
||||
},
|
||||
new("TPK Files")
|
||||
{
|
||||
Patterns = tpkItem
|
||||
},
|
||||
new("All Supported Files")
|
||||
{
|
||||
Patterns = allItem
|
||||
}
|
||||
};
|
||||
|
||||
var options = new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Select WGT/TPK File",
|
||||
FileTypeFilter = fileTypes,
|
||||
AllowMultiple = true
|
||||
};
|
||||
|
||||
var files = await storageProvider.OpenFilePickerAsync(options);
|
||||
|
||||
if (files?.Any() == true)
|
||||
return string.Join(";", files.Select(f => f.Path.LocalPath));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<ExtensionEntry> ParseExtensions(string output)
|
||||
{
|
||||
var extensions = new List<ExtensionEntry>();
|
||||
|
||||
foreach (Match match in RegexPatterns.Extension.ExtensionEntry.Matches(output))
|
||||
{
|
||||
extensions.Add(new ExtensionEntry
|
||||
{
|
||||
Index = int.Parse(match.Groups[1].Value),
|
||||
Name = match.Groups[2].Value.Trim(),
|
||||
Activated = bool.Parse(match.Groups[3].Value)
|
||||
});
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
public static async Task<string?> ReadWgtPackageId(string wgtPath)
|
||||
{
|
||||
if (!File.Exists(wgtPath))
|
||||
return null;
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (var originalStream = File.OpenRead(wgtPath))
|
||||
await originalStream.CopyToAsync(memoryStream);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read, true);
|
||||
var configEntry = archive.GetEntry("config.xml");
|
||||
if (configEntry == null)
|
||||
return null;
|
||||
|
||||
string configContent;
|
||||
using (var reader = new StreamReader(configEntry.Open(), Encoding.UTF8))
|
||||
configContent = await reader.ReadToEndAsync();
|
||||
|
||||
var match = RegexPatterns.WgtConfig.TizenApplicationId.Match(configContent);
|
||||
return match.Success ? match.Groups["pkg"].Value : null;
|
||||
}
|
||||
public static async Task<string?> ReadExtractedWgtPackageId(string workspaceRoot)
|
||||
{
|
||||
var configPath = Path.Combine(workspaceRoot, "config.xml");
|
||||
if (!File.Exists(configPath))
|
||||
return null;
|
||||
|
||||
var configContent = await File.ReadAllTextAsync(configPath, Encoding.UTF8);
|
||||
var match = RegexPatterns.WgtConfig.TizenApplicationId.Match(configContent);
|
||||
return match.Success ? match.Groups["pkg"].Value : null;
|
||||
}
|
||||
public static async Task<bool> ModifyWgtPackageId(string wgtPath)
|
||||
{
|
||||
if (!File.Exists(wgtPath))
|
||||
return false;
|
||||
|
||||
var oldPkg = await ReadWgtPackageId(wgtPath);
|
||||
if (string.IsNullOrEmpty(oldPkg))
|
||||
return false;
|
||||
|
||||
var newPkg = GenerateRandomString(oldPkg.Length);
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (var originalStream = File.OpenRead(wgtPath))
|
||||
await originalStream.CopyToAsync(memoryStream);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Update, true))
|
||||
{
|
||||
var configEntry = archive.GetEntry("config.xml");
|
||||
if (configEntry == null)
|
||||
return false;
|
||||
|
||||
string configContent;
|
||||
using (var reader = new StreamReader(configEntry.Open(), Encoding.UTF8))
|
||||
configContent = await reader.ReadToEndAsync();
|
||||
|
||||
// Replace old package ID with the new one
|
||||
var pattern = RegexPatterns.WgtConfig.CreatePackageIdReplacePattern(oldPkg);
|
||||
var regex = new Regex(pattern, RegexOptions.Multiline);
|
||||
|
||||
var newConfig = regex.Replace(configContent, m =>
|
||||
m.Value.Replace(oldPkg, newPkg)
|
||||
);
|
||||
|
||||
// Replace entry inside ZIP
|
||||
configEntry.Delete();
|
||||
var newEntry = archive.CreateEntry("config.xml");
|
||||
|
||||
using (var writer = new StreamWriter(newEntry.Open(), Encoding.UTF8))
|
||||
await writer.WriteAsync(newConfig);
|
||||
}
|
||||
|
||||
await File.WriteAllBytesAsync(wgtPath, memoryStream.ToArray());
|
||||
return true;
|
||||
}
|
||||
private static string GenerateRandomString(int length)
|
||||
{
|
||||
var sb = new StringBuilder(length);
|
||||
for (int i = 0; i < length; i++)
|
||||
sb.Append(Constants.CharacterSets.AlphaNumeric[Random.Shared.Next(Constants.CharacterSets.AlphaNumeric.Length)]);
|
||||
return sb.ToString();
|
||||
}
|
||||
public static async Task<string?> ReadWgtApplicationId(string wgtPath)
|
||||
{
|
||||
if (!File.Exists(wgtPath))
|
||||
return null;
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (var originalStream = File.OpenRead(wgtPath))
|
||||
await originalStream.CopyToAsync(memoryStream);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read, true);
|
||||
var configEntry = archive.GetEntry("config.xml");
|
||||
if (configEntry == null)
|
||||
return null;
|
||||
|
||||
string configContent;
|
||||
using (var reader = new StreamReader(configEntry.Open(), Encoding.UTF8))
|
||||
configContent = await reader.ReadToEndAsync();
|
||||
|
||||
// Prefer using a RegexPatterns entry if you want; otherwise this is safe and specific:
|
||||
var match = Regex.Match(
|
||||
configContent,
|
||||
@"<tizen:application\b[^>]*\bid\s*=\s*""(?<id>[^""]+)""",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
return match.Success ? match.Groups["id"].Value : null;
|
||||
}
|
||||
public static async Task<string?> ReadWgtRequiredVersion(string wgtPath)
|
||||
{
|
||||
if (!File.Exists(wgtPath))
|
||||
return null;
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (var originalStream = File.OpenRead(wgtPath))
|
||||
await originalStream.CopyToAsync(memoryStream);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read, true);
|
||||
var configEntry = archive.GetEntry("config.xml");
|
||||
if (configEntry == null)
|
||||
return null;
|
||||
|
||||
string configContent;
|
||||
using (var reader = new StreamReader(configEntry.Open(), Encoding.UTF8))
|
||||
configContent = await reader.ReadToEndAsync();
|
||||
|
||||
var match = Regex.Match(
|
||||
configContent,
|
||||
@"<tizen:application\b[^>]*\brequired_version\s*=\s*""(?<version>[^""]+)""",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
return match.Success ? match.Groups["version"].Value : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Jellyfin2Samsung-CrossOS/Helpers/Core/GitHubAuthHandler.cs
Normal file
111
Jellyfin2Samsung-CrossOS/Helpers/Core/GitHubAuthHandler.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public class GitHubAuthHandler : DelegatingHandler
|
||||
{
|
||||
private readonly string? _token;
|
||||
|
||||
public GitHubAuthHandler(string? token)
|
||||
: base(new HttpClientHandler())
|
||||
{
|
||||
_token = token;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_token) && IsGitHubRequest(request.RequestUri))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
||||
}
|
||||
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
// Token is expired or revoked — retry unauthenticated for public endpoints
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized &&
|
||||
request.Headers.Authorization != null)
|
||||
{
|
||||
Trace.TraceWarning("[GitHubAuth] Token rejected (401) — retrying without authorization");
|
||||
var retry = new HttpRequestMessage(request.Method, request.RequestUri);
|
||||
foreach (var header in request.Headers)
|
||||
{
|
||||
if (!string.Equals(header.Key, "Authorization", StringComparison.OrdinalIgnoreCase))
|
||||
retry.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
response = await base.SendAsync(retry, cancellationToken);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static bool IsGitHubRequest(Uri? uri)
|
||||
{
|
||||
if (uri == null) return false;
|
||||
var host = uri.Host;
|
||||
return host.Equals("api.github.com", StringComparison.OrdinalIgnoreCase)
|
||||
|| host.Equals("raw.githubusercontent.com", StringComparison.OrdinalIgnoreCase)
|
||||
|| host.Equals("github.com", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string? ResolveToken(AppSettings settings)
|
||||
{
|
||||
// 1. Explicit setting
|
||||
if (!string.IsNullOrWhiteSpace(settings.GitHubToken))
|
||||
{
|
||||
Trace.TraceInformation("[GitHubAuth] Using token from app settings");
|
||||
return settings.GitHubToken.Trim();
|
||||
}
|
||||
|
||||
// 2. Environment variable
|
||||
var envToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
|
||||
if (!string.IsNullOrWhiteSpace(envToken))
|
||||
{
|
||||
Trace.TraceInformation("[GitHubAuth] Using token from GITHUB_TOKEN environment variable");
|
||||
return envToken.Trim();
|
||||
}
|
||||
|
||||
// 3. GitHub CLI (gh auth token)
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "gh",
|
||||
Arguments = "auth token",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process != null)
|
||||
{
|
||||
var output = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit(5000);
|
||||
|
||||
if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
Trace.TraceInformation("[GitHubAuth] Using token from GitHub CLI (gh auth token)");
|
||||
return output;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// gh CLI not installed or not authenticated — ignore
|
||||
}
|
||||
|
||||
// 4. No token available — unauthenticated requests
|
||||
Trace.TraceInformation("[GitHubAuth] No token found — using unauthenticated requests");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
Jellyfin2Samsung-CrossOS/Helpers/Core/HtmlUtils.cs
Normal file
92
Jellyfin2Samsung-CrossOS/Helpers/Core/HtmlUtils.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public static class HtmlUtils
|
||||
{
|
||||
public static string EnsureBaseHref(string html)
|
||||
{
|
||||
if (html.Contains("<base", System.StringComparison.OrdinalIgnoreCase))
|
||||
return RegexPatterns.Html.BaseTag.Replace(html, "<base href=\".\">");
|
||||
|
||||
return html.Replace("<head>", "<head><base href=\".\">");
|
||||
}
|
||||
public static string RewriteLocalPaths(string html)
|
||||
{
|
||||
return RegexPatterns.Html.LocalPaths.Replace(html, "$1=\"$2\"");
|
||||
}
|
||||
public static string CleanAndApplyCsp(string html)
|
||||
{
|
||||
html = RegexPatterns.Html.CspMeta.Replace(html, "");
|
||||
return html.Replace("</head>",
|
||||
"<meta http-equiv=\"Content-Security-Policy\" content=\"default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;\">\n</head>");
|
||||
}
|
||||
public static string EnsurePublicJsIsLast(string html)
|
||||
{
|
||||
const string tag = "<script src=\"plugin_cache/public.js\"></script>";
|
||||
if (!html.Contains(tag)) return html;
|
||||
|
||||
html = html.Replace(tag, "");
|
||||
return html.Replace("</body>", tag + "\n</body>");
|
||||
}
|
||||
public static string EscapeJsString(string html)
|
||||
{
|
||||
if (string.IsNullOrEmpty(html)) return "";
|
||||
return html
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("'", "\\'")
|
||||
.Replace("\"", "\\\"")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace("\r", "\\r");
|
||||
}
|
||||
public static string RemoveMarkdownTable(string html)
|
||||
{
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return html;
|
||||
|
||||
var tablePattern = @"(\|[^\n]+\|\s*\n)+";
|
||||
|
||||
return Regex.Replace(html, tablePattern, string.Empty, RegexOptions.Multiline);
|
||||
}
|
||||
public static string StripHtml(string html)
|
||||
{
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return string.Empty;
|
||||
|
||||
// Simple HTML stripping - replace common tags
|
||||
var text = html
|
||||
.Replace("<br>", "\n")
|
||||
.Replace("<br/>", "\n")
|
||||
.Replace("<br />", "\n")
|
||||
.Replace("</p>", "\n")
|
||||
.Replace("</li>", "\n")
|
||||
.Replace("<li>", "• ");
|
||||
|
||||
// Remove all remaining HTML tags
|
||||
while (text.Contains('<') && text.Contains('>'))
|
||||
{
|
||||
var start = text.IndexOf('<');
|
||||
var end = text.IndexOf('>', start);
|
||||
if (end > start)
|
||||
text = text.Remove(start, end - start + 1);
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Decode common HTML entities
|
||||
text = text
|
||||
.Replace(" ", " ")
|
||||
.Replace("&", "&")
|
||||
.Replace("<", "<")
|
||||
.Replace(">", ">")
|
||||
.Replace(""", "\"")
|
||||
.Replace("'", "'");
|
||||
|
||||
// Clean up whitespace
|
||||
var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
return string.Join("\n", lines.Select(l => l.Trim()).Where(l => !string.IsNullOrEmpty(l)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides centralized, pre-configured JsonSerializerOptions for consistent JSON serialization
|
||||
/// throughout the application. This eliminates inconsistent settings and improves performance
|
||||
/// by reusing options instances.
|
||||
/// </summary>
|
||||
public static class JsonSerializerOptionsProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options for general JSON serialization/deserialization.
|
||||
/// - Case-insensitive property matching
|
||||
/// - Camel case naming policy
|
||||
/// - Ignores null values when writing
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Default { get; } = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Options for pretty-printed JSON output (e.g., config files, logs).
|
||||
/// Same as Default but with indentation enabled.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Indented { get; } = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Options optimized for GitHub API responses.
|
||||
/// Uses snake_case naming policy to match GitHub's JSON format.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions GitHub { get; } = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Options for strict JSON parsing (no case-insensitive matching).
|
||||
/// Use when exact property name matching is required.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Strict { get; } = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Options for web/API responses with standard web conventions.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Web { get; } = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,30 @@
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public class PackageHelper
|
||||
public class PackageHelper(
|
||||
ITizenInstallerService tizenInstaller,
|
||||
IDialogService dialogService,
|
||||
INetworkService networkService)
|
||||
{
|
||||
private readonly ITizenInstallerService _tizenInstaller;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly INetworkService _networkService;
|
||||
private readonly ITizenInstallerService _tizenInstaller = tizenInstaller;
|
||||
private readonly IDialogService _dialogService = dialogService;
|
||||
private readonly INetworkService _networkService = networkService;
|
||||
|
||||
public PackageHelper(
|
||||
ITizenInstallerService tizenInstaller,
|
||||
IDialogService dialogService,
|
||||
INetworkService networkService)
|
||||
{
|
||||
_tizenInstaller = tizenInstaller;
|
||||
_dialogService = dialogService;
|
||||
_networkService = networkService;
|
||||
}
|
||||
|
||||
public async Task<string?> DownloadReleaseAsync(GitHubRelease release, Asset selectedAsset, ProgressCallback? progress = null)
|
||||
public async Task<string?> DownloadReleaseAsync(GitHubRelease release, Asset? selectedAsset, ProgressCallback? progress = null)
|
||||
{
|
||||
if (release?.PrimaryDownloadUrl == null) return null;
|
||||
if (selectedAsset?.DownloadUrl == null) return null;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -38,18 +35,34 @@ namespace Jellyfin2Samsung.Helpers
|
||||
catch (Exception ex)
|
||||
{
|
||||
progress?.Invoke("DownloadFailed".Localized());
|
||||
await _dialogService.ShowErrorAsync($"{"DownloadFailed".Localized()} {ex.Message}");
|
||||
await _dialogService.ShowErrorAsync($"{"DownloadFailed".Localized()} {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> InstallPackageAsync(string packagePath, NetworkDevice selectedDevice, ProgressCallback? progress = null)
|
||||
public async Task<bool> InstallPackageAsync(string? packagePath, NetworkDevice selectedDevice, CancellationToken cancellationToken, ProgressCallback? progress = null, Action? onSamsungLoginStarted = null)
|
||||
{
|
||||
if(selectedDevice.DeveloperIP == null) return false;
|
||||
|
||||
var localIps = _networkService.GetRelevantLocalIPs()
|
||||
.Select(ip => ip.ToString())
|
||||
.ToList();
|
||||
|
||||
bool ipMismatch = !localIps.Contains(selectedDevice.DeveloperIP);
|
||||
bool ipMismatch = !localIps.Contains(selectedDevice.DeveloperIP) && !string.IsNullOrEmpty(selectedDevice.DeveloperIP);
|
||||
|
||||
if (!string.IsNullOrEmpty(AppSettings.Default.LocalIp)
|
||||
&& !string.IsNullOrEmpty(selectedDevice.DeveloperIP)
|
||||
&& _networkService.IsDifferentSubnet(AppSettings.Default.LocalIp, selectedDevice.DeveloperIP))
|
||||
{
|
||||
bool continueExecution =
|
||||
await _dialogService.ShowConfirmationAsync(
|
||||
"Subnet Mismatch",
|
||||
"subnetMismatch".Localized(),
|
||||
"keyContinue".Localized(),
|
||||
"keyStop".Localized());
|
||||
|
||||
if (!continueExecution)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(packagePath) || !File.Exists(packagePath))
|
||||
{
|
||||
@@ -84,7 +97,26 @@ namespace Jellyfin2Samsung.Helpers
|
||||
|
||||
if (ipMismatch)
|
||||
{
|
||||
bool continueExecution = await _dialogService.ShowConfirmationAsync("IP Mismatch","DeveloperIPMismatch".Localized(), "keyContinue".Localized(), "keyStop".Localized());
|
||||
bool isReversedIp = localIps
|
||||
.Select(ip => _networkService.InvertIPAddress(ip))
|
||||
.Contains(selectedDevice.DeveloperIP);
|
||||
|
||||
if (isReversedIp)
|
||||
{
|
||||
bool continueExecution = await _dialogService.ShowConfirmationAsync(
|
||||
"IP Reversed",
|
||||
"DeveloperIPReversed".Localized(),
|
||||
"keyContinue".Localized(),
|
||||
"keyStop".Localized());
|
||||
if (!continueExecution)
|
||||
return false;
|
||||
ipMismatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ipMismatch)
|
||||
{
|
||||
bool continueExecution = await _dialogService.ShowConfirmationAsync("IP Mismatch", "DeveloperIPMismatch".Localized(), "keyContinue".Localized(), "keyStop".Localized());
|
||||
if (!continueExecution)
|
||||
return false;
|
||||
}
|
||||
@@ -94,17 +126,27 @@ namespace Jellyfin2Samsung.Helpers
|
||||
var result = await _tizenInstaller.InstallPackageAsync(
|
||||
packagePath,
|
||||
selectedDevice.IpAddress,
|
||||
progress);
|
||||
cancellationToken,
|
||||
progress,
|
||||
onSamsungLoginStarted);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
var win = App.Services.GetRequiredService<InstallationCompleteWindow>();
|
||||
|
||||
if (Avalonia.Application.Current?.ApplicationLifetime is
|
||||
Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
|
||||
var prettyName = GetPrettyPackageName(packagePath);
|
||||
|
||||
if (win.DataContext is InstallationCompleteViewModel vm)
|
||||
{
|
||||
win.ShowDialog(desktop.MainWindow);
|
||||
vm.InstalledPackageName = prettyName;
|
||||
}
|
||||
|
||||
if (Avalonia.Application.Current?.ApplicationLifetime is
|
||||
IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
await win.ShowDialog(desktop.MainWindow);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
@@ -117,15 +159,14 @@ namespace Jellyfin2Samsung.Helpers
|
||||
catch (Exception ex)
|
||||
{
|
||||
progress?.Invoke("InstallationFailed".Localized());
|
||||
await _dialogService.ShowErrorAsync($"{"InstallationFailed".Localized()}: {ex.Message}");
|
||||
await _dialogService.ShowErrorAsync($"{"InstallationFailed".Localized()}: {ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public async Task<bool> InstallCustomPackagesAsync(
|
||||
string[] packagePaths,
|
||||
NetworkDevice device,
|
||||
Action<string> onProgress)
|
||||
public async Task<bool> InstallCustomPackagesAsync(string[] packagePaths, NetworkDevice? device, CancellationToken cancellationToken, Action<string> onProgress, Action? onSamsungLoginStarted = null)
|
||||
{
|
||||
if (device == null) return false;
|
||||
|
||||
onProgress("UsingCustomWGT".Localized());
|
||||
|
||||
var allSuccessful = true;
|
||||
@@ -140,7 +181,7 @@ namespace Jellyfin2Samsung.Helpers
|
||||
break;
|
||||
}
|
||||
|
||||
var success = await InstallPackageAsync(filePath, device);
|
||||
var success = await InstallPackageAsync(filePath, device, cancellationToken);
|
||||
if (!success)
|
||||
{
|
||||
allSuccessful = false;
|
||||
@@ -161,5 +202,15 @@ namespace Jellyfin2Samsung.Helpers
|
||||
}
|
||||
catch { /* Ignore cleanup errors */ }
|
||||
}
|
||||
private static string GetPrettyPackageName(string packagePath)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(packagePath);
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return string.Empty;
|
||||
|
||||
return char.ToUpper(name[0]) + name.Substring(1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
46
Jellyfin2Samsung-CrossOS/Helpers/Core/PackageWorkspace.cs
Normal file
46
Jellyfin2Samsung-CrossOS/Helpers/Core/PackageWorkspace.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public sealed class PackageWorkspace : IDisposable
|
||||
{
|
||||
public string Root { get; }
|
||||
private readonly string _originalPackage;
|
||||
private readonly string _tempPackage;
|
||||
|
||||
private PackageWorkspace(string root, string original, string temp)
|
||||
{
|
||||
Root = root;
|
||||
_originalPackage = original;
|
||||
_tempPackage = temp;
|
||||
}
|
||||
|
||||
public static PackageWorkspace Extract(string packagePath)
|
||||
{
|
||||
var baseDir = Path.GetDirectoryName(packagePath)!;
|
||||
var tempDir = Path.Combine(baseDir, $"JellyTemp_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
ZipFile.ExtractToDirectory(packagePath, tempDir);
|
||||
return new PackageWorkspace(tempDir, packagePath, packagePath + ".tmp");
|
||||
}
|
||||
|
||||
public void Repack()
|
||||
{
|
||||
if (File.Exists(_tempPackage))
|
||||
File.Delete(_tempPackage);
|
||||
|
||||
ZipFile.CreateFromDirectory(Root, _tempPackage);
|
||||
File.Delete(_originalPackage);
|
||||
File.Move(_tempPackage, _originalPackage);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { if (Directory.Exists(Root)) Directory.Delete(Root, true); } catch { }
|
||||
try { if (File.Exists(_tempPackage)) File.Delete(_tempPackage); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
195
Jellyfin2Samsung-CrossOS/Helpers/Core/PlatformService.cs
Normal file
195
Jellyfin2Samsung-CrossOS/Helpers/Core/PlatformService.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides centralized platform detection and platform-specific operations.
|
||||
/// Eliminates duplicate OperatingSystem.* and RuntimeInformation.* checks throughout the codebase.
|
||||
/// </summary>
|
||||
public static class PlatformService
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the current operating system platform.
|
||||
/// </summary>
|
||||
public enum Platform
|
||||
{
|
||||
Windows,
|
||||
Linux,
|
||||
MacOS,
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current platform.
|
||||
/// </summary>
|
||||
public static Platform CurrentPlatform
|
||||
{
|
||||
get
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
return Platform.Windows;
|
||||
if (OperatingSystem.IsLinux())
|
||||
return Platform.Linux;
|
||||
if (OperatingSystem.IsMacOS())
|
||||
return Platform.MacOS;
|
||||
return Platform.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current platform is Windows.
|
||||
/// </summary>
|
||||
public static bool IsWindows => OperatingSystem.IsWindows();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current platform is Linux.
|
||||
/// </summary>
|
||||
public static bool IsLinux => OperatingSystem.IsLinux();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current platform is macOS.
|
||||
/// </summary>
|
||||
public static bool IsMacOS => OperatingSystem.IsMacOS();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current platform is Unix-like (Linux or macOS).
|
||||
/// </summary>
|
||||
public static bool IsUnixLike => IsLinux || IsMacOS;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the appropriate TizenSdb search pattern for the current platform.
|
||||
/// </summary>
|
||||
/// <returns>The file search pattern for TizenSdb binary.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when the platform is not supported.</exception>
|
||||
public static string GetTizenSdbSearchPattern()
|
||||
{
|
||||
return CurrentPlatform switch
|
||||
{
|
||||
Platform.Windows => Constants.PlatformBinaries.TizenSdbWindowsPattern,
|
||||
Platform.Linux => Constants.PlatformBinaries.TizenSdbLinuxPattern,
|
||||
Platform.MacOS => Constants.PlatformBinaries.TizenSdbMacOsPattern,
|
||||
_ => throw new PlatformNotSupportedException("Unsupported operating system")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TizenSdb file name with version for the current platform.
|
||||
/// </summary>
|
||||
/// <param name="version">The version string to include in the file name.</param>
|
||||
/// <returns>The platform-specific file name.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when the platform is not supported.</exception>
|
||||
public static string GetTizenSdbFileName(string version)
|
||||
{
|
||||
return CurrentPlatform switch
|
||||
{
|
||||
Platform.Windows => $"TizenSdb_{version}{Constants.PlatformBinaries.WindowsExtension}",
|
||||
Platform.Linux => $"TizenSdb_{version}{Constants.PlatformBinaries.LinuxSuffix}",
|
||||
Platform.MacOS => $"TizenSdb_{version}{Constants.PlatformBinaries.MacOsSuffix}",
|
||||
_ => throw new PlatformNotSupportedException("Unsupported operating system")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the platform identifier for matching GitHub release assets.
|
||||
/// </summary>
|
||||
/// <returns>The platform identifier string used in asset file names.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when the platform is not supported.</exception>
|
||||
public static string GetAssetPlatformIdentifier()
|
||||
{
|
||||
return CurrentPlatform switch
|
||||
{
|
||||
Platform.Windows => "exe",
|
||||
Platform.Linux => "linux",
|
||||
Platform.MacOS => "macos",
|
||||
_ => throw new PlatformNotSupportedException("Unsupported operating system")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the esbuild binary path relative to the esbuild directory.
|
||||
/// </summary>
|
||||
/// <param name="esbuildBasePath">The base path to the esbuild directory.</param>
|
||||
/// <returns>The full path to the esbuild binary, or null if not supported.</returns>
|
||||
public static string? GetEsbuildPath(string esbuildBasePath)
|
||||
{
|
||||
string relativePath = CurrentPlatform switch
|
||||
{
|
||||
Platform.Windows => Path.Combine(
|
||||
Constants.PlatformBinaries.EsbuildWindows,
|
||||
Constants.PlatformBinaries.EsbuildExecutableWindows),
|
||||
Platform.Linux => Path.Combine(
|
||||
Constants.PlatformBinaries.EsbuildLinux,
|
||||
Constants.PlatformBinaries.EsbuildExecutable),
|
||||
Platform.MacOS => Path.Combine(
|
||||
Constants.PlatformBinaries.EsbuildMacOs,
|
||||
Constants.PlatformBinaries.EsbuildExecutable),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (relativePath == null)
|
||||
return null;
|
||||
|
||||
string fullPath = Path.Combine(esbuildBasePath, relativePath);
|
||||
return File.Exists(fullPath) ? fullPath : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ARP command arguments for the current platform.
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">The IP address to query.</param>
|
||||
/// <returns>The ARP command arguments.</returns>
|
||||
public static string GetArpArguments(string ipAddress)
|
||||
{
|
||||
return IsWindows ? $"-a {ipAddress}" : $"-n {ipAddress}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the appropriate X509KeyStorageFlags for the current platform.
|
||||
/// </summary>
|
||||
/// <returns>The platform-appropriate key storage flags.</returns>
|
||||
public static X509KeyStorageFlags GetX509KeyStorageFlags()
|
||||
{
|
||||
return IsWindows
|
||||
? X509KeyStorageFlags.EphemeralKeySet
|
||||
: X509KeyStorageFlags.PersistKeySet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets firewall configuration help text for the current platform.
|
||||
/// </summary>
|
||||
/// <param name="port">The port number to include in instructions.</param>
|
||||
/// <returns>Platform-specific firewall configuration instructions.</returns>
|
||||
public static string GetFirewallHelpText(int port)
|
||||
{
|
||||
return CurrentPlatform switch
|
||||
{
|
||||
Platform.Windows =>
|
||||
$"Windows:\n" +
|
||||
$" netstat -ano | findstr {port}\n\n" +
|
||||
$" New-NetFirewallRule -DisplayName \"Jellyfin2Samsung Logs\" \\\n" +
|
||||
$" -Direction Inbound -Protocol TCP -LocalPort {port} -Action Allow\n",
|
||||
|
||||
Platform.Linux =>
|
||||
$"Linux:\n sudo ufw allow {port}/tcp\n sudo ufw reload\n",
|
||||
|
||||
Platform.MacOS =>
|
||||
"macOS:\n" +
|
||||
" System Settings -> Network -> Firewall -> Options\n" +
|
||||
" Allow incoming connections for Jellyfin2Samsung\n",
|
||||
|
||||
_ => "Ensure your firewall allows inbound TCP connections.\n"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the file needs executable permissions set (Unix-like systems only).
|
||||
/// </summary>
|
||||
/// <returns>True if chmod is needed, false otherwise.</returns>
|
||||
public static bool RequiresExecutablePermissions()
|
||||
{
|
||||
return IsUnixLike;
|
||||
}
|
||||
}
|
||||
}
|
||||
141
Jellyfin2Samsung-CrossOS/Helpers/Core/ProcessHelper.cs
Normal file
141
Jellyfin2Samsung-CrossOS/Helpers/Core/ProcessHelper.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public class ProcessHelper
|
||||
{
|
||||
public static void KillSdbServers()
|
||||
{
|
||||
try
|
||||
{
|
||||
Process[] sdbProcesses = Process.GetProcessesByName("sdb");
|
||||
|
||||
if (sdbProcesses.Length == 0)
|
||||
return;
|
||||
|
||||
foreach (Process proc in sdbProcesses)
|
||||
{
|
||||
proc.Kill();
|
||||
proc.WaitForExit();
|
||||
Trace.WriteLine($"Killed SDB {proc.Id} - {proc.ProcessName}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Failed to stop SDB server: {ex}");
|
||||
}
|
||||
}
|
||||
string GetFirstArguments(string arguments)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(arguments))
|
||||
return string.Empty;
|
||||
|
||||
var matches = RegexPatterns.CommandLine.Arguments.Matches(arguments);
|
||||
|
||||
var firstTwo = matches.Cast<System.Text.RegularExpressions.Match>()
|
||||
.Take(1)
|
||||
.Select(m => m.Value.Trim('"'))
|
||||
.ToArray();
|
||||
|
||||
return string.Join(" ", firstTwo);
|
||||
}
|
||||
|
||||
public async Task<ProcessResult> RunCommandAsync(string fileName, string arguments, string? workingDirectory = null)
|
||||
{
|
||||
var result = new ProcessResult();
|
||||
|
||||
// Always run hidden (no console)
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = workingDirectory ?? ""
|
||||
};
|
||||
|
||||
// Build log file path (next to app .exe)
|
||||
string exeDir = AppContext.BaseDirectory;
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss-fff");
|
||||
string firstTwoArgs = GetFirstArguments(arguments);
|
||||
string sanitizedArguments = new(firstTwoArgs.Where(c => char.IsLetterOrDigit(c) || c == '_' || c == '-').ToArray());
|
||||
if (string.IsNullOrEmpty(sanitizedArguments))
|
||||
sanitizedArguments = "unknown";
|
||||
|
||||
string logFolder = Path.Combine(exeDir, "Logs");
|
||||
string logFilePath = Path.Combine(logFolder, $"process_{sanitizedArguments}_{timestamp}.log");
|
||||
|
||||
try
|
||||
{
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
|
||||
process.Start();
|
||||
|
||||
// Capture all output
|
||||
string stdOut = await process.StandardOutput.ReadToEndAsync();
|
||||
string stdErr = await process.StandardError.ReadToEndAsync();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
// Combine both
|
||||
var sb = new StringBuilder();
|
||||
if (!string.IsNullOrEmpty(stdOut))
|
||||
sb.AppendLine(stdOut.TrimEnd());
|
||||
if (!string.IsNullOrEmpty(stdErr))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("--- STDERR ---");
|
||||
sb.AppendLine(stdErr.TrimEnd());
|
||||
}
|
||||
|
||||
result.ExitCode = process.ExitCode;
|
||||
result.Output = sb.ToString();
|
||||
|
||||
// Always write everything to a log file
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(exeDir);
|
||||
await File.WriteAllTextAsync(logFilePath, result.Output);
|
||||
result.Output += $"\n[Log written to: {logFilePath}]";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Output += $"\n[Log write failed: {ex}]";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.ExitCode = -1;
|
||||
result.Output = $"[Process start failed: {ex}]";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task MakeExecutableAsync(string filePath)
|
||||
{
|
||||
if (PlatformService.RequiresExecutablePermissions())
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use chmod to make the file executable
|
||||
var output = await RunCommandAsync("chmod", $"+x \"{filePath}\"");
|
||||
Trace.WriteLine($"Set executable permissions on {filePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Error setting executable permissions: {ex}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
416
Jellyfin2Samsung-CrossOS/Helpers/Core/RegexPatterns.cs
Normal file
416
Jellyfin2Samsung-CrossOS/Helpers/Core/RegexPatterns.cs
Normal file
@@ -0,0 +1,416 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Centralized regex patterns used throughout the application.
|
||||
/// Pre-compiled patterns improve performance for frequently used expressions.
|
||||
/// </summary>
|
||||
public static partial class RegexPatterns
|
||||
{
|
||||
/// <summary>
|
||||
/// Patterns for version parsing and extraction.
|
||||
/// </summary>
|
||||
public static class Version
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract version from a file name (e.g., "TizenSdb_v1.0.0.exe" -> "v1.0.0").
|
||||
/// </summary>
|
||||
public const string FileNameVersionPattern = @"_([v]?\d+\.\d+\.\d+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for file name version extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex FileNameVersion = new(FileNameVersionPattern, RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for Tizen device capability parsing.
|
||||
/// </summary>
|
||||
public static class TizenCapability
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract platform version from Tizen capability output.
|
||||
/// </summary>
|
||||
public const string PlatformVersionPattern = @"platform_version:\s*([\d.]+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract SDK tool path from Tizen capability output.
|
||||
/// </summary>
|
||||
public const string SdkToolPathPattern = @"sdk_toolpath:\s*([^\r\n]+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to detect app uninstall test failure in Tizen diagnose output.
|
||||
/// </summary>
|
||||
public const string AppUninstallFailedPattern = @"Testing '0 vd_appuninstall test':\s*FAILED";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for platform version extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex PlatformVersion = new(PlatformVersionPattern, RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for SDK tool path extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex SdkToolPath = new(SdkToolPathPattern, RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for app uninstall test failure detection.
|
||||
/// </summary>
|
||||
public static readonly Regex AppUninstallFailed = new(
|
||||
AppUninstallFailedPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for Tizen app information parsing.
|
||||
/// </summary>
|
||||
public static class TizenApp
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern template to extract app block by title from Tizen apps output.
|
||||
/// Use string.Format or interpolation with the app title.
|
||||
/// </summary>
|
||||
public const string AppBlockByTitleTemplate = @"(^\s*-+app_title\s*=\s*{0}.*?)(?=^\s*-+app_title|\Z)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract Tizen app ID from an app block.
|
||||
/// </summary>
|
||||
public const string AppTizenIdPattern = @"app_tizen_id\s*=\s*([A-Za-z0-9._]+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract Tizen app ID with delimiter.
|
||||
/// </summary>
|
||||
public const string AppTizenIdWithDelimiterPattern = @"app_tizen_id\s*=\s*([A-Za-z0-9._-]+?)(?=-{3,})";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for app Tizen ID extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex AppTizenId = new(
|
||||
AppTizenIdPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for app Tizen ID with delimiter extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex AppTizenIdWithDelimiter = new(
|
||||
AppTizenIdWithDelimiterPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a regex to find an app block by title.
|
||||
/// </summary>
|
||||
/// <param name="appTitle">The app title to search for.</param>
|
||||
/// <returns>A regex instance for matching the app block.</returns>
|
||||
public static Regex CreateAppBlockByTitleRegex(string appTitle)
|
||||
{
|
||||
var escapedTitle = Regex.Escape(appTitle);
|
||||
var pattern = string.Format(AppBlockByTitleTemplate, escapedTitle);
|
||||
return new Regex(
|
||||
pattern,
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Multiline);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for WGT package config.xml parsing.
|
||||
/// </summary>
|
||||
public static class WgtConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract package ID from Tizen application element in config.xml.
|
||||
/// </summary>
|
||||
public const string TizenApplicationIdPattern =
|
||||
@"<tizen:application\s+id=""(?<pkg>[A-Za-z0-9]+)\.Jellyfin""\s+package=""\k<pkg>""";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for Tizen application ID extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex TizenApplicationId = new(
|
||||
TizenApplicationIdPattern,
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pattern to match and replace a specific package ID in config.xml.
|
||||
/// </summary>
|
||||
/// <param name="packageId">The package ID to match.</param>
|
||||
/// <returns>The pattern string.</returns>
|
||||
public static string CreatePackageIdReplacePattern(string packageId)
|
||||
{
|
||||
return $@"<tizen:application\s+id=""{packageId}\.Jellyfin""\s+package=""{packageId}""";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for HTML parsing and manipulation.
|
||||
/// </summary>
|
||||
public static class Html
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract href from link elements.
|
||||
/// </summary>
|
||||
public const string LinkHrefPattern = @"<link[^>]+href=[""']([^""']+)[""'][^>]*>";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract src from script elements.
|
||||
/// </summary>
|
||||
public const string ScriptSrcPattern = @"<script[^>]+src=[""']([^""']+)[""'][^>]*>[\s\S]*?<\/script>";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to match base tag elements for replacement.
|
||||
/// </summary>
|
||||
public const string BaseTagPattern = @"<base[^>]+>";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to rewrite local paths (src/href with /web/ prefix).
|
||||
/// </summary>
|
||||
public const string LocalPathsPattern = @"(src|href)=""[^""]*/web/([^""]+)""";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to match Content-Security-Policy meta tags.
|
||||
/// </summary>
|
||||
public const string CspMetaPattern = @"<meta[^>]*Content-Security-Policy[^>]*>";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for link href extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex LinkHref = new(
|
||||
LinkHrefPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for script src extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex ScriptSrc = new(
|
||||
ScriptSrcPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for base tag matching.
|
||||
/// </summary>
|
||||
public static readonly Regex BaseTag = new(
|
||||
BaseTagPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for local paths rewriting.
|
||||
/// </summary>
|
||||
public static readonly Regex LocalPaths = new(
|
||||
LocalPathsPattern,
|
||||
RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for CSP meta tag matching.
|
||||
/// </summary>
|
||||
public static readonly Regex CspMeta = new(
|
||||
CspMetaPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for network and MAC address parsing.
|
||||
/// </summary>
|
||||
public static class Network
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract MAC address from ARP output.
|
||||
/// </summary>
|
||||
public const string MacAddressPattern = @"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for MAC address extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex MacAddress = new(MacAddressPattern, RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for command line argument parsing.
|
||||
/// </summary>
|
||||
public static class CommandLine
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to parse command line arguments (handles quoted strings).
|
||||
/// </summary>
|
||||
public const string ArgumentsPattern = @"[\""].+?[\""]|[^ ]+";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for argument parsing.
|
||||
/// </summary>
|
||||
public static readonly Regex Arguments = new(ArgumentsPattern, RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for build info markdown parsing.
|
||||
/// </summary>
|
||||
public static class BuildInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract versions table from markdown.
|
||||
/// </summary>
|
||||
public const string VersionsTablePattern = @"## Versions\s*\n(?<table>(\|[^\n]+\n)+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract applications table from markdown.
|
||||
/// </summary>
|
||||
public const string ApplicationsTablePattern =
|
||||
@"\|\s*🧩 Application\s*\|\s*📝 Description\s*\|\s*🔗 Repository\s*\|\s*\n(?<table>(\|[^\n]+\n)+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract table rows with 2 columns.
|
||||
/// </summary>
|
||||
public const string TableRow2ColumnsPattern = @"^\|([^|]+)\|([^|]+)\|";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract table rows with 3 columns.
|
||||
/// </summary>
|
||||
public const string TableRow3ColumnsPattern = @"^\|([^|]+)\|([^|]+)\|([^|]+)\|";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to remove markdown bold formatting.
|
||||
/// </summary>
|
||||
public const string MarkdownBoldPattern = @"\*\*(.*?)\*\*";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to remove emoji characters.
|
||||
/// </summary>
|
||||
public const string EmojiRangePattern = @"[\u2600-\u27BF]";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for versions table extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex VersionsTable = new(
|
||||
VersionsTablePattern,
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for applications table extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex ApplicationsTable = new(
|
||||
ApplicationsTablePattern,
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for 2-column table rows.
|
||||
/// </summary>
|
||||
public static readonly Regex TableRow2Columns = new(
|
||||
TableRow2ColumnsPattern,
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for 3-column table rows.
|
||||
/// </summary>
|
||||
public static readonly Regex TableRow3Columns = new(
|
||||
TableRow3ColumnsPattern,
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for markdown bold removal.
|
||||
/// </summary>
|
||||
public static readonly Regex MarkdownBold = new(MarkdownBoldPattern, RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for emoji removal.
|
||||
/// </summary>
|
||||
public static readonly Regex EmojiRange = new(EmojiRangePattern, RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for plugin configuration parsing.
|
||||
/// </summary>
|
||||
public static class PluginConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract defaultSkin value from JSON config.
|
||||
/// </summary>
|
||||
public const string DefaultSkinPattern = @"""defaultSkin""\s*:\s*""([^""]+)""";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for defaultSkin extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex DefaultSkin = new(
|
||||
DefaultSkinPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for extension parsing in Tizen package manager output.
|
||||
/// </summary>
|
||||
public static class Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to parse extension entries from package manager output.
|
||||
/// </summary>
|
||||
public const string ExtensionEntryPattern =
|
||||
@"Index\s*:\s*(\d+)\s+Name\s*:\s*(.*?)\s+Repository\s*:\s*.*?\s+Id\s*:\s*.*?\s+Vendor\s*:\s*.*?\s+Description\s*:\s*.*?\s+Default\s*:\s*.*?\s+Activate\s*:\s*(true|false)";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for extension entry parsing.
|
||||
/// </summary>
|
||||
public static readonly Regex ExtensionEntry = new(
|
||||
ExtensionEntryPattern,
|
||||
RegexOptions.Compiled | RegexOptions.Singleline);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for KefinTweaks plugin patching.
|
||||
/// </summary>
|
||||
public static class KefinTweaks
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to detect KefinTweaks loader script reference in public.js.
|
||||
/// </summary>
|
||||
public const string LoaderPattern =
|
||||
@"script\.src\s*=\s*['""]https:\/\/cdn\.jsdelivr\.net\/gh\/ranaldsgift\/KefinTweaks[^'""]+['""]";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to match and replace kefinTweaksRoot configuration.
|
||||
/// </summary>
|
||||
public const string TweaksRootPattern =
|
||||
@"""kefinTweaksRoot""\s*:\s*""https:\/\/cdn\.jsdelivr\.net\/gh\/ranaldsgift\/KefinTweaks@latest\/""";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract script entries from injector.js.
|
||||
/// </summary>
|
||||
public const string ScriptEntryPattern = @"script\s*:\s*""([^""]+)""";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for loader detection.
|
||||
/// </summary>
|
||||
public static readonly Regex Loader = new(
|
||||
LoaderPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for tweaks root replacement.
|
||||
/// </summary>
|
||||
public static readonly Regex TweaksRoot = new(
|
||||
TweaksRootPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for script entry extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex ScriptEntry = new(
|
||||
ScriptEntryPattern,
|
||||
RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for plugin name cleaning and normalization.
|
||||
/// </summary>
|
||||
public static class PluginName
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to remove non-alphanumeric characters from plugin names.
|
||||
/// </summary>
|
||||
public const string NonAlphanumericPattern = @"[^a-z0-9]";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for plugin name cleaning.
|
||||
/// </summary>
|
||||
public static readonly Regex NonAlphanumeric = new(
|
||||
NonAlphanumericPattern,
|
||||
RegexOptions.Compiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
Jellyfin2Samsung-CrossOS/Helpers/Core/UrlHelper.cs
Normal file
94
Jellyfin2Samsung-CrossOS/Helpers/Core/UrlHelper.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides URL manipulation utilities.
|
||||
/// Centralizes URL normalization to eliminate duplicate TrimEnd('/') calls across the codebase.
|
||||
/// </summary>
|
||||
public static class UrlHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes a server URL by removing trailing slashes.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to normalize.</param>
|
||||
/// <returns>The normalized URL without trailing slashes, or empty string if null.</returns>
|
||||
public static string NormalizeServerUrl(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return string.Empty;
|
||||
|
||||
return url.TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines a base URL with a path segment, ensuring proper slash handling.
|
||||
/// </summary>
|
||||
/// <param name="baseUrl">The base URL (trailing slash will be removed).</param>
|
||||
/// <param name="path">The path to append (leading slash will be added if missing).</param>
|
||||
/// <returns>The combined URL.</returns>
|
||||
public static string CombineUrl(string? baseUrl, string? path)
|
||||
{
|
||||
var normalizedBase = NormalizeServerUrl(baseUrl);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return normalizedBase;
|
||||
|
||||
var normalizedPath = path.TrimStart('/');
|
||||
|
||||
return string.IsNullOrEmpty(normalizedBase)
|
||||
? normalizedPath
|
||||
: $"{normalizedBase}/{normalizedPath}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an absolute URI from a base server URL and a relative or absolute path.
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">The base server URL.</param>
|
||||
/// <param name="relativeOrAbsolutePath">A relative path or absolute URL.</param>
|
||||
/// <returns>The absolute URI.</returns>
|
||||
public static Uri GetAbsoluteUri(string serverUrl, string relativeOrAbsolutePath)
|
||||
{
|
||||
if (Uri.IsWellFormedUriString(relativeOrAbsolutePath, UriKind.Absolute))
|
||||
return new Uri(relativeOrAbsolutePath);
|
||||
|
||||
var baseUri = new Uri(NormalizeServerUrl(serverUrl) + "/");
|
||||
return new Uri(baseUri, relativeOrAbsolutePath.TrimStart('/'));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates if a string is a valid HTTP or HTTPS URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to validate.</param>
|
||||
/// <returns>True if the URL is valid, false otherwise.</returns>
|
||||
public static bool IsValidHttpUrl(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return false;
|
||||
|
||||
return Uri.TryCreate(url, UriKind.Absolute, out var uriResult)
|
||||
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the file name from a URL path.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to extract the file name from.</param>
|
||||
/// <returns>The file name, or empty string if not found.</returns>
|
||||
public static string GetFileNameFromUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
return System.IO.Path.GetFileName(uri.LocalPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
using FluentAvalonia.Core;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Org.BouncyCastle.Utilities.Net;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class DeviceHelper
|
||||
{
|
||||
private readonly INetworkService _networkService;
|
||||
private readonly ITizenInstallerService _installerService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public DeviceHelper(
|
||||
INetworkService networkService,
|
||||
ITizenInstallerService installerService,
|
||||
IDialogService dialogService,
|
||||
HttpClient httpClient)
|
||||
{
|
||||
_networkService = networkService;
|
||||
_installerService = installerService;
|
||||
_dialogService = dialogService;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<NetworkDevice> GetDeveloperInfoAsync(NetworkDevice device)
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = $"http://{device.IpAddress}:8001/api/v2/";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
string jsonContent = await response.Content.ReadAsStringAsync();
|
||||
JObject jsonObject = JObject.Parse(jsonContent);
|
||||
|
||||
return new NetworkDevice
|
||||
{
|
||||
IpAddress = jsonObject["device"]?["ip"]?.ToString(),
|
||||
DeviceName = WebUtility.HtmlDecode(jsonObject["device"]?["name"]?.ToString()),
|
||||
ModelName = jsonObject["device"]?["modelName"].ToString(),
|
||||
Manufacturer = jsonObject["device"]?["type"]?.ToString(),
|
||||
DeveloperMode = jsonObject["device"]?["developerMode"]?.ToString() ?? string.Empty,
|
||||
DeveloperIP = jsonObject["device"]?["developerIP"]?.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Error connecting to Samsung TV at {device.IpAddress}: {ex.Message}");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Error parsing JSON response: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Unexpected error: {ex.Message}");
|
||||
}
|
||||
|
||||
return new NetworkDevice
|
||||
{
|
||||
IpAddress = device.IpAddress,
|
||||
DeviceName = device.DeviceName,
|
||||
Manufacturer = device.Manufacturer,
|
||||
DeveloperMode = string.Empty,
|
||||
DeveloperIP = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<NetworkDevice>> ScanForDevicesAsync(CancellationToken cancellationToken = default, bool virtualScan = false)
|
||||
{
|
||||
var devices = new List<NetworkDevice>();
|
||||
|
||||
var networkDevices = await _networkService.GetLocalTizenAddresses(cancellationToken, virtualScan);
|
||||
|
||||
Debug.WriteLine($"NetworkDevices: {networkDevices.Count()}");
|
||||
foreach (NetworkDevice device in networkDevices)
|
||||
{
|
||||
if (await _networkService.IsPortOpenAsync(device.IpAddress, 8001, cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
var samsungDevice = await GetDeveloperInfoAsync(device);
|
||||
if (!string.IsNullOrEmpty(samsungDevice.DeviceName))
|
||||
devices.Add(samsungDevice);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
device.ModelName = device.ModelName;
|
||||
device.Manufacturer = device.Manufacturer;
|
||||
device.DeveloperMode = "1";
|
||||
device.DeveloperIP = string.Empty;
|
||||
|
||||
devices.Add(device);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
using Avalonia.Platform.Storage;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class FileHelper
|
||||
{
|
||||
private static readonly string[] wgtItem = ["*.wgt"];
|
||||
private static readonly string[] tpkItem = ["*.tpk"];
|
||||
private static readonly string[] allItem = ["*.wgt", "*.tpk"];
|
||||
|
||||
public async Task<string?> BrowseWgtFilesAsync(IStorageProvider storageProvider)
|
||||
{
|
||||
var fileTypes = new List<FilePickerFileType>
|
||||
{
|
||||
new("WGT Files")
|
||||
{
|
||||
Patterns = wgtItem
|
||||
},
|
||||
new("TPK Files")
|
||||
{
|
||||
Patterns = tpkItem
|
||||
},
|
||||
new("All Supported Files")
|
||||
{
|
||||
Patterns = allItem
|
||||
}
|
||||
};
|
||||
|
||||
var options = new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Select WGT/TPK File",
|
||||
FileTypeFilter = fileTypes,
|
||||
AllowMultiple = true
|
||||
};
|
||||
|
||||
var files = await storageProvider.OpenFilePickerAsync(options);
|
||||
|
||||
if (files?.Any() == true)
|
||||
{
|
||||
var newPaths = new List<string>();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var originalPath = file.Path.LocalPath;
|
||||
var directory = Path.GetDirectoryName(originalPath);
|
||||
var baseName = Path.GetFileNameWithoutExtension(originalPath);
|
||||
var extension = Path.GetExtension(originalPath);
|
||||
|
||||
var random = new Random();
|
||||
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
var randomSuffix = new string(Enumerable.Range(0, 4).Select(_ => chars[random.Next(chars.Length)]).ToArray());
|
||||
|
||||
var newFileName = $"{baseName}{randomSuffix}{extension}";
|
||||
var newFilePath = Path.Combine(directory ?? Environment.CurrentDirectory, newFileName);
|
||||
|
||||
File.Copy(originalPath, newFilePath, overwrite: true);
|
||||
|
||||
newPaths.Add(newFilePath);
|
||||
}
|
||||
|
||||
return string.Join(";", newPaths);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
public List<ExtensionEntry> ParseExtensions(string output)
|
||||
{
|
||||
var extensions = new List<ExtensionEntry>();
|
||||
var regex = new Regex(
|
||||
@"Index\s*:\s*(\d+)\s+Name\s*:\s*(.*?)\s+Repository\s*:\s*.*?\s+Id\s*:\s*.*?\s+Vendor\s*:\s*.*?\s+Description\s*:\s*.*?\s+Default\s*:\s*.*?\s+Activate\s*:\s*(true|false)",
|
||||
RegexOptions.Singleline);
|
||||
|
||||
foreach (Match match in regex.Matches(output))
|
||||
{
|
||||
extensions.Add(new ExtensionEntry
|
||||
{
|
||||
Index = int.Parse(match.Groups[1].Value),
|
||||
Name = match.Groups[2].Value.Trim(),
|
||||
Activated = bool.Parse(match.Groups[3].Value)
|
||||
});
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
public static async Task<bool> ModifyWgtPackageId(string wgtPath)
|
||||
{
|
||||
if (!File.Exists(wgtPath))
|
||||
return false;
|
||||
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
using (var originalStream = File.OpenRead(wgtPath))
|
||||
originalStream.CopyTo(memoryStream);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Update, true))
|
||||
{
|
||||
var configEntry = archive.GetEntry("config.xml");
|
||||
if (configEntry == null)
|
||||
return false;
|
||||
|
||||
string configContent;
|
||||
using (var reader = new StreamReader(configEntry.Open(), Encoding.UTF8))
|
||||
configContent = reader.ReadToEnd();
|
||||
|
||||
// Match the tizen:application line
|
||||
var regex = new Regex(@"<tizen:application\s+id=""(?<pkg>[A-Za-z0-9]+)\.Jellyfin""\s+package=""\k<pkg>""", RegexOptions.Multiline);
|
||||
var match = regex.Match(configContent);
|
||||
if (!match.Success)
|
||||
return false;
|
||||
|
||||
string oldPkg = match.Groups["pkg"].Value;
|
||||
string newPkg = GenerateRandomString(oldPkg.Length);
|
||||
|
||||
string newConfig = regex.Replace(configContent, m =>
|
||||
m.Value.Replace(oldPkg, newPkg));
|
||||
|
||||
configEntry.Delete(); // remove old
|
||||
var newEntry = archive.CreateEntry("config.xml");
|
||||
|
||||
using (var writer = new StreamWriter(newEntry.Open(), Encoding.UTF8))
|
||||
writer.Write(newConfig);
|
||||
}
|
||||
|
||||
// Write back to the original file
|
||||
File.WriteAllBytes(wgtPath, memoryStream.ToArray());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
private static string GenerateRandomString(int length)
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
var random = new Random();
|
||||
var sb = new StringBuilder(length);
|
||||
for (int i = 0; i < length; i++)
|
||||
sb.Append(chars[random.Next(chars.Length)]);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Jellyfin2Samsung-CrossOS/Helpers/Jellyfin/CSS/CustomCss.cs
Normal file
46
Jellyfin2Samsung-CrossOS/Helpers/Jellyfin/CSS/CustomCss.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.CSS
|
||||
{
|
||||
/// <summary>
|
||||
/// Injects custom CSS into the Jellyfin web app.
|
||||
/// Supports both inline CSS and @import rules for external themes like ElegantFin or Ultrachromic.
|
||||
/// </summary>
|
||||
public class CustomCss
|
||||
{
|
||||
public async Task InjectAsync(PackageWorkspace ws)
|
||||
{
|
||||
var customCss = AppSettings.Default.CustomCss;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(customCss))
|
||||
{
|
||||
Trace.WriteLine("[InjectCustomCss] No custom CSS configured, skipping injection");
|
||||
return;
|
||||
}
|
||||
|
||||
string indexPath = Path.Combine(ws.Root, "www", "index.html");
|
||||
if (!File.Exists(indexPath))
|
||||
{
|
||||
Trace.WriteLine("[InjectCustomCss] index.html not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var html = await File.ReadAllTextAsync(indexPath);
|
||||
|
||||
var cssBlock = new StringBuilder();
|
||||
cssBlock.AppendLine("<style id=\"jellyfin-custom-css\">");
|
||||
cssBlock.AppendLine(customCss);
|
||||
cssBlock.AppendLine("</style>");
|
||||
|
||||
// Inject before </head> to ensure CSS is loaded with the page
|
||||
html = html.Replace("</head>", cssBlock + "</head>");
|
||||
|
||||
await File.WriteAllTextAsync(indexPath, html);
|
||||
Trace.WriteLine("[InjectCustomCss] Custom CSS injected successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Diagnostic
|
||||
{
|
||||
public class JellyfinDiagnostic
|
||||
{
|
||||
public async Task InjectDevLogsAsync(PackageWorkspace ws)
|
||||
{
|
||||
string indexPath = Path.Combine(ws.Root, "www", "index.html");
|
||||
|
||||
if (!File.Exists(indexPath)) return;
|
||||
|
||||
var html = await File.ReadAllTextAsync(indexPath);
|
||||
|
||||
// 2. CSP UPDATE: WebSockets (ws:) are often blocked by default CSP
|
||||
if (html.Contains("Content-Security-Policy"))
|
||||
{
|
||||
// Add ws: to the connect-src or default-src
|
||||
html = html.Replace("default-src", "connect-src * ws: wss:; default-src");
|
||||
}
|
||||
|
||||
var script = new StringBuilder();
|
||||
script.AppendLine("<script>");
|
||||
script.AppendLine("(function(){");
|
||||
script.AppendLine(" try {");
|
||||
script.AppendLine($" var ws = new WebSocket('ws://{AppSettings.Default.LocalIp}:54321');");
|
||||
script.AppendLine(" ws.onopen = function(){ ws.send('INJECTOR: websocket connected'); };");
|
||||
script.AppendLine(" ws.onerror = function(e){ alert('WS Error. Check Firewall on port 54321'); };");
|
||||
|
||||
// ES5 Helper (No Arrow Functions)
|
||||
script.AppendLine(" var s = function(t, d) {");
|
||||
script.AppendLine(" try { if(ws.readyState === 1) ws.send(JSON.stringify({type:t, data:d})); } catch(e){}");
|
||||
script.AppendLine(" };");
|
||||
|
||||
// console.log override (No Spread Operator)
|
||||
script.AppendLine(" var oldLog = console.log;");
|
||||
script.AppendLine(" console.log = function() {");
|
||||
script.AppendLine(" var args = Array.prototype.slice.call(arguments);");
|
||||
script.AppendLine(" if(oldLog) oldLog.apply(console, args);");
|
||||
script.AppendLine(" s('log', args);");
|
||||
script.AppendLine(" };");
|
||||
|
||||
script.AppendLine(" window.onerror = function(m, sr, l, c) { s('error', [m, sr, l, c]); };");
|
||||
script.AppendLine(" } catch (e) { alert('Injector Failed: ' + e.message); }");
|
||||
script.AppendLine("})();");
|
||||
script.AppendLine("</script>");
|
||||
|
||||
// Inject before </head>
|
||||
html = html.Replace("</head>", script.ToString() + "\n</head>");
|
||||
await File.WriteAllTextAsync(indexPath, html);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Jellyfin2Samsung.Helpers.API;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.CSS;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Diagnostic;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Fixes;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Patches;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Plugins;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin
|
||||
{
|
||||
public class JellyfinPackagePatcher
|
||||
{
|
||||
private readonly JellyfinIndex _indexHtml;
|
||||
private readonly JellyfinDiagnostic _diagnostic;
|
||||
private readonly FixYouTube _youTube;
|
||||
private readonly CustomCss _customCss;
|
||||
|
||||
public JellyfinPackagePatcher(HttpClient http)
|
||||
{
|
||||
var api = new JellyfinApiClient(http);
|
||||
var plugins = new PluginManager(http, api);
|
||||
|
||||
_indexHtml = new JellyfinIndex(http, api, plugins);
|
||||
_diagnostic = new JellyfinDiagnostic();
|
||||
_youTube = new FixYouTube();
|
||||
_customCss = new CustomCss();
|
||||
}
|
||||
|
||||
public async Task<InstallResult> ApplyJellyfinConfigAsync(string packagePath)
|
||||
{
|
||||
using var ws = PackageWorkspace.Extract(packagePath);
|
||||
|
||||
// Apply server scripts (JS injection) if enabled
|
||||
if (AppSettings.Default.UseServerScripts)
|
||||
await _indexHtml.PatchIndexAsync(ws, AppSettings.Default.JellyfinFullUrl);
|
||||
|
||||
// Apply YouTube plugin patch if enabled
|
||||
if (AppSettings.Default.PatchYoutubePlugin)
|
||||
{
|
||||
await _youTube.PatchPluginAsync(ws);
|
||||
await _youTube.UpdateCorsAsync(ws);
|
||||
await _youTube.CreateYouTubeResolverAsync(ws);
|
||||
}
|
||||
|
||||
// Always update server address
|
||||
await _indexHtml.UpdateServerAddressAsync(ws);
|
||||
|
||||
// Inject auto-login credentials if available
|
||||
if (!string.IsNullOrEmpty(AppSettings.Default.JellyfinAccessToken) &&
|
||||
!string.IsNullOrEmpty(AppSettings.Default.JellyfinUserId))
|
||||
{
|
||||
Trace.WriteLine("Injecting auto-login credentials...");
|
||||
await _indexHtml.InjectAutoLoginAsync(ws);
|
||||
}
|
||||
|
||||
if (AppSettings.Default.EnableDevLogs)
|
||||
{
|
||||
Trace.WriteLine("Injecting dev logs...");
|
||||
await _diagnostic.InjectDevLogsAsync(ws);
|
||||
}
|
||||
|
||||
// Inject custom CSS if configured
|
||||
if (!string.IsNullOrWhiteSpace(AppSettings.Default.CustomCss))
|
||||
{
|
||||
Trace.WriteLine("Injecting custom CSS...");
|
||||
await _customCss.InjectAsync(ws);
|
||||
}
|
||||
|
||||
ws.Repack();
|
||||
return InstallResult.SuccessResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
935
Jellyfin2Samsung-CrossOS/Helpers/Jellyfin/Patches/FixYouTube.cs
Normal file
935
Jellyfin2Samsung-CrossOS/Helpers/Jellyfin/Patches/FixYouTube.cs
Normal file
@@ -0,0 +1,935 @@
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Fixes
|
||||
{
|
||||
public class FixYouTube
|
||||
{
|
||||
private async Task<int> ResolveServicePortAsync(PackageWorkspace ws)
|
||||
{
|
||||
var packageId = await FileHelper.ReadExtractedWgtPackageId(ws.Root);
|
||||
return packageId == "JepZAARz4r" ? 8124 : 8123;
|
||||
}
|
||||
public async Task PatchPluginAsync(PackageWorkspace ws)
|
||||
{
|
||||
int servicePort = await ResolveServicePortAsync(ws);
|
||||
var www = Path.Combine(ws.Root, "www");
|
||||
var utf8NoBom = new UTF8Encoding(false);
|
||||
|
||||
var candidates = Directory.GetFiles(www, "youtubePlayer-plugin*.js", SearchOption.AllDirectories)
|
||||
.Concat(Directory.GetFiles(www, "youtubePlayer-plugin*.chunk.js", SearchOption.AllDirectories))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
// V17: YT object protection + trailer fallback for non-English metadata
|
||||
string injected = """
|
||||
(function () {
|
||||
if (window.__YT_FIX_V17__) return;
|
||||
window.__YT_FIX_V17__ = true;
|
||||
|
||||
var SERVICE_BASE = 'http://localhost:8123';
|
||||
var currentPlayerInstance = null;
|
||||
|
||||
function sLog(msg, data) {
|
||||
try {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', SERVICE_BASE + '/log', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
var cleanData = (data && typeof data === 'object') ? JSON.stringify(data) : (data || '');
|
||||
xhr.send(JSON.stringify({args: ['[V17]', msg, cleanData]}));
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
sLog('LOADED', { href: window.location.href });
|
||||
|
||||
try {
|
||||
var appId = tizen.application.getCurrentApplication().appInfo.id;
|
||||
var pkgId = appId.split('.')[0];
|
||||
tizen.application.launch(pkgId + '.ytresolver', function() { sLog('SVC_LAUNCH_OK'); }, function(e) { sLog('SVC_LAUNCH_ERR', e.message); });
|
||||
} catch (e) { sLog('SVC_LAUNCH_FAIL', e.message); }
|
||||
|
||||
// ========================================================================
|
||||
// CUSTOM PLAYER CLASS - Mirrors Jellyfin's YoutubePlayer interface
|
||||
// ========================================================================
|
||||
function CustomPlayer(idOrEl, cfg) {
|
||||
var self = this;
|
||||
currentPlayerInstance = this;
|
||||
|
||||
var videoId = '';
|
||||
if (typeof cfg === 'string') videoId = cfg;
|
||||
else if (cfg && typeof cfg === 'object') videoId = cfg.videoId || cfg.id || '';
|
||||
|
||||
this._state = -1;
|
||||
this._currentTime = 0;
|
||||
this._duration = 0;
|
||||
this._volume = 100;
|
||||
this._ready = false;
|
||||
this._queue = [];
|
||||
this._destroyed = false;
|
||||
this._container = null;
|
||||
this._iframe = null;
|
||||
this._observer = null;
|
||||
this._messageHandler = null;
|
||||
|
||||
var container = (typeof idOrEl === 'string') ? document.getElementById(idOrEl) : idOrEl;
|
||||
this._container = container;
|
||||
|
||||
var iframe = document.createElement('iframe');
|
||||
this._iframe = iframe;
|
||||
|
||||
// Message handler for iframe communication
|
||||
this._messageHandler = function(ev) {
|
||||
if (self._destroyed) return;
|
||||
var m = ev.data;
|
||||
if (!m || !m.__ytbridge) return;
|
||||
|
||||
if (m.type === 'ready') {
|
||||
self._ready = true;
|
||||
sLog('IFRAME_READY');
|
||||
|
||||
// HIDE SPINNER AGAIN (in case it reappeared)
|
||||
try {
|
||||
if (window.Loading && window.Loading.hide) {
|
||||
window.Loading.hide();
|
||||
}
|
||||
var spinners = document.querySelectorAll('.docspinner');
|
||||
for (var i = 0; i < spinners.length; i++) {
|
||||
spinners[i].classList.remove('mdlSpinnerActive');
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
if (cfg.events && cfg.events.onReady) cfg.events.onReady({ target: self });
|
||||
|
||||
// Process queued commands
|
||||
while(self._queue.length) {
|
||||
var q = self._queue.shift();
|
||||
self._send(q.cmd, q.val);
|
||||
}
|
||||
|
||||
// ENSURE AUTOPLAY: Jellyfin expects video to start playing immediately
|
||||
// The YouTube iframe should auto-play, but we'll trigger it again to be sure
|
||||
setTimeout(function() {
|
||||
sLog('AUTOPLAY_TRIGGER');
|
||||
self._send('play');
|
||||
}, 200);
|
||||
} else if (m.type === 'state') {
|
||||
self._state = m.data;
|
||||
if (cfg.events && cfg.events.onStateChange) {
|
||||
cfg.events.onStateChange({ target: self, data: m.data });
|
||||
}
|
||||
} else if (m.type === 'time') {
|
||||
self._currentTime = m.t / 1000;
|
||||
self._duration = m.d / 1000;
|
||||
self._state = m.s;
|
||||
} else if (m.type === 'error') {
|
||||
if (cfg.events && cfg.events.onError) {
|
||||
cfg.events.onError(m.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', this._messageHandler);
|
||||
|
||||
function mount() {
|
||||
if (!container || self._destroyed) return;
|
||||
sLog('MOUNTING', { extractedId: videoId });
|
||||
|
||||
if (!videoId) {
|
||||
sLog('ERR_MISSING_ID', 'Missing videoId');
|
||||
return;
|
||||
}
|
||||
|
||||
// HIDE JELLYFIN LOADING SPINNER
|
||||
try {
|
||||
if (window.Loading && window.Loading.hide) {
|
||||
window.Loading.hide();
|
||||
sLog('SPINNER_HIDDEN_VIA_API');
|
||||
}
|
||||
// Fallback: directly remove spinner classes
|
||||
var spinners = document.querySelectorAll('.docspinner');
|
||||
for (var i = 0; i < spinners.length; i++) {
|
||||
spinners[i].classList.remove('mdlSpinnerActive');
|
||||
}
|
||||
} catch(e) {
|
||||
sLog('SPINNER_HIDE_ERR', e.message);
|
||||
}
|
||||
|
||||
// Use fixed positioning with max z-index to stay on top
|
||||
iframe.style.cssText = 'width:100vw; height:100vh; border:0; background:#000; position:fixed; top:0; left:0; z-index:2147483647;';
|
||||
iframe.setAttribute('allow', 'autoplay; encrypted-media; fullscreen');
|
||||
iframe.src = SERVICE_BASE + '/player.html?videoId=' + encodeURIComponent(videoId);
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(iframe);
|
||||
|
||||
// Watch for React wiping the container
|
||||
self._observer = new MutationObserver(function(mutations) {
|
||||
if (self._destroyed) return;
|
||||
if (container && !container.contains(iframe)) {
|
||||
sLog('REACT_WIPE_RESTORE');
|
||||
container.appendChild(iframe);
|
||||
}
|
||||
});
|
||||
self._observer.observe(container, { childList: true });
|
||||
}
|
||||
|
||||
this._send = function(cmd, val) {
|
||||
if (this._destroyed) return;
|
||||
if (!this._ready) {
|
||||
this._queue.push({cmd:cmd, val:val});
|
||||
return;
|
||||
}
|
||||
if (this._iframe && this._iframe.contentWindow) {
|
||||
this._iframe.contentWindow.postMessage({ __ytbridge_cmd: true, cmd: cmd, val: val }, '*');
|
||||
}
|
||||
};
|
||||
|
||||
// API Methods matching YT.Player interface
|
||||
this.playVideo = function() {
|
||||
sLog('CMD_PLAY');
|
||||
this._send('play');
|
||||
};
|
||||
|
||||
this.pauseVideo = function() {
|
||||
sLog('CMD_PAUSE');
|
||||
this._send('pause');
|
||||
};
|
||||
|
||||
this.stopVideo = function() {
|
||||
sLog('CMD_STOP');
|
||||
this._send('stop');
|
||||
};
|
||||
|
||||
this.seekTo = function(s, allowSeekAhead) {
|
||||
sLog('CMD_SEEK', s);
|
||||
this._send('seek', s * 1000);
|
||||
};
|
||||
|
||||
this.setVolume = function(v) {
|
||||
this._volume = v;
|
||||
this._send('volume', v);
|
||||
};
|
||||
|
||||
this.getVolume = function() {
|
||||
return this._volume;
|
||||
};
|
||||
|
||||
this.getCurrentTime = function() {
|
||||
return this._currentTime;
|
||||
};
|
||||
|
||||
this.getDuration = function() {
|
||||
return this._duration;
|
||||
};
|
||||
|
||||
this.getPlayerState = function() {
|
||||
return this._state;
|
||||
};
|
||||
|
||||
this.mute = function() {
|
||||
this._send('mute', true);
|
||||
};
|
||||
|
||||
this.unMute = function() {
|
||||
this._send('mute', false);
|
||||
};
|
||||
|
||||
this.isMuted = function() {
|
||||
return this._muted || false;
|
||||
};
|
||||
|
||||
this.setSize = function(width, height) {
|
||||
// Size is already 100vw/100vh, no action needed
|
||||
sLog('SET_SIZE', { w: width, h: height });
|
||||
};
|
||||
|
||||
// CRITICAL: Proper cleanup to prevent background playback
|
||||
this.destroy = function() {
|
||||
sLog('DESTROY_CALLED');
|
||||
|
||||
if (this._destroyed) return;
|
||||
this._destroyed = true;
|
||||
|
||||
// Stop playback first
|
||||
if (this._iframe && this._iframe.contentWindow) {
|
||||
try {
|
||||
this._iframe.contentWindow.postMessage({ __ytbridge_cmd: true, cmd: 'stop' }, '*');
|
||||
} catch(e) {
|
||||
sLog('DESTROY_STOP_ERR', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up event listeners
|
||||
if (this._messageHandler) {
|
||||
window.removeEventListener('message', this._messageHandler);
|
||||
this._messageHandler = null;
|
||||
}
|
||||
|
||||
// Disconnect observer
|
||||
if (this._observer) {
|
||||
this._observer.disconnect();
|
||||
this._observer = null;
|
||||
}
|
||||
|
||||
// Remove iframe from DOM
|
||||
if (this._iframe) {
|
||||
if (this._iframe.parentNode) {
|
||||
this._iframe.parentNode.removeChild(this._iframe);
|
||||
}
|
||||
this._iframe = null;
|
||||
}
|
||||
|
||||
// Clear container
|
||||
if (this._container) {
|
||||
this._container.innerHTML = '';
|
||||
this._container = null;
|
||||
}
|
||||
|
||||
// Clear queue
|
||||
this._queue = [];
|
||||
|
||||
if (currentPlayerInstance === this) {
|
||||
currentPlayerInstance = null;
|
||||
}
|
||||
|
||||
sLog('DESTROYED');
|
||||
};
|
||||
|
||||
mount();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// YT NAMESPACE - Make it immutable to prevent overwriting
|
||||
// ========================================================================
|
||||
var customYT = {
|
||||
Player: CustomPlayer,
|
||||
PlayerState: {
|
||||
UNSTARTED: -1,
|
||||
ENDED: 0,
|
||||
PLAYING: 1,
|
||||
PAUSED: 2,
|
||||
BUFFERING: 3,
|
||||
CUED: 5
|
||||
},
|
||||
loaded: 1,
|
||||
__CUSTOM__: true
|
||||
};
|
||||
|
||||
// Make YT property non-writable so it can't be overwritten
|
||||
Object.defineProperty(window, 'YT', {
|
||||
value: customYT,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
sLog('YT_PROTECTED');
|
||||
|
||||
// Trigger ready callback if it exists
|
||||
if (window.onYouTubeIframeAPIReady) {
|
||||
setTimeout(function() {
|
||||
sLog('TRIGGER_READY_CALLBACK');
|
||||
window.onYouTubeIframeAPIReady();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// NAVIGATION CLEANUP - Hook into Jellyfin's router (FIXED)
|
||||
// ========================================================================
|
||||
|
||||
var lastPath = window.location.pathname;
|
||||
|
||||
// Listen for Jellyfin page changes
|
||||
document.addEventListener('viewshow', function() {
|
||||
var currentPath = window.location.pathname;
|
||||
sLog('VIEW_SHOW_EVENT', { lastPath: lastPath, currentPath: currentPath });
|
||||
|
||||
// Only cleanup if we actually navigated away (path changed)
|
||||
if (currentPath !== lastPath) {
|
||||
lastPath = currentPath;
|
||||
|
||||
// If we're navigating away from video page, ensure cleanup
|
||||
if (currentPath !== '/video' && currentPlayerInstance) {
|
||||
sLog('NAV_CLEANUP_TRIGGER');
|
||||
try {
|
||||
currentPlayerInstance.destroy();
|
||||
} catch(e) {
|
||||
sLog('NAV_CLEANUP_ERR', e.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sLog('VIEW_SHOW_SAME_PATH');
|
||||
}
|
||||
});
|
||||
|
||||
// Also listen for back button via popstate
|
||||
window.addEventListener('popstate', function() {
|
||||
sLog('POPSTATE_EVENT');
|
||||
setTimeout(function() {
|
||||
var currentPath = window.location.pathname;
|
||||
if (currentPath !== '/video' && currentPlayerInstance) {
|
||||
sLog('POPSTATE_CLEANUP_TRIGGER');
|
||||
try {
|
||||
currentPlayerInstance.destroy();
|
||||
} catch(e) {
|
||||
sLog('POPSTATE_CLEANUP_ERR', e.message);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// TRAILER FALLBACK - Find trailers for non-English metadata languages
|
||||
// ========================================================================
|
||||
|
||||
var _tfTimer = null;
|
||||
var _tfTmdbKey = '';
|
||||
var _tfKeyFetched = false;
|
||||
|
||||
// Fetch TMDB API key from Jellyfin server (works for admin users, silent fail for others)
|
||||
function _tfFetchTmdbKey(api, cb) {
|
||||
if (_tfKeyFetched) return cb();
|
||||
_tfKeyFetched = true;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', api.serverAddress() + '/Plugins', true);
|
||||
xhr.setRequestHeader('Authorization', 'MediaBrowser Token="' + api.accessToken() + '"');
|
||||
xhr.onload = function() {
|
||||
if (xhr.status !== 200) { sLog('TF_KEY_NO_PLUGINS'); return cb(); }
|
||||
try {
|
||||
var plugins = JSON.parse(xhr.responseText);
|
||||
var tmdbPlugin = null;
|
||||
for (var i = 0; i < plugins.length; i++) {
|
||||
if (plugins[i].Name && plugins[i].Name.indexOf('TMDb') !== -1) { tmdbPlugin = plugins[i]; break; }
|
||||
}
|
||||
if (!tmdbPlugin) { sLog('TF_KEY_NO_TMDB_PLUGIN'); return cb(); }
|
||||
var xhr2 = new XMLHttpRequest();
|
||||
xhr2.open('GET', api.serverAddress() + '/Plugins/' + tmdbPlugin.Id + '/Configuration', true);
|
||||
xhr2.setRequestHeader('Authorization', 'MediaBrowser Token="' + api.accessToken() + '"');
|
||||
xhr2.onload = function() {
|
||||
if (xhr2.status === 200) {
|
||||
try {
|
||||
var cfg = JSON.parse(xhr2.responseText);
|
||||
if (cfg.TmdbApiKey) { _tfTmdbKey = cfg.TmdbApiKey; sLog('TF_KEY_CUSTOM', _tfTmdbKey.substring(0, 8) + '...'); }
|
||||
else { sLog('TF_KEY_DEFAULT'); }
|
||||
} catch(e) {}
|
||||
}
|
||||
cb();
|
||||
};
|
||||
xhr2.onerror = function() { cb(); };
|
||||
xhr2.send();
|
||||
} catch(e) { cb(); }
|
||||
};
|
||||
xhr.onerror = function() { cb(); };
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function _tfCheck() {
|
||||
if (_tfTimer) clearTimeout(_tfTimer);
|
||||
_tfTimer = setTimeout(_tfDoCheck, 2000);
|
||||
}
|
||||
|
||||
function _tfDoCheck() {
|
||||
var hash = window.location.hash || window.location.href;
|
||||
if (hash.indexOf('details') === -1 && hash.indexOf('item') === -1) return;
|
||||
|
||||
var existing = document.querySelector('.btnPlayTrailer, [data-action="playtrailer"]');
|
||||
if (existing) { sLog('TF_HAS_TRAILER'); return; }
|
||||
|
||||
var match = hash.match(/[?&]id=([^&]+)/);
|
||||
if (!match) return;
|
||||
var itemId = match[1];
|
||||
|
||||
var api = window.ApiClient;
|
||||
if (!api) { sLog('TF_NO_API'); return; }
|
||||
|
||||
// Fetch TMDB key first (cached after first call), then proceed
|
||||
_tfFetchTmdbKey(api, function() {
|
||||
sLog('TF_CHECK', { itemId: itemId });
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
var url = api.serverAddress() + '/Users/' + api.getCurrentUserId() + '/Items/' + itemId + '?Fields=ProviderIds,RemoteTrailers';
|
||||
xhr.open('GET', url, true);
|
||||
xhr.setRequestHeader('Authorization', 'MediaBrowser Token="' + api.accessToken() + '"');
|
||||
xhr.onload = function() {
|
||||
if (xhr.status !== 200) return;
|
||||
try {
|
||||
var item = JSON.parse(xhr.responseText);
|
||||
if (item.Type !== 'Movie' && item.Type !== 'Series') return;
|
||||
if (item.RemoteTrailers && item.RemoteTrailers.length > 0) { sLog('TF_HAS_REMOTE'); return; }
|
||||
if (item.LocalTrailerCount && item.LocalTrailerCount > 0) { sLog('TF_HAS_LOCAL'); return; }
|
||||
|
||||
var tmdbId = (item.ProviderIds && item.ProviderIds.Tmdb) || '';
|
||||
var title = item.Name || '';
|
||||
var year = item.ProductionYear || '';
|
||||
var lang = (document.documentElement.lang || navigator.language || 'en').split('-')[0];
|
||||
|
||||
if (!tmdbId && !title) return;
|
||||
sLog('TF_SEARCH', { tmdbId: tmdbId, title: title, lang: lang });
|
||||
|
||||
var svcUrl = SERVICE_BASE + '/trailer?tmdbId=' + encodeURIComponent(tmdbId) + '&title=' + encodeURIComponent(title) + '&year=' + encodeURIComponent(year) + '&lang=' + encodeURIComponent(lang);
|
||||
if (_tfTmdbKey) svcUrl += '&tmdbKey=' + encodeURIComponent(_tfTmdbKey);
|
||||
|
||||
var xhr2 = new XMLHttpRequest();
|
||||
xhr2.open('GET', svcUrl, true);
|
||||
xhr2.onload = function() {
|
||||
if (xhr2.status !== 200) return;
|
||||
try {
|
||||
var r = JSON.parse(xhr2.responseText);
|
||||
if (r.videoKey) {
|
||||
sLog('TF_FOUND', { key: r.videoKey, source: r.source });
|
||||
_tfInjectBtn(r.videoKey);
|
||||
} else { sLog('TF_NOT_FOUND'); }
|
||||
} catch(e) { sLog('TF_ERR', e.message); }
|
||||
};
|
||||
xhr2.send();
|
||||
} catch(e) { sLog('TF_ITEM_ERR', e.message); }
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
function _tfInjectBtn(videoKey) {
|
||||
var container = document.querySelector('.mainDetailButtons, .detailButtons');
|
||||
if (!container || document.querySelector('.btnTrailerInjected')) return;
|
||||
|
||||
var btn = document.createElement('button');
|
||||
btn.setAttribute('is', 'emby-button');
|
||||
btn.setAttribute('type', 'button');
|
||||
btn.className = 'button-flat btnPlayTrailer btnTrailerInjected detailButton emby-button';
|
||||
|
||||
var icon = document.createElement('span');
|
||||
icon.className = 'material-icons detailButton-icon';
|
||||
icon.textContent = 'theaters';
|
||||
|
||||
var wrap = document.createElement('span');
|
||||
wrap.className = 'detailButton-content';
|
||||
var txt = document.createElement('span');
|
||||
txt.className = 'button-text';
|
||||
txt.textContent = 'Trailer';
|
||||
wrap.appendChild(txt);
|
||||
|
||||
btn.appendChild(icon);
|
||||
btn.appendChild(wrap);
|
||||
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
sLog('TF_PLAY', { key: videoKey });
|
||||
_tfPlayOverlay(videoKey);
|
||||
});
|
||||
|
||||
var first = container.querySelector('button, .detailButton');
|
||||
if (first && first.nextSibling) container.insertBefore(btn, first.nextSibling);
|
||||
else container.appendChild(btn);
|
||||
|
||||
sLog('TF_BTN_INJECTED');
|
||||
}
|
||||
|
||||
function _tfPlayOverlay(videoKey) {
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'trailerOverlay';
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:2147483647;background:#000;';
|
||||
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.style.cssText = 'width:100%;height:100%;border:0;';
|
||||
iframe.setAttribute('allow', 'autoplay; encrypted-media; fullscreen');
|
||||
iframe.src = SERVICE_BASE + '/player.html?videoId=' + encodeURIComponent(videoKey);
|
||||
overlay.appendChild(iframe);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
var msgH = function(ev) {
|
||||
if (!ev.data || !ev.data.__ytbridge) return;
|
||||
if (ev.data.type === 'state' && ev.data.data === 0) _tfClose();
|
||||
};
|
||||
window.addEventListener('message', msgH);
|
||||
|
||||
var keyH = function(ev) {
|
||||
if (ev.keyCode === 10009 || ev.keyCode === 27 || ev.keyCode === 8) {
|
||||
ev.preventDefault(); ev.stopPropagation(); _tfClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', keyH, true);
|
||||
|
||||
function _tfClose() {
|
||||
window.removeEventListener('message', msgH);
|
||||
document.removeEventListener('keydown', keyH, true);
|
||||
try { if (iframe.contentWindow) iframe.contentWindow.postMessage({ __ytbridge_cmd: true, cmd: 'stop' }, '*'); } catch(e) {}
|
||||
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||
sLog('TF_CLOSED');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('viewshow', _tfCheck);
|
||||
|
||||
sLog('INIT_COMPLETE');
|
||||
})();
|
||||
""".Replace("http://localhost:8123", $"http://localhost:{servicePort}");
|
||||
|
||||
foreach (var file in candidates)
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(file);
|
||||
if (content.Contains("__YT_FIX_V17__")) continue;
|
||||
await File.WriteAllTextAsync(file, injected + "\n" + content, utf8NoBom);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. CREATE THE NODE.JS SERVICE (with trailer fallback endpoint)
|
||||
public async Task CreateYouTubeResolverAsync(PackageWorkspace ws)
|
||||
{
|
||||
int servicePort = await ResolveServicePortAsync(ws);
|
||||
var utf8NoBom = new UTF8Encoding(false);
|
||||
string serviceDir = Path.Combine(ws.Root, "service");
|
||||
string serviceJsPath = Path.Combine(serviceDir, "service.js");
|
||||
if (!Directory.Exists(serviceDir)) Directory.CreateDirectory(serviceDir);
|
||||
|
||||
string serviceJsContent = """
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var urlMod = require('url');
|
||||
|
||||
var PORT = 8123;
|
||||
var LISTEN_HOST = '0.0.0.0';
|
||||
var LOGS = [];
|
||||
var TMDB_KEY = '4219e299c89411838049ab0dab19ebd5'; // fallback key from Jellyfin TmdbUtils.cs, used when runtime key extraction fails
|
||||
|
||||
function log(msg, data) {
|
||||
var line = new Date().toISOString() + ' ' + msg + ' ' + (data ? JSON.stringify(data) : '');
|
||||
LOGS.push(line);
|
||||
if (LOGS.length > 2000) LOGS.shift();
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
function write(res, code, contentType, body, additionalHeaders) {
|
||||
var headers = {
|
||||
'Content-Type': contentType,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
'Access-Control-Allow-Private-Network': 'true',
|
||||
'Cache-Control': 'no-store'
|
||||
};
|
||||
if (additionalHeaders) Object.assign(headers, additionalHeaders);
|
||||
res.writeHead(code, headers);
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TRAILER FALLBACK HELPERS - TMDB + DuckDuckGo Lite
|
||||
// ========================================================================
|
||||
|
||||
function httpsGet(reqUrl, cb) {
|
||||
var done = false;
|
||||
function finish(err, status, body) { if (done) return; done = true; cb(err, status, body); }
|
||||
var parsed = urlMod.parse(reqUrl);
|
||||
var opts = {
|
||||
hostname: parsed.hostname,
|
||||
path: parsed.path,
|
||||
port: 443,
|
||||
method: 'GET',
|
||||
rejectUnauthorized: false,
|
||||
headers: { 'User-Agent': 'JellyfinSamsungTV/1.0' }
|
||||
};
|
||||
var req = https.request(opts, function(resp) {
|
||||
var body = '';
|
||||
resp.on('data', function(c) { body += c; });
|
||||
resp.on('end', function() { finish(null, resp.statusCode, body); });
|
||||
});
|
||||
req.on('error', function(e) { finish(e); });
|
||||
req.setTimeout(8000, function() { req.abort(); });
|
||||
req.end();
|
||||
}
|
||||
|
||||
function fetchTmdbTrailers(tmdbId, lang, apiKey, cb) {
|
||||
if (!tmdbId) return cb(null, { langKey: null, enKey: null });
|
||||
var key = apiKey || TMDB_KEY;
|
||||
var u = 'https://api.themoviedb.org/3/movie/' + tmdbId +
|
||||
'?api_key=' + key +
|
||||
'&language=' + lang +
|
||||
'&append_to_response=videos' +
|
||||
'&include_video_language=' + lang + ',en,null';
|
||||
log('TMDB_FETCH ' + u);
|
||||
httpsGet(u, function(err, status, body) {
|
||||
if (err || status !== 200) { log('TMDB_ERR ' + (err ? err.message : status)); return cb(null, { langKey: null, enKey: null }); }
|
||||
try {
|
||||
var data = JSON.parse(body);
|
||||
var vids = (data.videos && data.videos.results) || [];
|
||||
var trailers = [];
|
||||
for (var i = 0; i < vids.length; i++) {
|
||||
if (vids[i].site === 'YouTube' && (vids[i].type === 'Trailer' || vids[i].type === 'Teaser')) trailers.push(vids[i]);
|
||||
}
|
||||
var langPick = null, enPick = null;
|
||||
for (var j = 0; j < trailers.length; j++) {
|
||||
if (!langPick && trailers[j].iso_639_1 === lang) langPick = trailers[j];
|
||||
if (!enPick && trailers[j].iso_639_1 === 'en') enPick = trailers[j];
|
||||
}
|
||||
log('TMDB_RESULT langKey=' + (langPick ? langPick.key : 'null') + ' enKey=' + (enPick ? enPick.key : 'null'));
|
||||
cb(null, {
|
||||
langKey: langPick ? langPick.key : null,
|
||||
enKey: enPick ? enPick.key : null
|
||||
});
|
||||
} catch(e) { log('TMDB_PARSE_ERR ' + e.message); cb(null, { langKey: null, enKey: null }); }
|
||||
});
|
||||
}
|
||||
|
||||
function searchDdg(title, year, lang, cb) {
|
||||
var langMap = __LANG_MAP__;
|
||||
var langName = langMap[lang] || '';
|
||||
var langKeywords = langName.toLowerCase().split(' ');
|
||||
var q = title + (year ? ' ' + year : '') + ' Trailer ' + langName + ' site:youtube.com';
|
||||
var encoded = encodeURIComponent(q).replace(/%20/g, '+');
|
||||
var u = 'https://lite.duckduckgo.com/lite/?q=' + encoded;
|
||||
log('DDG_FETCH q=' + q);
|
||||
httpsGet(u, function(err, status, body) {
|
||||
if (err || status !== 200) { log('DDG_ERR ' + (err ? err.message : status)); return cb(null, { langKey: null, fallbackKey: null }); }
|
||||
var decoded = body.replace(/%2F/gi, '/').replace(/%3F/gi, '?').replace(/%3D/gi, '=').replace(/%26/gi, '&');
|
||||
|
||||
// Extract results with titles: find <a> tags followed by youtube URLs
|
||||
var linkRe = /<a[^>]+href="([^"]*youtube\.com\/watch\?v=([a-zA-Z0-9_\-]{11})[^"]*)"[^>]*>([^<]*)<\/a>/gi;
|
||||
var m, seen = {}, results = [];
|
||||
while ((m = linkRe.exec(decoded)) !== null) {
|
||||
if (!seen[m[2]]) {
|
||||
seen[m[2]] = true;
|
||||
results.push({ key: m[2], title: m[3] });
|
||||
}
|
||||
}
|
||||
|
||||
// Also catch URL-encoded links
|
||||
var encRe = /href="[^"]*youtube\.com%2Fwatch%3Fv%3D([a-zA-Z0-9_\-]{11})[^"]*"[^>]*>([^<]*)<\/a>/gi;
|
||||
while ((m = encRe.exec(body)) !== null) {
|
||||
if (!seen[m[1]]) {
|
||||
seen[m[1]] = true;
|
||||
results.push({ key: m[1], title: m[2] });
|
||||
}
|
||||
}
|
||||
|
||||
if (!results.length) { log('DDG_NO_RESULTS'); return cb(null, { langKey: null, fallbackKey: null }); }
|
||||
|
||||
// Split: language-matched vs non-matched results
|
||||
var langMatch = null;
|
||||
var fallback = null;
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var t = results[i].title.toLowerCase();
|
||||
var isLangMatch = false;
|
||||
for (var j = 0; j < langKeywords.length; j++) {
|
||||
if (langKeywords[j] && t.indexOf(langKeywords[j]) !== -1) { isLangMatch = true; break; }
|
||||
}
|
||||
if (isLangMatch && !langMatch) langMatch = results[i];
|
||||
if (!isLangMatch && !fallback) fallback = results[i];
|
||||
if (langMatch && fallback) break;
|
||||
}
|
||||
|
||||
log('DDG_FOUND langKey=' + (langMatch ? langMatch.key : 'null') + ' fallbackKey=' + (fallback ? fallback.key : 'null') + ' total=' + results.length);
|
||||
cb(null, { langKey: langMatch ? langMatch.key : null, fallbackKey: fallback ? fallback.key : null });
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
|
||||
var PLAYER_HTML = `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<style>html,body{margin:0;padding:0;background:#000;width:100%;height:100%;overflow:hidden;}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="player" style="width:100%;height:100%;"></div>
|
||||
<script>
|
||||
var VID = new URLSearchParams(window.location.search).get('videoId');
|
||||
function post(type, data, t, d, s) {
|
||||
window.parent.postMessage({ __ytbridge: true, type: type, data: data, t: t||0, d: d||0, s: s||-1 }, '*');
|
||||
}
|
||||
var tag = document.createElement('script');
|
||||
tag.src = "https://www.youtube.com/iframe_api";
|
||||
document.head.appendChild(tag);
|
||||
|
||||
var player;
|
||||
var autoplayAttempted = false;
|
||||
|
||||
window.onYouTubeIframeAPIReady = function() {
|
||||
player = new YT.Player('player', {
|
||||
height: '100%', width: '100%', videoId: VID,
|
||||
playerVars: {
|
||||
'autoplay': 1,
|
||||
'controls': 0,
|
||||
'enablejsapi': 1,
|
||||
'origin': 'http://localhost:8123',
|
||||
'playsinline': 1,
|
||||
'mute': 0
|
||||
},
|
||||
events: {
|
||||
'onReady': function(ev) {
|
||||
post('ready');
|
||||
|
||||
// Ensure autoplay starts
|
||||
if (!autoplayAttempted) {
|
||||
autoplayAttempted = true;
|
||||
setTimeout(function() {
|
||||
if (player && player.playVideo) {
|
||||
player.playVideo();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
setInterval(function(){
|
||||
if(player && player.getCurrentTime)
|
||||
post('time', null, player.getCurrentTime()*1000, player.getDuration()*1000, player.getPlayerState());
|
||||
}, 500);
|
||||
},
|
||||
'onStateChange': function(ev) { post('state', ev.data); },
|
||||
'onError': function(ev) { post('error', ev.data); }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('message', function(ev) {
|
||||
if (!ev.data || !ev.data.__ytbridge_cmd || !player) return;
|
||||
var m = ev.data;
|
||||
if (m.cmd === 'play') player.playVideo();
|
||||
else if (m.cmd === 'pause') player.pauseVideo();
|
||||
else if (m.cmd === 'stop') player.stopVideo();
|
||||
else if (m.cmd === 'seek') player.seekTo(m.val / 1000, true);
|
||||
else if (m.cmd === 'volume') player.setVolume(m.val);
|
||||
else if (m.cmd === 'mute') {
|
||||
if (m.val) player.mute();
|
||||
else player.unMute();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
function handler(req, res) {
|
||||
var u = urlMod.parse(req.url, true);
|
||||
if (req.method === 'OPTIONS') return write(res, 204, 'text/plain', '');
|
||||
|
||||
if (u.pathname === '/log') {
|
||||
var body = '';
|
||||
req.on('data', function(c) { body += c; });
|
||||
req.on('end', function() {
|
||||
try {
|
||||
var j = JSON.parse(body);
|
||||
log(j.args ? j.args.join(' ') : 'LOG');
|
||||
} catch(e){}
|
||||
write(res, 200, 'application/json', '{}');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (u.pathname === '/debug/logs') return write(res, 200, 'application/json', JSON.stringify({logs: LOGS}));
|
||||
|
||||
if (u.pathname === '/player.html') {
|
||||
return write(res, 200, 'text/html', PLAYER_HTML, { 'Referrer-Policy': 'no-referrer-when-downgrade' });
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// TRAILER FALLBACK ENDPOINT
|
||||
// 1 TMDB call fetches both lang + en, then: lang(TMDB) -> lang(DDG) -> en(CACHED) -> en(CACHED)
|
||||
// ====================================================================
|
||||
if (u.pathname === '/trailer') {
|
||||
var tId = u.query.tmdbId || '';
|
||||
var tTitle = u.query.title || '';
|
||||
var tYear = u.query.year || '';
|
||||
var tLang = u.query.lang || 'en';
|
||||
var tKey = u.query.tmdbKey || '';
|
||||
log('TRAILER_REQ tmdbId=' + tId + ' title=' + tTitle + ' lang=' + tLang + ' customKey=' + (tKey ? 'yes' : 'no'));
|
||||
|
||||
// Single TMDB call: fetches both user-language and English trailers
|
||||
fetchTmdbTrailers(tId, tLang, tKey, function(e1, tmdb) {
|
||||
|
||||
// Step 1: TMDB user-language trailer (cached)
|
||||
if (tmdb.langKey) return write(res, 200, 'application/json', JSON.stringify({videoKey:tmdb.langKey, source:'tmdb_'+tLang}));
|
||||
|
||||
// Single DDG call: returns both language-matched and fallback keys
|
||||
searchDdg(tTitle, tYear, tLang, function(e2, ddg) {
|
||||
|
||||
// Step 2: DDG language-matched trailer (cached)
|
||||
if (ddg.langKey) return write(res, 200, 'application/json', JSON.stringify({videoKey:ddg.langKey, source:'ddg_'+tLang}));
|
||||
|
||||
if (tLang !== 'en') {
|
||||
// Step 3: TMDB English fallback (cached from step 1, no extra call)
|
||||
if (tmdb.enKey) return write(res, 200, 'application/json', JSON.stringify({videoKey:tmdb.enKey, source:'tmdb_en_fallback'}));
|
||||
|
||||
// Step 4: DDG non-language result as English fallback (cached from step 2, no extra call)
|
||||
if (ddg.fallbackKey) return write(res, 200, 'application/json', JSON.stringify({videoKey:ddg.fallbackKey, source:'ddg_en_fallback'}));
|
||||
}
|
||||
|
||||
write(res, 200, 'application/json', JSON.stringify({videoKey:null, source:null}));
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return write(res, 404, 'text/plain', 'Not Found');
|
||||
}
|
||||
|
||||
var server = http.createServer(handler);
|
||||
server.listen(PORT, LISTEN_HOST, function() { log('SERVER LISTENING ' + LISTEN_HOST + ':' + PORT); });
|
||||
""".Replace("var PORT = 8123", $"var PORT = {servicePort}")
|
||||
.Replace("'origin': 'http://localhost:8123'", $"'origin': 'http://localhost:{servicePort}'")
|
||||
.Replace("__LANG_MAP__", TrailerLanguageMap.JsObject);
|
||||
await File.WriteAllTextAsync(serviceJsPath, serviceJsContent, utf8NoBom);
|
||||
}
|
||||
|
||||
// 3. UPDATE CONFIG.XML
|
||||
public async Task UpdateCorsAsync(PackageWorkspace ws)
|
||||
{
|
||||
int servicePort = await ResolveServicePortAsync(ws);
|
||||
var path = Path.Combine(ws.Root, "config.xml");
|
||||
if (!File.Exists(path)) return;
|
||||
|
||||
var doc = XDocument.Load(path);
|
||||
XNamespace ns = "http://www.w3.org/ns/widgets";
|
||||
XNamespace tizen = "http://tizen.org/ns/widgets";
|
||||
|
||||
doc.Root.Elements(ns + "access").Remove();
|
||||
doc.Root.Elements(ns + "allow-navigation").Remove();
|
||||
doc.Root.Elements(tizen + "allow-navigation").Remove();
|
||||
doc.Root.Elements(tizen + "content-security-policy").Remove();
|
||||
|
||||
doc.Root.Add(new XElement(ns + "access", new XAttribute("origin", "*"), new XAttribute("subdomains", "true")));
|
||||
doc.Root.Add(new XElement(ns + "allow-navigation", new XAttribute("href", "*")));
|
||||
doc.Root.Add(new XElement(tizen + "allow-navigation", "*"));
|
||||
|
||||
var serviceId = "ytresolver";
|
||||
if (!doc.Descendants(tizen + "service").Any(x => x.Attribute("name")?.Value == serviceId))
|
||||
{
|
||||
var pkgId = doc.Root.Element(tizen + "application")?.Attribute("package")?.Value ?? "AprZAARz4r";
|
||||
doc.Root.Add(new XElement(tizen + "service",
|
||||
new XAttribute("id", pkgId + "." + serviceId),
|
||||
new XAttribute("type", "service"),
|
||||
new XElement(tizen + "content", new XAttribute("src", "service/service.js")),
|
||||
new XElement(tizen + "name", serviceId)
|
||||
));
|
||||
}
|
||||
|
||||
// UpdateCorsAsync - the CSP string is short so interpolation is fine there since there's no JS braces involved:
|
||||
string csp = $"default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; " +
|
||||
$"script-src * 'unsafe-inline' 'unsafe-eval' http://localhost:{servicePort} https://www.youtube.com; " +
|
||||
$"frame-src * http://localhost:{servicePort} https://www.youtube.com; " +
|
||||
$"connect-src * http://localhost:{servicePort};";
|
||||
|
||||
doc.Root.Add(new XElement(tizen + "content-security-policy", csp));
|
||||
doc.Root.Add(new XElement(tizen + "allow-mixed-content", "true"));
|
||||
|
||||
var privs = new[] {
|
||||
"http://tizen.org/privilege/internet",
|
||||
"http://tizen.org/privilege/network.public",
|
||||
"http://tizen.org/privilege/content.read"
|
||||
};
|
||||
foreach (var p in privs)
|
||||
{
|
||||
if (!doc.Descendants(tizen + "privilege").Any(x => x.Attribute("name")?.Value == p))
|
||||
doc.Root.Add(new XElement(tizen + "privilege", new XAttribute("name", p)));
|
||||
}
|
||||
|
||||
doc.Save(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
using Jellyfin2Samsung.Helpers.API;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Plugins;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Patches
|
||||
{
|
||||
public class JellyfinIndex(
|
||||
HttpClient http,
|
||||
JellyfinApiClient api,
|
||||
PluginManager plugins)
|
||||
{
|
||||
private readonly JellyfinApiClient _apiClient = api;
|
||||
private readonly JellyfinPluginPatcher _plugins = new(http, api, plugins);
|
||||
|
||||
public async Task PatchIndexAsync(PackageWorkspace ws, string serverUrl)
|
||||
{
|
||||
string index = Path.Combine(ws.Root, "www", "index.html");
|
||||
if (!File.Exists(index)) return;
|
||||
|
||||
var html = await File.ReadAllTextAsync(index);
|
||||
|
||||
html = HtmlUtils.EnsureBaseHref(html);
|
||||
html = HtmlUtils.RewriteLocalPaths(html);
|
||||
|
||||
var css = new StringBuilder();
|
||||
var headJs = new StringBuilder();
|
||||
var bodyJs = new StringBuilder();
|
||||
|
||||
await _plugins.PatchPluginsAsync(ws, serverUrl, css, headJs, bodyJs);
|
||||
|
||||
html = html.Replace("</head>", css + "\n" + headJs + "\n</head>");
|
||||
html = html.Replace("</body>", bodyJs + "\n</body>");
|
||||
|
||||
html = HtmlUtils.CleanAndApplyCsp(html);
|
||||
html = HtmlUtils.EnsurePublicJsIsLast(html);
|
||||
|
||||
await File.WriteAllTextAsync(index, html);
|
||||
}
|
||||
public async Task UpdateServerAddressAsync(PackageWorkspace ws)
|
||||
{
|
||||
string path = Path.Combine(ws.Root, "www", "config.json");
|
||||
|
||||
JsonObject config;
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(path);
|
||||
config = JsonNode.Parse(json)?.AsObject()
|
||||
?? new JsonObject();
|
||||
}
|
||||
else
|
||||
{
|
||||
config = new JsonObject();
|
||||
}
|
||||
|
||||
// Ensure multiserver is set
|
||||
config["multiserver"] = false;
|
||||
|
||||
// Ensure servers array exists
|
||||
if (config["servers"] is not JsonArray servers)
|
||||
{
|
||||
servers = new JsonArray();
|
||||
config["servers"] = servers;
|
||||
}
|
||||
|
||||
var serverUrl = UrlHelper.NormalizeServerUrl(AppSettings.Default.JellyfinFullUrl);
|
||||
|
||||
// Avoid duplicates
|
||||
if (!servers.Any(s => s?.GetValue<string>() == serverUrl))
|
||||
servers.Add(serverUrl);
|
||||
|
||||
// Add LocalAddress (IP-based) as fallback when the primary URL uses mDNS (.local)
|
||||
// Samsung TVs (Tizen) cannot reliably resolve mDNS hostnames, especially after network disruptions
|
||||
var localAddress = UrlHelper.NormalizeServerUrl(AppSettings.Default.JellyfinServerLocalAddress);
|
||||
if (!string.IsNullOrEmpty(localAddress) &&
|
||||
localAddress != serverUrl &&
|
||||
UrlHelper.IsValidHttpUrl(localAddress) &&
|
||||
!servers.Any(s => s?.GetValue<string>() == localAddress))
|
||||
{
|
||||
servers.Add(localAddress);
|
||||
Trace.WriteLine($"[UpdateServerAddress] Added LocalAddress fallback: {localAddress}");
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(path, config.ToJsonString());
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Injects auto-login credentials into the Jellyfin web app.
|
||||
/// This stores the access token and server info in localStorage format.
|
||||
/// Uses the real server ID from /System/Info/Public to prevent ServerMismatch errors.
|
||||
/// </summary>
|
||||
public async Task InjectAutoLoginAsync(PackageWorkspace ws)
|
||||
{
|
||||
var accessToken = AppSettings.Default.JellyfinAccessToken;
|
||||
var userId = AppSettings.Default.JellyfinUserId;
|
||||
var serverUrl = UrlHelper.NormalizeServerUrl(AppSettings.Default.JellyfinFullUrl);
|
||||
var serverId = AppSettings.Default.JellyfinServerId;
|
||||
var localAddress = AppSettings.Default.JellyfinServerLocalAddress;
|
||||
var serverName = AppSettings.Default.JellyfinServerName;
|
||||
|
||||
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(serverUrl))
|
||||
{
|
||||
Trace.WriteLine("[InjectAutoLogin] Missing credentials, skipping auto-login injection");
|
||||
return;
|
||||
}
|
||||
|
||||
// If server ID or server name is not stored, try to fetch it now
|
||||
if (string.IsNullOrEmpty(serverId) || string.IsNullOrEmpty(serverName))
|
||||
{
|
||||
Trace.WriteLine("[InjectAutoLogin] Server ID/Name not cached, fetching from server...");
|
||||
var serverInfo = await _apiClient.GetPublicSystemInfoAsync(serverUrl);
|
||||
if (serverInfo != null && !string.IsNullOrEmpty(serverInfo.Id))
|
||||
{
|
||||
serverId = serverInfo.Id;
|
||||
localAddress = serverInfo.LocalAddress ?? "";
|
||||
serverName = serverInfo.ServerName ?? "";
|
||||
AppSettings.Default.JellyfinServerId = serverId;
|
||||
AppSettings.Default.JellyfinServerLocalAddress = localAddress;
|
||||
AppSettings.Default.JellyfinServerName = serverName;
|
||||
AppSettings.Default.Save();
|
||||
Trace.WriteLine($"[InjectAutoLogin] Fetched and stored server ID: {serverId}, Name: {serverName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine("[InjectAutoLogin] WARNING: Could not fetch server ID, auto-login may fail with ServerMismatch");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
string indexPath = Path.Combine(ws.Root, "www", "index.html");
|
||||
if (!File.Exists(indexPath))
|
||||
{
|
||||
Trace.WriteLine("[InjectAutoLogin] index.html not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var html = await File.ReadAllTextAsync(indexPath);
|
||||
|
||||
// Create the credentials object that Jellyfin web expects
|
||||
// Using the REAL server ID (GUID) from /System/Info/Public to prevent ServerMismatch
|
||||
var credentialsScript = new StringBuilder();
|
||||
credentialsScript.AppendLine("<script>");
|
||||
credentialsScript.AppendLine("(function() {");
|
||||
credentialsScript.AppendLine(" try {");
|
||||
credentialsScript.AppendLine($" var serverUrl = '{HtmlUtils.EscapeJsString(serverUrl)}';");
|
||||
credentialsScript.AppendLine($" var serverId = '{HtmlUtils.EscapeJsString(serverId)}';");
|
||||
credentialsScript.AppendLine($" var localAddress = '{HtmlUtils.EscapeJsString(localAddress)}';");
|
||||
credentialsScript.AppendLine($" var serverName = '{HtmlUtils.EscapeJsString(serverName)}';");
|
||||
credentialsScript.AppendLine($" var userId = '{HtmlUtils.EscapeJsString(userId)}';");
|
||||
credentialsScript.AppendLine($" var accessToken = '{HtmlUtils.EscapeJsString(accessToken)}';");
|
||||
credentialsScript.AppendLine();
|
||||
credentialsScript.AppendLine(" // Create credentials object matching Jellyfin's expected format");
|
||||
credentialsScript.AppendLine(" // Using real server ID (GUID) from /System/Info/Public");
|
||||
credentialsScript.AppendLine(" var credentials = {");
|
||||
credentialsScript.AppendLine(" Servers: [{");
|
||||
credentialsScript.AppendLine(" Name: serverName || serverUrl,");
|
||||
credentialsScript.AppendLine(" ManualAddress: serverUrl,");
|
||||
credentialsScript.AppendLine(" LocalAddress: localAddress || serverUrl,");
|
||||
credentialsScript.AppendLine(" Id: serverId,");
|
||||
credentialsScript.AppendLine(" UserId: userId,");
|
||||
credentialsScript.AppendLine(" AccessToken: accessToken,");
|
||||
credentialsScript.AppendLine(" DateLastAccessed: new Date().getTime()");
|
||||
credentialsScript.AppendLine(" }]");
|
||||
credentialsScript.AppendLine(" };");
|
||||
credentialsScript.AppendLine();
|
||||
credentialsScript.AppendLine(" // Store in localStorage");
|
||||
credentialsScript.AppendLine(" localStorage.setItem('jellyfin_credentials', JSON.stringify(credentials));");
|
||||
credentialsScript.AppendLine();
|
||||
credentialsScript.AppendLine(" console.log('[Auto-Login] Credentials injected for server: ' + serverName + ' (' + serverUrl + ') with ID: ' + serverId);");
|
||||
credentialsScript.AppendLine(" } catch(e) {");
|
||||
credentialsScript.AppendLine(" console.error('[Auto-Login] Failed to inject credentials:', e);");
|
||||
credentialsScript.AppendLine(" }");
|
||||
credentialsScript.AppendLine("})();");
|
||||
credentialsScript.AppendLine("</script>");
|
||||
|
||||
// Inject before </head> to ensure it runs before Jellyfin's scripts
|
||||
html = html.Replace("</head>", credentialsScript + "\n</head>");
|
||||
|
||||
await File.WriteAllTextAsync(indexPath, html);
|
||||
Trace.WriteLine($"[InjectAutoLogin] Auto-login credentials injected successfully with server ID: {serverId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Fixes
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps ISO 639 language codes to search keywords for DuckDuckGo Lite trailer search.
|
||||
/// Covers all languages supported by Jellyfin's localization system.
|
||||
/// Each entry contains the English name + native name for maximum DDG search match rate.
|
||||
/// </summary>
|
||||
public static class TrailerLanguageMap
|
||||
{
|
||||
public const string JsObject = @"{
|
||||
ab:'Abkhaz Аԥсуа',
|
||||
af:'Afrikaans',
|
||||
ar:'Arabic العربية',
|
||||
as:'Assamese অসমীয়া',
|
||||
be:'Belarusian Беларуская',
|
||||
bg:'Bulgarian Български',
|
||||
bn:'Bengali বাংলা',
|
||||
ca:'Catalan Català',
|
||||
chr:'Cherokee ᏣᎳᎩ',
|
||||
cs:'Czech Český',
|
||||
cy:'Welsh Cymraeg',
|
||||
da:'Danish Dansk',
|
||||
de:'Deutsch German',
|
||||
el:'Greek Ελληνικά',
|
||||
en:'English',
|
||||
enm:'English',
|
||||
eo:'Esperanto',
|
||||
es:'Spanish Español',
|
||||
et:'Estonian Eesti',
|
||||
eu:'Basque Euskara',
|
||||
fa:'Persian Farsi فارسی',
|
||||
fi:'Finnish Suomi',
|
||||
fil:'Filipino Tagalog',
|
||||
fo:'Faroese Føroyskt',
|
||||
fr:'French Français',
|
||||
ga:'Irish Gaeilge',
|
||||
gl:'Galician Galego',
|
||||
gsw:'Swiss German Schweizerdeutsch',
|
||||
he:'Hebrew עברית',
|
||||
hi:'Hindi हिन्दी',
|
||||
hr:'Croatian Hrvatski',
|
||||
ht:'Haitian Creole Kreyòl',
|
||||
hu:'Hungarian Magyar',
|
||||
hy:'Armenian Հայերեն',
|
||||
id:'Indonesian Bahasa',
|
||||
is:'Icelandic Íslenska',
|
||||
it:'Italian Italiano',
|
||||
ja:'Japanese 日本語',
|
||||
jbo:'Lojban',
|
||||
ka:'Georgian ქართული',
|
||||
kab:'Kabyle Taqbaylit',
|
||||
kk:'Kazakh Қазақша',
|
||||
km:'Khmer ភាសាខ្មែរ',
|
||||
kn:'Kannada ಕನ್ನಡ',
|
||||
ko:'Korean 한국어',
|
||||
kw:'Cornish Kernewek',
|
||||
ky:'Kyrgyz Кыргызча',
|
||||
lb:'Luxembourgish Lëtzebuergesch',
|
||||
lt:'Lithuanian Lietuvių',
|
||||
lv:'Latvian Latviešu',
|
||||
lzh:'Chinese 中文',
|
||||
mi:'Maori Māori',
|
||||
mk:'Macedonian Македонски',
|
||||
ml:'Malayalam മലയാളം',
|
||||
mn:'Mongolian Монгол',
|
||||
mr:'Marathi मराठी',
|
||||
ms:'Malay Melayu',
|
||||
mt:'Maltese Malti',
|
||||
my:'Burmese Myanmar မြန်မာ',
|
||||
nb:'Norwegian Norsk',
|
||||
ne:'Nepali नेपाली',
|
||||
nl:'Dutch Nederlands',
|
||||
nn:'Norwegian Norsk',
|
||||
oc:'Occitan',
|
||||
or:'Odia ଓଡ଼ିଆ',
|
||||
pa:'Punjabi ਪੰਜਾਬੀ',
|
||||
pl:'Polish Polski',
|
||||
pr:'English',
|
||||
pt:'Portuguese Português',
|
||||
ro:'Romanian Română',
|
||||
ru:'Russian Русский',
|
||||
si:'Sinhala සිංහල',
|
||||
sk:'Slovak Slovenský',
|
||||
sl:'Slovenian Slovenščina',
|
||||
sn:'Shona',
|
||||
sq:'Albanian Shqip',
|
||||
sr:'Serbian Српски',
|
||||
sv:'Swedish Svenska',
|
||||
sw:'Swahili Kiswahili',
|
||||
ta:'Tamil தமிழ்',
|
||||
te:'Telugu తెలుగు',
|
||||
th:'Thai ไทย',
|
||||
tr:'Turkish Türkçe',
|
||||
ug:'Uyghur ئۇيغۇرچە',
|
||||
uk:'Ukrainian Українська',
|
||||
ur:'Urdu اردو',
|
||||
uz:'Uzbek Oʻzbekcha',
|
||||
vi:'Vietnamese Tiếng Việt',
|
||||
zh:'Chinese 中文',
|
||||
zu:'Zulu isiZulu'
|
||||
}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Plugins.EditorsChoice
|
||||
{
|
||||
public class PatchEditorsChoice
|
||||
{
|
||||
public string ApplyAsync(string js)
|
||||
{
|
||||
string css = @"
|
||||
.hss-hero {
|
||||
position: relative;
|
||||
width: 94%;
|
||||
height: 500px !important;
|
||||
margin: 30px auto 30px auto !important;
|
||||
overflow: hidden;
|
||||
border-radius: 15px;
|
||||
background: #000;
|
||||
border: 0.06em solid var(--borderColor) !important;
|
||||
}
|
||||
|
||||
.hss-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0;
|
||||
transition: opacity 1s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hss-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 0) 60%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
) !important;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.hss-content {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 30px !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hss-logo {
|
||||
max-width: 400px;
|
||||
max-height: 120px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 15px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.hss-logo + div,
|
||||
.hss-rate {
|
||||
color: #fff !important;
|
||||
font-size: 1em !important;
|
||||
font-weight: 400 !important;
|
||||
height: 10px !important;
|
||||
}
|
||||
|
||||
.hss-overview {
|
||||
color: #eee !important;
|
||||
width: 45%;
|
||||
font-size: 0.9em !important;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 25px !important;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis !important;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.hss-btn {
|
||||
background: #fff !important;
|
||||
color: #000 !important;
|
||||
border: none;
|
||||
padding: 0.9em 1em !important;
|
||||
border-radius: 0.5em !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 1em !important;
|
||||
text-transform: none !important;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
width: fit-content;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.hss-btn:focus {
|
||||
background: var(--highlightOutlineColor) !important;
|
||||
color: #fff !important;
|
||||
transform: none !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.editorsChoiceItemBanner,
|
||||
.editorsChoiceItemsContainer {
|
||||
display: none !important;
|
||||
}
|
||||
";
|
||||
|
||||
|
||||
return @"
|
||||
(function () {
|
||||
var style = document.createElement('style');
|
||||
style.innerText = `" + css.Replace("\r\n", " ").Replace("\"", "\\\"") + @"`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
function init() {
|
||||
var api = window.ApiClient || (window.ConnectionManager && window.ConnectionManager.getcurrentitem().apiClient);
|
||||
var container = document.querySelector('.sections') || document.querySelector('.homeSectionsContainer');
|
||||
|
||||
if (!api || !container || document.getElementById('hss-hero')) return;
|
||||
|
||||
api.getItems(api.getCurrentUserId(), { IncludeItemTypes: 'Movie', SortBy: 'Random', SortOrder: 'Descending', Limit: 8, Recursive: true, Fields: 'Overview,ImageTags,CommunityRating' }).then(function(res) {
|
||||
var items = res.Items || [];
|
||||
if (!items.length) return;
|
||||
|
||||
var html = `
|
||||
<div id=""hss-hero"" class=""hss-hero"">
|
||||
<img class=""hss-bg"" crossorigin=""anonymous"">
|
||||
<div class=""hss-overlay""></div>
|
||||
<div class=""hss-content"">
|
||||
<img class=""hss-logo"" crossorigin=""anonymous"">
|
||||
<div style=""color:#f5c518; font-size:1.6em; font-weight:bold; margin-bottom:10px;"">⭐ <span class=""hss-rate""></span></div>
|
||||
<p class=""hss-overview""></p>
|
||||
<button class=""hss-btn"">Watch Now</button>
|
||||
</div>
|
||||
</div>`;
|
||||
container.insertAdjacentHTML('afterbegin', html);
|
||||
|
||||
var idx = 0;
|
||||
function slide() {
|
||||
var item = items[idx];
|
||||
var el = document.getElementById('hss-hero');
|
||||
var host = api.serverAddress();
|
||||
var token = api.accessToken();
|
||||
|
||||
var bg = el.querySelector('.hss-bg');
|
||||
bg.style.opacity = '0';
|
||||
|
||||
setTimeout(function() {
|
||||
bg.src = host + '/Items/' + item.Id + '/Images/Backdrop/0?api_key=' + token;
|
||||
bg.onload = function() { bg.style.opacity = '0.8'; };
|
||||
|
||||
var logo = el.querySelector('.hss-logo');
|
||||
if (item.ImageTags.Logo) {
|
||||
logo.src = host + '/Items/' + item.Id + '/Images/Logo/0?api_key=' + token;
|
||||
logo.style.display = 'block';
|
||||
} else { logo.style.display = 'none'; }
|
||||
|
||||
el.querySelector('.hss-rate').innerText = (item.CommunityRating || '7.5');
|
||||
el.querySelector('.hss-overview').innerText = item.Overview || '';
|
||||
el.querySelector('.hss-btn').onclick = function() {
|
||||
if(window.Emby && window.Emby.Page) window.Emby.Page.showItem(item.Id);
|
||||
else window.location.hash = '#!/item?id=' + item.Id;
|
||||
};
|
||||
idx = (idx + 1) % items.length;
|
||||
}, 400);
|
||||
}
|
||||
slide();
|
||||
setInterval(slide, 10000);
|
||||
});
|
||||
}
|
||||
setInterval(init, 2000);
|
||||
})();";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using Jellyfin2Samsung.Helpers.API;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// Orchestrates plugin-related patching:
|
||||
/// - Fetch server index
|
||||
/// - Cache plugin assets referenced by index.html
|
||||
/// - Apply API-installed plugins using PluginManager + plugin patch classes
|
||||
/// </summary>
|
||||
public class JellyfinPluginPatcher
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly JellyfinApiClient _apiClient;
|
||||
private readonly PluginManager _pluginManager;
|
||||
|
||||
private readonly HashSet<string> _injectedScripts = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _injectedStyles = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public JellyfinPluginPatcher(
|
||||
HttpClient httpClient,
|
||||
JellyfinApiClient apiClient,
|
||||
PluginManager pluginManager)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_apiClient = apiClient;
|
||||
_pluginManager = pluginManager;
|
||||
}
|
||||
|
||||
public async Task PatchPluginsAsync(
|
||||
PackageWorkspace workspace,
|
||||
string serverUrl,
|
||||
StringBuilder cssBuilder,
|
||||
StringBuilder headJsBuilder,
|
||||
StringBuilder bodyJsBuilder)
|
||||
{
|
||||
string pluginCacheDir = Path.Combine(workspace.Root, "www", "plugin_cache");
|
||||
Directory.CreateDirectory(pluginCacheDir);
|
||||
|
||||
string serverHtml = await FetchServerIndexAsync(serverUrl);
|
||||
if (!string.IsNullOrWhiteSpace(serverHtml))
|
||||
{
|
||||
await CacheServerAssetsAsync(
|
||||
serverHtml,
|
||||
serverUrl,
|
||||
pluginCacheDir,
|
||||
cssBuilder,
|
||||
bodyJsBuilder);
|
||||
}
|
||||
|
||||
await ApplyApiInstalledPluginsAsync(
|
||||
workspace,
|
||||
serverUrl,
|
||||
pluginCacheDir,
|
||||
cssBuilder,
|
||||
headJsBuilder,
|
||||
bodyJsBuilder);
|
||||
}
|
||||
|
||||
private async Task<string> FetchServerIndexAsync(string serverUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = UrlHelper.CombineUrl(serverUrl, "/web/index.html");
|
||||
Trace.WriteLine($"▶ Fetching server index.html: {url}");
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
return await _httpClient.GetStringAsync(url, cts.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ Failed to fetch server index.html: {ex}");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CacheServerAssetsAsync(
|
||||
string serverHtml,
|
||||
string serverUrl,
|
||||
string pluginCacheDir,
|
||||
StringBuilder cssBuilder,
|
||||
StringBuilder jsBuilder)
|
||||
{
|
||||
Trace.WriteLine("▶ Extracting plugin assets from server index…");
|
||||
|
||||
// --- CSS ---
|
||||
var cssMatches = RegexPatterns.Html.LinkHref.Matches(serverHtml);
|
||||
|
||||
foreach (Match m in cssMatches)
|
||||
{
|
||||
string href = m.Groups[1].Value;
|
||||
if (string.IsNullOrWhiteSpace(href)) continue;
|
||||
|
||||
if (!_pluginManager.TryClassifyServerAsset(href, out var kind) || kind != ServerAssetKind.PluginAsset)
|
||||
continue;
|
||||
|
||||
await CacheCssAsync(serverUrl, href, pluginCacheDir, cssBuilder);
|
||||
}
|
||||
|
||||
// --- JS ---
|
||||
var jsMatches = RegexPatterns.Html.ScriptSrc.Matches(serverHtml);
|
||||
|
||||
foreach (Match m in jsMatches)
|
||||
{
|
||||
string src = m.Groups[1].Value;
|
||||
if (string.IsNullOrWhiteSpace(src)) continue;
|
||||
|
||||
if (!_pluginManager.TryClassifyServerAsset(src, out var kind) || kind != ServerAssetKind.PluginAsset)
|
||||
continue;
|
||||
|
||||
await CacheJsAsync(serverUrl, src, pluginCacheDir, jsBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CacheCssAsync(string serverUrl, string href, string pluginCacheDir, StringBuilder cssBuilder)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = GetAbsoluteUri(serverUrl, href);
|
||||
var fileName = Path.GetFileName(uri.AbsolutePath);
|
||||
|
||||
if (!fileName.EndsWith(".css", StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
var localPath = Path.Combine(pluginCacheDir, fileName);
|
||||
var bytes = await _httpClient.GetByteArrayAsync(uri);
|
||||
await File.WriteAllBytesAsync(localPath, bytes);
|
||||
|
||||
var outHref = $"plugin_cache/{fileName}";
|
||||
AppendStyleOnce(cssBuilder, outHref);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ Failed to cache CSS '{href}': {ex}");
|
||||
}
|
||||
}
|
||||
private async Task CacheJsAsync(string serverUrl, string src, string pluginCacheDir, StringBuilder jsBuilder)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Server-relative or absolute; cache into plugin_cache root with filename
|
||||
var uri = GetAbsoluteUri(serverUrl, src);
|
||||
var fileName = Path.GetFileName(uri.AbsolutePath);
|
||||
|
||||
if (!fileName.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
|
||||
fileName += ".js";
|
||||
|
||||
var localPath = Path.Combine(pluginCacheDir, fileName);
|
||||
|
||||
var jsContent = await _httpClient.GetStringAsync(uri);
|
||||
jsContent = await EsbuildHelper.TranspileAsync(jsContent, uri.ToString());
|
||||
|
||||
await File.WriteAllTextAsync(localPath, jsContent);
|
||||
|
||||
var outSrc = $"plugin_cache/{fileName}";
|
||||
AppendScriptOnce(jsBuilder, $"<script src=\"{outSrc}\"></script>", outSrc);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ Failed to cache JS '{src}': {ex}");
|
||||
}
|
||||
}
|
||||
private async Task ApplyApiInstalledPluginsAsync(
|
||||
PackageWorkspace workspace,
|
||||
string serverUrl,
|
||||
string pluginCacheDir,
|
||||
StringBuilder cssBuilder,
|
||||
StringBuilder headJsBuilder,
|
||||
StringBuilder bodyJsBuilder)
|
||||
{
|
||||
var apiPlugins = await _apiClient.GetInstalledPluginsAsync(serverUrl);
|
||||
|
||||
foreach (var plugin in apiPlugins)
|
||||
{
|
||||
var entry = _pluginManager.FindPluginEntry(plugin);
|
||||
if (entry == null) continue;
|
||||
|
||||
var patch = _pluginManager.ResolvePatch(entry);
|
||||
if (patch == null)
|
||||
{
|
||||
Trace.WriteLine($"ℹ No patch implementation registered for plugin '{entry.Name}', skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
Trace.WriteLine($"⚙ Applying plugin patch: {entry.Name}");
|
||||
|
||||
var ctx = new PluginPatchContext(
|
||||
workspace: workspace,
|
||||
serverUrl: serverUrl,
|
||||
pluginCacheDir: pluginCacheDir,
|
||||
matrixEntry: entry,
|
||||
pluginManager: _pluginManager,
|
||||
cssBuilder: cssBuilder,
|
||||
headJsBuilder: headJsBuilder,
|
||||
bodyJsBuilder: bodyJsBuilder,
|
||||
injectedScripts: _injectedScripts,
|
||||
injectedStyles: _injectedStyles
|
||||
);
|
||||
|
||||
await patch.ApplyAsync(ctx);
|
||||
}
|
||||
}
|
||||
private static Uri GetAbsoluteUri(string serverUrl, string relOrAbs)
|
||||
{
|
||||
return UrlHelper.GetAbsoluteUri(serverUrl, relOrAbs);
|
||||
}
|
||||
private void AppendScriptOnce(StringBuilder js, string scriptTag, string src)
|
||||
{
|
||||
if (_injectedScripts.Add(src))
|
||||
js.AppendLine(scriptTag);
|
||||
else
|
||||
Trace.WriteLine($"ℹ Script already injected, skipping: {src}");
|
||||
}
|
||||
private void AppendStyleOnce(StringBuilder css, string href)
|
||||
{
|
||||
if (_injectedStyles.Add(href))
|
||||
css.AppendLine($"<link rel=\"stylesheet\" href=\"{href}\" />");
|
||||
else
|
||||
Trace.WriteLine($"ℹ CSS already injected, skipping: {href}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Plugins.KefinTweaks
|
||||
{
|
||||
public sealed class KefinTweaksPatch : IJellyfinPluginPatch
|
||||
{
|
||||
private const string KefinTweaksRawRoot =
|
||||
"https://raw.githubusercontent.com/ranaldsgift/KefinTweaks/v0.4.5/";
|
||||
|
||||
public async Task ApplyAsync(PluginPatchContext ctx)
|
||||
{
|
||||
string pluginCacheDir = ctx.PluginCacheDir;
|
||||
string serverUrl = ctx.ServerUrl;
|
||||
|
||||
string publicJsPath = Path.Combine(pluginCacheDir, "public.js");
|
||||
if (!File.Exists(publicJsPath))
|
||||
{
|
||||
Trace.WriteLine("▶ KefinTweaks: public.js not found, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
string js = await File.ReadAllTextAsync(publicJsPath, Encoding.UTF8);
|
||||
|
||||
if (!RegexPatterns.KefinTweaks.Loader.IsMatch(js))
|
||||
{
|
||||
Trace.WriteLine("▶ KefinTweaks: loader not found, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
Trace.WriteLine("⚙ KefinTweaks: patching public.js");
|
||||
|
||||
Directory.CreateDirectory(Path.Combine(pluginCacheDir, "kefinTweaks"));
|
||||
|
||||
// --- main plugin ---
|
||||
await ctx.PluginManager.DownloadAndTranspileAsync(
|
||||
KefinTweaksRawRoot + "kefinTweaks-plugin.js",
|
||||
pluginCacheDir,
|
||||
Path.Combine("kefinTweaks", "kefinTweaks-plugin.js"));
|
||||
|
||||
RewritePluginRoot(pluginCacheDir);
|
||||
|
||||
// --- injector ---
|
||||
var injectorPath = await ctx.PluginManager.DownloadAndTranspileAsync(
|
||||
KefinTweaksRawRoot + "injector.js",
|
||||
pluginCacheDir,
|
||||
Path.Combine("kefinTweaks", "injector.js"));
|
||||
|
||||
if (injectorPath != null)
|
||||
{
|
||||
await ProcessModulesAsync(ctx, injectorPath);
|
||||
await ProcessCssAsync(ctx);
|
||||
js = await InjectSkinCssAsync(ctx, js);
|
||||
}
|
||||
|
||||
// --- rewrite loader ---
|
||||
js = RegexPatterns.KefinTweaks.Loader.Replace(
|
||||
js,
|
||||
"script.src = 'plugin_cache/kefinTweaks/kefinTweaks-plugin.js';");
|
||||
|
||||
js = RegexPatterns.KefinTweaks.TweaksRoot.Replace(
|
||||
js,
|
||||
@"""kefinTweaksRoot"": ""plugin_cache/kefinTweaks/""");
|
||||
|
||||
await File.WriteAllTextAsync(publicJsPath, js, Encoding.UTF8);
|
||||
|
||||
Trace.WriteLine("✓ KefinTweaks patched successfully");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Core helpers
|
||||
// ------------------------------------------------------------
|
||||
|
||||
private static void RewritePluginRoot(string pluginCacheDir)
|
||||
{
|
||||
string path = Path.Combine(pluginCacheDir, "kefinTweaks", "kefinTweaks-plugin.js");
|
||||
if (!File.Exists(path)) return;
|
||||
|
||||
var js = File.ReadAllText(path);
|
||||
js = js.Replace(
|
||||
"https://cdn.jsdelivr.net/gh/ranaldsgift/KefinTweaks",
|
||||
"plugin_cache/kefinTweaks");
|
||||
|
||||
File.WriteAllText(path, js);
|
||||
}
|
||||
|
||||
private async Task ProcessModulesAsync(PluginPatchContext ctx, string injectorPath)
|
||||
{
|
||||
string src = await File.ReadAllTextAsync(injectorPath, Encoding.UTF8);
|
||||
var matches = RegexPatterns.KefinTweaks.ScriptEntry.Matches(src);
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (Match m in matches)
|
||||
{
|
||||
string name = m.Groups[1].Value.Trim();
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
if (!name.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (!seen.Add(name)) continue;
|
||||
if (name is "kefinTweaks-plugin.js" or "injector.js") continue;
|
||||
|
||||
string modulePath =
|
||||
name.Equals("jquery.flurry.min.js", StringComparison.OrdinalIgnoreCase)
|
||||
? "scripts/third%20party/" + name
|
||||
: "scripts/" + name;
|
||||
|
||||
string url = KefinTweaksRawRoot + modulePath;
|
||||
|
||||
string relPath = Path.Combine(
|
||||
"kefinTweaks",
|
||||
modulePath.Replace("%20", " ")
|
||||
.Replace("/", Path.DirectorySeparatorChar.ToString()));
|
||||
|
||||
await ctx.PluginManager.DownloadAndTranspileAsync(url, ctx.PluginCacheDir, relPath);
|
||||
}
|
||||
|
||||
Trace.WriteLine("✓ KefinTweaks modules cached");
|
||||
}
|
||||
|
||||
private async Task ProcessCssAsync(PluginPatchContext ctx)
|
||||
{
|
||||
var cssFiles = new[]
|
||||
{
|
||||
"chromic-kefin.css",
|
||||
"elegant-kefin.css",
|
||||
"fin-kefin-10.11.css",
|
||||
"flow-kefin.css",
|
||||
"glassfin-kefin.css",
|
||||
"jamfin-kefin-10.css",
|
||||
"jamfin-kefin.css",
|
||||
"neutralfin-kefin.css",
|
||||
"scyfin-kefin.css",
|
||||
"optional/ElegantFin/solidAppBar.css",
|
||||
"optional/ElegantFin/libraryLabelVisibility.css",
|
||||
"optional/ElegantFin/extraOverlayButtons.css",
|
||||
"optional/ElegantFin/centerPlayButton.css",
|
||||
"optional/ElegantFin/cardHoverEffect.css",
|
||||
};
|
||||
|
||||
Trace.WriteLine("▶ KefinTweaks: caching CSS skins");
|
||||
|
||||
foreach (var file in cssFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = KefinTweaksRawRoot + "skins/" + file;
|
||||
|
||||
string local = Path.Combine(
|
||||
ctx.PluginCacheDir,
|
||||
"kefinTweaks",
|
||||
"skins",
|
||||
"css",
|
||||
file.Replace("/", Path.DirectorySeparatorChar.ToString()));
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(local)!);
|
||||
|
||||
var bytes = await ctx.PluginManager.DownloadBytesAsync(url);
|
||||
if (bytes != null)
|
||||
await File.WriteAllBytesAsync(local, bytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ KefinTweaks CSS failed: {file} {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> InjectSkinCssAsync(PluginPatchContext ctx, string js)
|
||||
{
|
||||
var info = await ctx.PluginManager.Api.GetPublicSystemInfoAsync(ctx.ServerUrl);
|
||||
var version = info?.Version ?? "0.0.0";
|
||||
var majorMinor = string.Join(".", version.Split('.').Take(2));
|
||||
|
||||
var skin = GetKefinDefaultSkin(ctx.PluginCacheDir);
|
||||
if (string.IsNullOrWhiteSpace(skin))
|
||||
return js;
|
||||
|
||||
string skinLower = skin.ToLowerInvariant();
|
||||
|
||||
js = EnsureCssLinked(js,
|
||||
$"plugin_cache/kefinTweaks/skins/css/{skinLower}-kefin.css");
|
||||
|
||||
js = EnsureCssLinked(js,
|
||||
$"plugin_cache/kefinTweaks/skins/css/{skinLower}-kefin-{majorMinor}.css");
|
||||
|
||||
Trace.WriteLine($"✓ KefinTweaks skin injected: {skin} ({majorMinor})");
|
||||
|
||||
return js;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Utility helpers (verbatim behavior)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
private static string EnsureCssLinked(string js, string href)
|
||||
{
|
||||
if (js.Contains(href, StringComparison.OrdinalIgnoreCase))
|
||||
return js;
|
||||
|
||||
return js + $@"
|
||||
(function () {{
|
||||
try {{
|
||||
var href = '{href}';
|
||||
if (document.querySelector('link[href=""' + href + '""]')) return;
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
console.log('🪼 KefinTweaks CSS injected:', href);
|
||||
}} catch (e) {{
|
||||
console.error('Failed to inject KefinTweaks CSS', e);
|
||||
}}
|
||||
}})();
|
||||
";
|
||||
}
|
||||
|
||||
private static string? GetKefinDefaultSkin(string pluginCacheDir)
|
||||
{
|
||||
try
|
||||
{
|
||||
string configPath = Path.Combine(
|
||||
pluginCacheDir,
|
||||
"kefinTweaks",
|
||||
"config",
|
||||
"config.json");
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
return null;
|
||||
|
||||
var json = File.ReadAllText(configPath);
|
||||
var match = RegexPatterns.PluginConfig.DefaultSkin.Match(json);
|
||||
|
||||
return match.Success ? match.Groups[1].Value : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
using Jellyfin2Samsung.Helpers.API;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Plugins.KefinTweaks;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// Responsible for:
|
||||
/// - Mapping installed plugins -> PluginMatrixEntry
|
||||
/// - Resolving plugin patch implementations (per-plugin behavior)
|
||||
/// - Download/transpile helpers used by patches
|
||||
/// </summary>
|
||||
public sealed class PluginManager
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly JellyfinApiClient _apiClient;
|
||||
|
||||
public PluginManager(HttpClient httpClient, JellyfinApiClient apiClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_apiClient = apiClient;
|
||||
}
|
||||
|
||||
public PluginMatrixEntry? FindPluginEntry(JellyfinPluginInfo plugin)
|
||||
{
|
||||
if (plugin?.Name == null) return null;
|
||||
|
||||
string pluginName = plugin.Name.ToLowerInvariant();
|
||||
|
||||
return PluginMatrix.Matrix.FirstOrDefault(entry =>
|
||||
pluginName.Contains(entry.Name, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
public IJellyfinPluginPatch? ResolvePatch(PluginMatrixEntry entry)
|
||||
{
|
||||
return entry.Name switch
|
||||
{
|
||||
"EditorsChoice" => new EditorsChoicePatch(),
|
||||
"Jellyfin Enhanced" => new JellyfinEnhancedPatch(),
|
||||
"Media Bar" => new MediaBarPatch(),
|
||||
"Home Screen Sections" => new HomeScreenSectionsPatch(),
|
||||
"KefinTweaks" => new KefinTweaksPatch(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public bool TryClassifyServerAsset(string url, out ServerAssetKind kind)
|
||||
{
|
||||
kind = ServerAssetKind.Unknown;
|
||||
|
||||
foreach (var rule in PluginMatrix.ServerAssetRules)
|
||||
{
|
||||
if (rule.match(url))
|
||||
{
|
||||
kind = rule.treatAs;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task DownloadExplicitFilesAsync(string serverUrl, string pluginCacheDir, PluginMatrixEntry entry)
|
||||
{
|
||||
if (entry?.ExplicitServerFiles == null || entry.ExplicitServerFiles.Count == 0)
|
||||
return;
|
||||
|
||||
Trace.WriteLine("▶ Downloading explicit Enhanced JS modules...");
|
||||
|
||||
foreach (var rel in entry.ExplicitServerFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = UrlHelper.CombineUrl(serverUrl, rel);
|
||||
Trace.WriteLine($" → Fetch: {url}");
|
||||
|
||||
string js = await _httpClient.GetStringAsync(url);
|
||||
|
||||
// Transpile to es2015 using esbuild; fallback is original JS.
|
||||
js = await EsbuildHelper.TranspileAsync(js, rel);
|
||||
|
||||
string relPath = rel.TrimStart('/');
|
||||
string outPath = Path.Combine(pluginCacheDir, relPath.Replace('/', Path.DirectorySeparatorChar));
|
||||
|
||||
string? directory = Path.GetDirectoryName(outPath);
|
||||
if (directory != null)
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
if (!Path.HasExtension(outPath) || !outPath.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
|
||||
outPath += ".js";
|
||||
|
||||
await File.WriteAllTextAsync(outPath, js, Encoding.UTF8);
|
||||
|
||||
string logPath = outPath.Replace(pluginCacheDir + Path.DirectorySeparatorChar, "plugin_cache/");
|
||||
Trace.WriteLine($" ✓ Saved {logPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($" ⚠ Failed Enhanced JS '{rel}': {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
public async Task<string?> DownloadAndTranspileAsync(string url, string cacheDir, string relPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Trace.WriteLine($"▶ Downloading plugin JS: {url}");
|
||||
|
||||
string js = await _httpClient.GetStringAsync(url);
|
||||
js = await EsbuildHelper.TranspileAsync(js, relPath);
|
||||
|
||||
string localPath = Path.Combine(cacheDir, relPath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(localPath)!);
|
||||
|
||||
await File.WriteAllTextAsync(localPath, js, Encoding.UTF8);
|
||||
return localPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ Plugin download failed: {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<byte[]?> DownloadBytesAsync(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _httpClient.GetByteArrayAsync(url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ DownloadBytes download failed: {url} {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> DownloadStringAsync(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _httpClient.GetStringAsync(url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ DownloadString failed: {url} {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<JellyfinPublicSystemInfo?> GetPublicSystemInfoAsync(string serverUrl)
|
||||
=> _apiClient.GetPublicSystemInfoAsync(serverUrl);
|
||||
|
||||
public JellyfinApiClient Api => _apiClient;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Plugins
|
||||
{
|
||||
public static class PluginMatrix
|
||||
{
|
||||
public static readonly List<PluginMatrixEntry> Matrix =
|
||||
[
|
||||
new PluginMatrixEntry
|
||||
{
|
||||
Name = "Jellyfin Enhanced",
|
||||
FallbackUrls = new(),
|
||||
ExplicitServerFiles = new List<string>
|
||||
{
|
||||
"/JellyfinEnhanced/script",
|
||||
"/JellyfinEnhanced/js/splashscreen.js",
|
||||
"/JellyfinEnhanced/js/reviews.js",
|
||||
"/JellyfinEnhanced/js/qualitytags.js",
|
||||
"/JellyfinEnhanced/js/plugin.js",
|
||||
"/JellyfinEnhanced/js/pausescreen.js",
|
||||
"/JellyfinEnhanced/js/migrate.js",
|
||||
"/JellyfinEnhanced/js/letterboxd-links.js",
|
||||
"/JellyfinEnhanced/js/languagetags.js",
|
||||
"/JellyfinEnhanced/js/genretags.js",
|
||||
"/JellyfinEnhanced/js/elsewhere.js",
|
||||
"/JellyfinEnhanced/js/arr-tag-links.js",
|
||||
"/JellyfinEnhanced/js/arr-links.js",
|
||||
"/JellyfinEnhanced/js/enhanced/config.js",
|
||||
"/JellyfinEnhanced/js/enhanced/events.js",
|
||||
"/JellyfinEnhanced/js/enhanced/features.js",
|
||||
"/JellyfinEnhanced/js/enhanced/helpers.js",
|
||||
"/JellyfinEnhanced/js/enhanced/playback.js",
|
||||
"/JellyfinEnhanced/js/enhanced/subtitles.js",
|
||||
"/JellyfinEnhanced/js/enhanced/themer.js",
|
||||
"/JellyfinEnhanced/js/enhanced/ui.js",
|
||||
"/JellyfinEnhanced/js/jellyseerr/api.js",
|
||||
"/JellyfinEnhanced/js/jellyseerr/jellyseerr.js",
|
||||
"/JellyfinEnhanced/js/jellyseerr/modal.js",
|
||||
"/JellyfinEnhanced/js/jellyseerr/ui.js"
|
||||
}
|
||||
},
|
||||
new PluginMatrixEntry
|
||||
{
|
||||
Name = "Media Bar",
|
||||
FallbackUrls = new List<string>
|
||||
{
|
||||
"https://cdn.jsdelivr.net/gh/IAmParadox27/jellyfin-plugin-media-bar@main/slideshowpure.js"
|
||||
},
|
||||
UseBabel = true
|
||||
},
|
||||
new PluginMatrixEntry
|
||||
{
|
||||
Name = "EditorsChoice",
|
||||
FallbackUrls = new List<string>
|
||||
{
|
||||
"https://raw.githubusercontent.com/lachlandcp/jellyfin-editors-choice-plugin/refs/heads/main/EditorsChoicePlugin/Api/client.js"
|
||||
},
|
||||
UseBabel = false
|
||||
},
|
||||
new PluginMatrixEntry
|
||||
{
|
||||
Name = "Home Screen Sections",
|
||||
FallbackUrls = new List<string>
|
||||
{
|
||||
"https://raw.githubusercontent.com/IAmParadox27/jellyfin-plugin-home-sections/main/src/Jellyfin.Plugin.HomeScreenSections/Inject/HomeScreenSections.js"
|
||||
},
|
||||
UseBabel = true
|
||||
},
|
||||
new PluginMatrixEntry
|
||||
{
|
||||
Name = "Plugin Pages",
|
||||
FallbackUrls = new(),
|
||||
UseBabel = true
|
||||
},
|
||||
new PluginMatrixEntry
|
||||
{
|
||||
Name = "KefinTweaks",
|
||||
FallbackUrls = new List<string>
|
||||
{
|
||||
"https://cdn.jsdelivr.net/gh/ranaldsgift/KefinTweaks@latest/kefinTweaks-plugin.js"
|
||||
},
|
||||
RawRoot = "https://raw.githubusercontent.com/ranaldsgift/KefinTweaks/v0.4.5/",
|
||||
UseBabel = true
|
||||
}
|
||||
];
|
||||
public static readonly List<ServerAssetRule> ServerAssetRules =
|
||||
[
|
||||
new ServerAssetRule(
|
||||
pluginName: "GenericPluginAsset",
|
||||
match: url => url.Contains("/plugins/", StringComparison.OrdinalIgnoreCase),
|
||||
treatAs: ServerAssetKind.PluginAsset),
|
||||
|
||||
new ServerAssetRule("EditorsChoice", url => url.Contains("editorschoice", StringComparison.OrdinalIgnoreCase), ServerAssetKind.PluginAsset),
|
||||
new ServerAssetRule("KefinTweaks", url => url.Contains("kefin", StringComparison.OrdinalIgnoreCase), ServerAssetKind.PluginAsset),
|
||||
new ServerAssetRule("Media Bar", url => url.Contains("mediabar", StringComparison.OrdinalIgnoreCase), ServerAssetKind.PluginAsset),
|
||||
new ServerAssetRule("Home Screen Sections", url => url.Contains("homescreensections", StringComparison.OrdinalIgnoreCase), ServerAssetKind.PluginAsset),
|
||||
new ServerAssetRule("Jellyfin Enhanced", url => url.Contains("jellyfinenhanced", StringComparison.OrdinalIgnoreCase) || url.Contains("/JellyfinEnhanced/", StringComparison.OrdinalIgnoreCase), ServerAssetKind.PluginAsset),
|
||||
];
|
||||
|
||||
public static List<PluginMatrixEntry> GetMatrix() => Matrix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Plugins.EditorsChoice;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Plugins
|
||||
{
|
||||
public sealed class PluginPatchContext
|
||||
{
|
||||
public PluginPatchContext(
|
||||
PackageWorkspace workspace,
|
||||
string serverUrl,
|
||||
string pluginCacheDir,
|
||||
PluginMatrixEntry matrixEntry,
|
||||
PluginManager pluginManager,
|
||||
StringBuilder cssBuilder,
|
||||
StringBuilder headJsBuilder,
|
||||
StringBuilder bodyJsBuilder,
|
||||
System.Collections.Generic.HashSet<string> injectedScripts,
|
||||
System.Collections.Generic.HashSet<string> injectedStyles)
|
||||
{
|
||||
Workspace = workspace;
|
||||
ServerUrl = serverUrl;
|
||||
PluginCacheDir = pluginCacheDir;
|
||||
MatrixEntry = matrixEntry;
|
||||
PluginManager = pluginManager;
|
||||
CssBuilder = cssBuilder;
|
||||
HeadJsBuilder = headJsBuilder;
|
||||
BodyJsBuilder = bodyJsBuilder;
|
||||
InjectedScripts = injectedScripts;
|
||||
InjectedStyles = injectedStyles;
|
||||
}
|
||||
|
||||
public PackageWorkspace Workspace { get; }
|
||||
public string ServerUrl { get; }
|
||||
public string PluginCacheDir { get; }
|
||||
public PluginMatrixEntry MatrixEntry { get; }
|
||||
public PluginManager PluginManager { get; }
|
||||
|
||||
public StringBuilder CssBuilder { get; }
|
||||
public StringBuilder HeadJsBuilder { get; }
|
||||
public StringBuilder BodyJsBuilder { get; }
|
||||
|
||||
// For dedup / consistent injection
|
||||
public System.Collections.Generic.HashSet<string> InjectedScripts { get; }
|
||||
public System.Collections.Generic.HashSet<string> InjectedStyles { get; }
|
||||
|
||||
public void InjectScript(string scriptTag, string src)
|
||||
{
|
||||
if (InjectedScripts.Add(src))
|
||||
BodyJsBuilder.AppendLine(scriptTag);
|
||||
}
|
||||
|
||||
public void InjectStyle(string href)
|
||||
{
|
||||
if (InjectedStyles.Add(href))
|
||||
CssBuilder.AppendLine($"<link rel=\"stylesheet\" href=\"{href}\" />");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// EditorsChoice
|
||||
// ------------------------
|
||||
public sealed class EditorsChoicePatch : IJellyfinPluginPatch
|
||||
{
|
||||
public async Task ApplyAsync(PluginPatchContext ctx)
|
||||
{
|
||||
var entry = ctx.MatrixEntry;
|
||||
|
||||
foreach (var url in entry.FallbackUrls)
|
||||
{
|
||||
string cleanName = RegexPatterns.PluginName.NonAlphanumeric.Replace(entry.Name.ToLowerInvariant(), "");
|
||||
string fileName = Path.GetFileName(new Uri(url).AbsolutePath);
|
||||
string relPath = Path.Combine(cleanName, fileName);
|
||||
|
||||
var local = await ctx.PluginManager.DownloadAndTranspileAsync(url, ctx.PluginCacheDir, relPath);
|
||||
if (local == null) continue;
|
||||
|
||||
// Apply EditorsChoice patch (your existing patch class)
|
||||
var js = await File.ReadAllTextAsync(local, Encoding.UTF8);
|
||||
js = new PatchEditorsChoice().ApplyAsync(js);
|
||||
await File.WriteAllTextAsync(local, js, Encoding.UTF8);
|
||||
|
||||
string injectedSrc = $"plugin_cache/{cleanName}/{fileName}";
|
||||
ctx.InjectScript($"<script defer src=\"{injectedSrc}\"></script>", injectedSrc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// Jellyfin Enhanced (ExplicitServerFiles)
|
||||
// ------------------------
|
||||
public sealed class JellyfinEnhancedPatch : IJellyfinPluginPatch
|
||||
{
|
||||
public async Task ApplyAsync(PluginPatchContext ctx)
|
||||
{
|
||||
var entry = ctx.MatrixEntry;
|
||||
|
||||
// Download all explicit server files into plugin_cache preserving structure
|
||||
await ctx.PluginManager.DownloadExplicitFilesAsync(ctx.ServerUrl, ctx.PluginCacheDir, entry);
|
||||
|
||||
// Inject main script last
|
||||
// Your explicit list contains "/JellyfinEnhanced/script" -> stored as plugin_cache/JellyfinEnhanced/script.js
|
||||
var main = Path.Combine(ctx.PluginCacheDir, "JellyfinEnhanced", "script.js");
|
||||
if (File.Exists(main))
|
||||
{
|
||||
await PatchEnhancedMainScript(main);
|
||||
|
||||
var outSrc = "plugin_cache/JellyfinEnhanced/script.js";
|
||||
ctx.InjectScript($"<script src=\"{outSrc}\"></script>", outSrc);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task PatchEnhancedMainScript(string scriptPath)
|
||||
{
|
||||
string original = await File.ReadAllTextAsync(scriptPath);
|
||||
|
||||
string patch = @"
|
||||
// ---- J2S SCRIPT PATCH: FORCE LOCAL ENHANCED MODULE LOADING ----
|
||||
(function () {
|
||||
|
||||
function rewriteEnhancedUrl(url) {
|
||||
try {
|
||||
if (typeof url !== 'string') return url;
|
||||
|
||||
var base = url.split('?')[0];
|
||||
|
||||
if (base.endsWith('.js') && base.indexOf('/JellyfinEnhanced/') !== -1) {
|
||||
var idx = base.indexOf('/JellyfinEnhanced/');
|
||||
var sub = base.substring(idx + '/JellyfinEnhanced/'.length);
|
||||
return 'plugin_cache/JellyfinEnhanced/' + sub;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('J2S rewriteEnhancedUrl failed', e);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
var _createElement = document.createElement;
|
||||
document.createElement = function (tag) {
|
||||
var el = _createElement.call(document, tag);
|
||||
|
||||
if (tag && tag.toLowerCase() === 'script') {
|
||||
var _setAttribute = el.setAttribute;
|
||||
|
||||
el.setAttribute = function (name, value) {
|
||||
if (name === 'src') {
|
||||
value = rewriteEnhancedUrl(value);
|
||||
}
|
||||
return _setAttribute.call(el, name, value);
|
||||
};
|
||||
|
||||
Object.defineProperty(el, 'src', {
|
||||
configurable: true,
|
||||
get: function () { return el.getAttribute('src'); },
|
||||
set: function (value) {
|
||||
value = rewriteEnhancedUrl(value);
|
||||
_setAttribute.call(el, 'src', value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
if (typeof window.fetch === 'function') {
|
||||
var _fetch = window.fetch;
|
||||
window.fetch = function (resource, init) {
|
||||
try {
|
||||
if (typeof resource === 'string') resource = rewriteEnhancedUrl(resource);
|
||||
} catch (e) {
|
||||
console.error('FETCH rewrite failed', e);
|
||||
}
|
||||
return _fetch.call(this, resource, init);
|
||||
};
|
||||
}
|
||||
|
||||
console.log('🪼 J2S: Enhanced loader patched to use plugin_cache for Enhanced JS modules');
|
||||
})();
|
||||
";
|
||||
await File.WriteAllTextAsync(scriptPath, patch + "\n\n" + original);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// Media Bar (example: CSS + fallback JS)
|
||||
// ------------------------
|
||||
public sealed class MediaBarPatch : IJellyfinPluginPatch
|
||||
{
|
||||
public async Task ApplyAsync(PluginPatchContext ctx)
|
||||
{
|
||||
// CSS
|
||||
const string cssUrl = "https://cdn.jsdelivr.net/gh/IAmParadox27/jellyfin-plugin-media-bar@main/slideshowpure.css";
|
||||
var bytes = await ctx.PluginManager.DownloadBytesAsync(cssUrl);
|
||||
if (bytes != null)
|
||||
{
|
||||
var dir = Path.Combine(ctx.PluginCacheDir, "mediabar");
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
var local = Path.Combine(dir, "slideshowpure.css");
|
||||
await File.WriteAllBytesAsync(local, bytes);
|
||||
|
||||
ctx.InjectStyle("plugin_cache/mediabar/slideshowpure.css");
|
||||
}
|
||||
|
||||
// JS (from matrix fallback)
|
||||
var entry = ctx.MatrixEntry;
|
||||
foreach (var url in entry.FallbackUrls)
|
||||
{
|
||||
var clean = "mediabar";
|
||||
var fileName = Path.GetFileName(new Uri(url).AbsolutePath);
|
||||
var relPath = Path.Combine(clean, fileName);
|
||||
|
||||
var local = await ctx.PluginManager.DownloadAndTranspileAsync(url, ctx.PluginCacheDir, relPath);
|
||||
if (local == null) continue;
|
||||
|
||||
var injectedSrc = $"plugin_cache/{clean}/{fileName}";
|
||||
ctx.InjectScript($"<script src=\"{injectedSrc}\"></script>", injectedSrc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// Home Screen Sections (CSS + fallback JS)
|
||||
// ------------------------
|
||||
public sealed class HomeScreenSectionsPatch : IJellyfinPluginPatch
|
||||
{
|
||||
public async Task ApplyAsync(PluginPatchContext ctx)
|
||||
{
|
||||
const string cssUrl =
|
||||
"https://raw.githubusercontent.com/IAmParadox27/jellyfin-plugin-home-sections/main/src/Jellyfin.Plugin.HomeScreenSections/Inject/HomeScreenSections.css";
|
||||
|
||||
var bytes = await ctx.PluginManager.DownloadBytesAsync(cssUrl);
|
||||
if (bytes != null)
|
||||
{
|
||||
var dir = Path.Combine(ctx.PluginCacheDir, "homescreensections");
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
var local = Path.Combine(dir, "HomeScreenSections.css");
|
||||
await File.WriteAllBytesAsync(local, bytes);
|
||||
|
||||
ctx.InjectStyle("plugin_cache/homescreensections/HomeScreenSections.css");
|
||||
}
|
||||
|
||||
var entry = ctx.MatrixEntry;
|
||||
foreach (var url in entry.FallbackUrls)
|
||||
{
|
||||
var clean = "homescreensections";
|
||||
var fileName = Path.GetFileName(new Uri(url).AbsolutePath);
|
||||
var relPath = Path.Combine(clean, fileName);
|
||||
|
||||
var local = await ctx.PluginManager.DownloadAndTranspileAsync(url, ctx.PluginCacheDir, relPath);
|
||||
if (local == null) continue;
|
||||
|
||||
var injectedSrc = $"plugin_cache/{clean}/{fileName}";
|
||||
ctx.InjectScript($"<script src=\"{injectedSrc}\"></script>", injectedSrc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class JellyfinHelper
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ProcessHelper _processHelper;
|
||||
public JellyfinHelper(
|
||||
HttpClient httpClient,
|
||||
ProcessHelper processHelper)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_processHelper = processHelper;
|
||||
}
|
||||
|
||||
public static bool IsValidJellyfinConfiguration()
|
||||
{
|
||||
return !string.IsNullOrEmpty(AppSettings.Default.JellyfinIP) &&
|
||||
!string.IsNullOrEmpty(AppSettings.Default.JellyfinApiKey) &&
|
||||
AppSettings.Default.JellyfinApiKey.Length == 32 &&
|
||||
IsValidUrl($"http://{AppSettings.Default.JellyfinIP}/Users");
|
||||
}
|
||||
|
||||
public static bool IsValidUrl(string url)
|
||||
{
|
||||
return Uri.TryCreate(url, UriKind.Absolute, out var uriResult)
|
||||
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
|
||||
}
|
||||
|
||||
public async Task<List<JellyfinAuth>> LoadJellyfinUsersAsync()
|
||||
{
|
||||
var users = new List<JellyfinAuth>();
|
||||
|
||||
if (!IsValidJellyfinConfiguration())
|
||||
{
|
||||
Debug.WriteLine("Invalid Jellyfin configuration - skipping user load");
|
||||
return users;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("SamsungJellyfinInstaller/1.0");
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization", $"MediaBrowser Token=\"{AppSettings.Default.JellyfinApiKey}\"");
|
||||
|
||||
var url = $"http://{AppSettings.Default.JellyfinIP}/Users";
|
||||
Debug.WriteLine($"Attempting to load users from: {url}");
|
||||
|
||||
using var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var jellyfinUsers = JsonSerializer.Deserialize<List<JellyfinAuth>>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (jellyfinUsers != null && jellyfinUsers.Any())
|
||||
{
|
||||
users.AddRange(jellyfinUsers);
|
||||
|
||||
if (users.Count > 1)
|
||||
{
|
||||
users.Add(new JellyfinAuth
|
||||
{
|
||||
Id = "everyone",
|
||||
Name = "Everyone"
|
||||
});
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Successfully loaded {users.Count} users");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("No users found in response");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"Failed to load users - Status: {response.StatusCode}, Reason: {response.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Debug.WriteLine($"HTTP error loading Jellyfin users: {ex.Message}");
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
Debug.WriteLine($"Request timeout loading Jellyfin users: {ex.Message}");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Debug.WriteLine($"JSON parsing error: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Unexpected error loading Jellyfin users: {ex.Message}");
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
public async Task<InstallResult> ApplyJellyfinConfigAsync(
|
||||
string packagePath,
|
||||
string[] userIds)
|
||||
{
|
||||
string? tempDir = null;
|
||||
string? tempPackage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var baseDir = Path.GetDirectoryName(packagePath)
|
||||
?? throw new InvalidOperationException("Invalid package path");
|
||||
|
||||
tempDir = Path.Combine(baseDir, $"JellyTemp_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
ZipFile.ExtractToDirectory(packagePath, tempDir);
|
||||
|
||||
if (AppSettings.Default.ConfigUpdateMode.Contains("Server") ||
|
||||
AppSettings.Default.ConfigUpdateMode.Contains("All"))
|
||||
{
|
||||
Debug.WriteLine(tempDir);
|
||||
UpdateMultiServerConfig(tempDir);
|
||||
}
|
||||
|
||||
if (AppSettings.Default.ConfigUpdateMode.Contains("Browser") ||
|
||||
AppSettings.Default.ConfigUpdateMode.Contains("All"))
|
||||
{
|
||||
await InjectUserSettingsScriptAsync(tempDir, userIds);
|
||||
}
|
||||
|
||||
tempPackage = Path.Combine(baseDir, $"{Path.GetFileNameWithoutExtension(packagePath)}_mod.wgt");
|
||||
if (File.Exists(tempPackage)) File.Delete(tempPackage);
|
||||
ZipFile.CreateFromDirectory(tempDir, tempPackage);
|
||||
|
||||
File.Delete(packagePath);
|
||||
File.Move(tempPackage, packagePath);
|
||||
|
||||
|
||||
return InstallResult.SuccessResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return InstallResult.FailureResult($"Error modifying Jellyfin package: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
Directory.Delete(tempDir, true);
|
||||
|
||||
if (tempPackage != null && File.Exists(tempPackage))
|
||||
File.Delete(tempPackage);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task UpdateMultiServerConfig(string tempDirectory)
|
||||
{
|
||||
string configPath = Path.Combine(tempDirectory, "www", "config.json");
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
throw new FileNotFoundException("Jellyfin config.json not found", configPath);
|
||||
|
||||
string jsonText = await File.ReadAllTextAsync(configPath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(jsonText))
|
||||
throw new InvalidDataException("config.json is empty or invalid");
|
||||
|
||||
var config = JsonNode.Parse(jsonText) ?? throw new JsonException("Failed to parse config.json");
|
||||
config["multiserver"] = false;
|
||||
|
||||
string serverUrl = $"http://{AppSettings.Default.JellyfinIP}";
|
||||
config["servers"] = new JsonArray { JsonValue.Create(serverUrl) };
|
||||
|
||||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||
await File.WriteAllTextAsync(configPath, config.ToJsonString(options));
|
||||
}
|
||||
public static async Task InjectUserSettingsScriptAsync(string tempDirectory, string[] userIds)
|
||||
{
|
||||
if (userIds == null || userIds.Length == 0)
|
||||
return;
|
||||
|
||||
string indexPath = Path.Combine(tempDirectory, "index.html");
|
||||
|
||||
if (!File.Exists(indexPath))
|
||||
throw new FileNotFoundException("index.html not found", indexPath);
|
||||
|
||||
string htmlContent = await File.ReadAllTextAsync(indexPath);
|
||||
|
||||
const string injectionMarker = "<!-- SAMSUNG_JELLYFIN_AUTO_INJECTED -->";
|
||||
if (htmlContent.Contains(injectionMarker))
|
||||
return;
|
||||
|
||||
string BoolToJsString(bool value) => value.ToString().ToLower();
|
||||
|
||||
var scriptBuilder = new StringBuilder();
|
||||
scriptBuilder.AppendLine(injectionMarker);
|
||||
scriptBuilder.AppendLine("<script>");
|
||||
scriptBuilder.AppendLine("(function() {");
|
||||
|
||||
foreach (string userId in userIds.Where(u => !string.IsNullOrWhiteSpace(u)))
|
||||
{
|
||||
scriptBuilder.AppendLine($" // Settings for user: {userId}");
|
||||
scriptBuilder.AppendLine($" var userId = '{userId}';");
|
||||
scriptBuilder.AppendLine();
|
||||
scriptBuilder.AppendLine(" if (localStorage.getItem('samsung-jellyfin-injected-' + userId)) return;");
|
||||
scriptBuilder.AppendLine();
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-appTheme', '{AppSettings.Default.SelectedTheme ?? "dark"}');");
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-enableBackdrops', '{BoolToJsString(AppSettings.Default.EnableBackdrops)}');");
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-enableThemeSongs', '{BoolToJsString(AppSettings.Default.EnableThemeSongs)}');");
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-enableThemeVideos', '{BoolToJsString(AppSettings.Default.EnableThemeVideos)}');");
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-backdropScreensaver', '{BoolToJsString(AppSettings.Default.BackdropScreensaver)}');");
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-detailsBanner', '{BoolToJsString(AppSettings.Default.DetailsBanner)}');");
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-cinemaMode', '{BoolToJsString(AppSettings.Default.CinemaMode)}');");
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-nextUpEnabled', '{BoolToJsString(AppSettings.Default.NextUpEnabled)}');");
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-enableExternalVideoPlayers', '{BoolToJsString(AppSettings.Default.EnableExternalVideoPlayers)}');");
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-skipIntros', '{BoolToJsString(AppSettings.Default.SkipIntros)}');");
|
||||
scriptBuilder.AppendLine();
|
||||
|
||||
// Language preferences
|
||||
if (!string.IsNullOrWhiteSpace(AppSettings.Default.AudioLanguagePreference))
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-audioLanguagePreference', '{AppSettings.Default.AudioLanguagePreference}');");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(AppSettings.Default.SubtitleLanguagePreference))
|
||||
scriptBuilder.AppendLine($" localStorage.setItem(userId + '-subtitleLanguagePreference', '{AppSettings.Default.SubtitleLanguagePreference}');");
|
||||
|
||||
// Mark injected
|
||||
scriptBuilder.AppendLine(" localStorage.setItem('samsung-jellyfin-injected-' + userId, 'true');");
|
||||
scriptBuilder.AppendLine();
|
||||
}
|
||||
|
||||
scriptBuilder.AppendLine("})();");
|
||||
scriptBuilder.AppendLine("</script>");
|
||||
|
||||
htmlContent = htmlContent.Replace("<head>", "<head>" + scriptBuilder.ToString());
|
||||
await File.WriteAllTextAsync(indexPath, htmlContent);
|
||||
}
|
||||
public async Task UpdateJellyfinUsersAsync(string[] userIds)
|
||||
{
|
||||
if (userIds == null || userIds.Length == 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization", $"MediaBrowser Token=\"{AppSettings.Default.JellyfinApiKey}\"");
|
||||
|
||||
foreach (string userId in userIds.Where(u => !string.IsNullOrWhiteSpace(u)))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Fetch user info
|
||||
var getUserResponse = await _httpClient.GetAsync($"http://{AppSettings.Default.JellyfinIP}/Users/{userId}");
|
||||
getUserResponse.EnsureSuccessStatusCode();
|
||||
var userJson = await getUserResponse.Content.ReadAsStringAsync();
|
||||
|
||||
var userNode = JsonNode.Parse(userJson) ?? throw new JsonException("Failed to parse user JSON");
|
||||
|
||||
// Update auto-login setting
|
||||
userNode["EnableAutoLogin"] = AppSettings.Default.UserAutoLogin;
|
||||
|
||||
var userContent = new StringContent(
|
||||
userNode.ToJsonString(new JsonSerializerOptions { WriteIndented = true }),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
using (userContent)
|
||||
{
|
||||
var userResponse = await _httpClient.PostAsync($"http://{AppSettings.Default.JellyfinIP}/Users?userId={userId}", userContent);
|
||||
userResponse.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// Update additional user configurations
|
||||
var userConfig = new
|
||||
{
|
||||
AppSettings.Default.PlayDefaultAudioTrack,
|
||||
AppSettings.Default.SubtitleLanguagePreference,
|
||||
SubtitleMode = AppSettings.Default.SelectedSubtitleMode,
|
||||
AppSettings.Default.RememberAudioSelections,
|
||||
AppSettings.Default.RememberSubtitleSelections,
|
||||
EnableNextEpisodeAutoPlay = AppSettings.Default.AutoPlayNextEpisode,
|
||||
};
|
||||
|
||||
var configJson = JsonSerializer.Serialize(userConfig, new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
using var configContent = new StringContent(configJson, Encoding.UTF8, "application/json");
|
||||
var configResponse = await _httpClient.PostAsync($"http://{AppSettings.Default.JellyfinIP}/Users/Configuration?userId={userId}", configContent);
|
||||
configResponse.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception userEx)
|
||||
{
|
||||
Debug.WriteLine($"Failed to update configuration for user {userId}: {userEx.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"General error updating user configurations: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class ProcessHelper
|
||||
{
|
||||
public static void KillSdbServers()
|
||||
{
|
||||
try
|
||||
{
|
||||
Process[] sdbProcesses = Process.GetProcessesByName("sdb");
|
||||
|
||||
if (sdbProcesses.Length == 0)
|
||||
return;
|
||||
|
||||
foreach (Process proc in sdbProcesses)
|
||||
{
|
||||
proc.Kill();
|
||||
proc.WaitForExit();
|
||||
Debug.WriteLine($"Killed SDB {proc.Id} - {proc.ProcessName}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to stop SDB server: {ex.Message}");
|
||||
}
|
||||
}
|
||||
public Task<ProcessResult> RunCommandCmdAsync(string fileName, string arguments, string? workingDirectory = null)
|
||||
{
|
||||
var cmdArgs = $"/c \"\"{fileName}\" {arguments}\"";
|
||||
return RunCommandAsync("cmd.exe", cmdArgs, workingDirectory);
|
||||
}
|
||||
public async Task<ProcessResult> RunCommandAsync(string fileName, string arguments, string? workingDirectory = null)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<ProcessResult>();
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = workingDirectory ?? ""
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
var output = new StringBuilder();
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data != null) output.AppendLine(e.Data); };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data != null) output.AppendLine(e.Data); };
|
||||
|
||||
process.Exited += (_, _) =>
|
||||
{
|
||||
tcs.SetResult(new ProcessResult
|
||||
{
|
||||
ExitCode = process.ExitCode,
|
||||
Output = output.ToString()
|
||||
});
|
||||
};
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
return await tcs.Task;
|
||||
}
|
||||
public async Task MakeExecutableAsync(string filePath)
|
||||
{
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use chmod to make the file executable
|
||||
var output = await RunCommandAsync("chmod", $"+x \"{filePath}\"");
|
||||
Debug.WriteLine($"Set executable permissions on {filePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error setting executable permissions: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers.Tizen.Certificate
|
||||
{
|
||||
public class CertificateHelper
|
||||
{
|
||||
@@ -31,25 +31,25 @@ namespace Jellyfin2Samsung.Helpers
|
||||
if (!Directory.Exists(certificateFolders))
|
||||
return certificates;
|
||||
|
||||
|
||||
var p12Files = Directory.GetFiles(
|
||||
certificateFolders,
|
||||
"author.p12",
|
||||
SearchOption.AllDirectories);
|
||||
|
||||
foreach(var p12Path in p12Files)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(p12Path);
|
||||
if (directory == null)
|
||||
continue;
|
||||
var p12Files = Directory.GetFiles(
|
||||
certificateFolders,
|
||||
"author.p12",
|
||||
SearchOption.AllDirectories);
|
||||
|
||||
var passwordPath = Path.Combine(directory, "password.txt");
|
||||
if (!File.Exists(passwordPath))
|
||||
continue;
|
||||
foreach (var p12Path in p12Files)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(p12Path);
|
||||
if (directory == null)
|
||||
continue;
|
||||
|
||||
var password = File.ReadAllText(passwordPath).Trim();
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
continue;
|
||||
var passwordPath = Path.Combine(directory, "password.txt");
|
||||
if (!File.Exists(passwordPath))
|
||||
continue;
|
||||
|
||||
var password = File.ReadAllText(passwordPath).Trim();
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
continue;
|
||||
try
|
||||
{
|
||||
var cert = new X509Certificate2(
|
||||
@@ -105,7 +105,7 @@ namespace Jellyfin2Samsung.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to load certificate '{p12Path}': {ex.Message}");
|
||||
Trace.WriteLine($"Failed to load certificate '{p12Path}': {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers.Tizen.Certificate
|
||||
{
|
||||
public class CipherUtil
|
||||
{
|
||||
@@ -0,0 +1,67 @@
|
||||
using Jellyfin2Samsung.Helpers.API;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Tizen.Devices
|
||||
{
|
||||
public class DeviceHelper
|
||||
{
|
||||
private readonly INetworkService _networkService;
|
||||
private readonly TizenApiClient _tizenApiClient;
|
||||
|
||||
public DeviceHelper(
|
||||
INetworkService networkService,
|
||||
TizenApiClient tizenApiClient)
|
||||
{
|
||||
_networkService = networkService;
|
||||
_tizenApiClient = tizenApiClient;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<List<NetworkDevice>> ScanForDevicesAsync(CancellationToken cancellationToken = default, bool virtualScan = false)
|
||||
{
|
||||
var devices = new List<NetworkDevice>();
|
||||
var networkDevices = await _networkService.GetLocalTizenAddresses(cancellationToken, virtualScan);
|
||||
|
||||
foreach (NetworkDevice device in networkDevices)
|
||||
{
|
||||
// Check for cancellation before processing each device
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (await _networkService.IsPortOpenAsync(device.IpAddress, 8001, cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
var samsungDevice = await _tizenApiClient.GetDeveloperInfoAsync(device);
|
||||
if (!string.IsNullOrEmpty(samsungDevice.DeviceName))
|
||||
devices.Add(samsungDevice);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Trace.WriteLine($"Failed to get developer info for device at {device.IpAddress}.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
device.ModelName = device.ModelName;
|
||||
device.Manufacturer = device.Manufacturer;
|
||||
device.DeveloperMode = "1";
|
||||
device.DeveloperIP = string.Empty;
|
||||
|
||||
devices.Add(device);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Jellyfin2Samsung-CrossOS/Interfaces/IJellyfinPluginPatch.cs
Normal file
12
Jellyfin2Samsung-CrossOS/Interfaces/IJellyfinPluginPatch.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Plugins;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Interfaces
|
||||
{
|
||||
public interface IJellyfinPluginPatch
|
||||
{
|
||||
//string PluginName { get; }
|
||||
Task ApplyAsync(PluginPatchContext ctx);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,5 +15,7 @@ namespace Jellyfin2Samsung.Interfaces
|
||||
string GetLocalIPAddress();
|
||||
string InvertIPAddress(string ipAddress);
|
||||
Task<string?> GetManufacturerFromIp(string ipAddress);
|
||||
bool IsDifferentSubnet(string ip1, string ip2);
|
||||
Task<IReadOnlyList<NetworkInterfaceOption>> GetNetworkInterfaceOptionsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
12
Jellyfin2Samsung-CrossOS/Interfaces/IThemeService.cs
Normal file
12
Jellyfin2Samsung-CrossOS/Interfaces/IThemeService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin2Samsung.Interfaces
|
||||
{
|
||||
public interface IThemeService
|
||||
{
|
||||
bool IsDarkMode { get; }
|
||||
event EventHandler<bool>? ThemeChanged;
|
||||
void SetTheme(bool isDarkMode);
|
||||
void ApplyTheme();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Interfaces
|
||||
@@ -9,6 +11,6 @@ namespace Jellyfin2Samsung.Interfaces
|
||||
Task<string> GetTvNameAsync(string tvIpAddress);
|
||||
Task<string> EnsureTizenSdbAvailable();
|
||||
Task<string> DownloadPackageAsync(string downloadUrl);
|
||||
Task<InstallResult> InstallPackageAsync(string packageUrl, string tvIpAddress, ProgressCallback? progress = null);
|
||||
Task<InstallResult> InstallPackageAsync(string packageUrl, string tvIpAddress, CancellationToken cancellationToken, ProgressCallback? progress = null, Action? onSamsungLoginStarted = null);
|
||||
}
|
||||
}
|
||||
|
||||
62
Jellyfin2Samsung-CrossOS/Interfaces/IUpdateDialogService.cs
Normal file
62
Jellyfin2Samsung-CrossOS/Interfaces/IUpdateDialogService.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the user's choice in the update dialog.
|
||||
/// </summary>
|
||||
public enum UpdateDialogChoice
|
||||
{
|
||||
/// <summary>
|
||||
/// User cancelled or closed the dialog.
|
||||
/// </summary>
|
||||
Cancel,
|
||||
|
||||
/// <summary>
|
||||
/// User chose to open the releases page manually.
|
||||
/// </summary>
|
||||
Manual,
|
||||
|
||||
/// <summary>
|
||||
/// User chose to download and install the update automatically.
|
||||
/// </summary>
|
||||
Automatic,
|
||||
|
||||
/// <summary>
|
||||
/// User chose to skip this update.
|
||||
/// </summary>
|
||||
Skip
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for showing update-related dialogs.
|
||||
/// </summary>
|
||||
public interface IUpdateDialogService
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the update available dialog with options for manual or automatic update.
|
||||
/// </summary>
|
||||
/// <param name="updateInfo">Information about the available update.</param>
|
||||
/// <returns>The user's choice.</returns>
|
||||
Task<UpdateDialogChoice> ShowUpdateAvailableDialogAsync(UpdateCheckResult updateInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a progress dialog while downloading the update.
|
||||
/// </summary>
|
||||
/// <param name="progress">Progress reporter (0-100).</param>
|
||||
/// <returns>True if download completed, false if cancelled.</returns>
|
||||
Task<bool> ShowDownloadProgressAsync(System.IProgress<int> progress);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a message that the update is being applied and the app will restart.
|
||||
/// </summary>
|
||||
Task ShowApplyingUpdateMessageAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Shows an error message related to the update process.
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">The error message to display.</param>
|
||||
Task ShowUpdateErrorAsync(string errorMessage);
|
||||
}
|
||||
}
|
||||
56
Jellyfin2Samsung-CrossOS/Interfaces/IUpdaterService.cs
Normal file
56
Jellyfin2Samsung-CrossOS/Interfaces/IUpdaterService.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for checking and applying application updates via GitHub releases.
|
||||
/// Uses the Atom feed endpoint to avoid API rate limiting.
|
||||
/// </summary>
|
||||
public interface IUpdaterService
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a newer version of the application is available.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>Update check result containing version information and download URLs.</returns>
|
||||
Task<UpdateCheckResult> CheckForUpdateAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the latest release to a temporary location.
|
||||
/// </summary>
|
||||
/// <param name="downloadUrl">The URL to download the release from.</param>
|
||||
/// <param name="progress">Progress callback reporting download percentage (0-100).</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>Path to the downloaded file.</returns>
|
||||
Task<string> DownloadUpdateAsync(
|
||||
string downloadUrl,
|
||||
IProgress<int>? progress = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the downloaded update by extracting, replacing files, and scheduling a restart.
|
||||
/// </summary>
|
||||
/// <param name="downloadedFilePath">Path to the downloaded update archive.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>True if the update was successfully prepared and app should restart.</returns>
|
||||
Task<bool> ApplyUpdateAsync(string downloadedFilePath, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the GitHub releases page in the default browser.
|
||||
/// </summary>
|
||||
void OpenReleasesPage();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL of the GitHub releases page.
|
||||
/// </summary>
|
||||
string ReleasesPageUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current application version.
|
||||
/// </summary>
|
||||
string CurrentVersion { get; }
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,13 @@
|
||||
<LinuxDesktopFileName>jellyfin2samsung.desktop</LinuxDesktopFileName>
|
||||
<PackageId>Jellyfin2Samsung</PackageId>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
@@ -72,7 +79,9 @@
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Fleck" Version="1.2.0" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="2.4.0" />
|
||||
<PackageReference Include="Jint" Version="4.4.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.3.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
@@ -83,4 +92,17 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Assets\TizenSDB\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
||||
<AvaloniaResource Update="Assets\esbuild\linux-x64\esbuild">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AvaloniaResource>
|
||||
<AvaloniaResource Update="Assets\esbuild\macos-x64\esbuild">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AvaloniaResource>
|
||||
<AvaloniaResource Update="Assets\esbuild\win-x64\esbuild.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AvaloniaResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
11
Jellyfin2Samsung-CrossOS/Models/BuildVersion.cs
Normal file
11
Jellyfin2Samsung-CrossOS/Models/BuildVersion.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public partial class BuildVersion : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private string fileName = string.Empty;
|
||||
[ObservableProperty] private string description = string.Empty;
|
||||
[ObservableProperty] private string repoUrl = string.Empty;
|
||||
}
|
||||
}
|
||||
73
Jellyfin2Samsung-CrossOS/Models/GitHubAtomEntry.cs
Normal file
73
Jellyfin2Samsung-CrossOS/Models/GitHubAtomEntry.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a release entry parsed from the GitHub Atom feed.
|
||||
/// The Atom feed does not have rate limits unlike the REST API.
|
||||
/// </summary>
|
||||
public class GitHubAtomEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique ID of the release (e.g., "tag:github.com,2008:Repository/123456/v1.0.0").
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The release title/name.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the release was last updated.
|
||||
/// </summary>
|
||||
public DateTime? Updated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The link to the release page.
|
||||
/// </summary>
|
||||
public string Link { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The release content/description (HTML).
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The author who published the release.
|
||||
/// </summary>
|
||||
public string AuthorName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the tag name (version) from the release ID or link.
|
||||
/// </summary>
|
||||
public string TagName
|
||||
{
|
||||
get
|
||||
{
|
||||
// Try to extract from link first: https://github.com/owner/repo/releases/tag/v1.0.0
|
||||
if (!string.IsNullOrEmpty(Link))
|
||||
{
|
||||
const string tagMarker = "/releases/tag/";
|
||||
var tagIndex = Link.IndexOf(tagMarker, StringComparison.OrdinalIgnoreCase);
|
||||
if (tagIndex >= 0)
|
||||
{
|
||||
return Link.Substring(tagIndex + tagMarker.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: extract from ID: tag:github.com,2008:Repository/123456/v1.0.0
|
||||
if (!string.IsNullOrEmpty(Id))
|
||||
{
|
||||
var lastSlash = Id.LastIndexOf('/');
|
||||
if (lastSlash >= 0 && lastSlash < Id.Length - 1)
|
||||
{
|
||||
return Id.Substring(lastSlash + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,30 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
|
||||
public class GitHubRelease
|
||||
{
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("tag_name")]
|
||||
public string TagName { get; set; }
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("published_at")]
|
||||
public string PublishedAt { get; set; }
|
||||
[JsonPropertyName("tag_name")]
|
||||
public string TagName { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("assets")]
|
||||
public List<Asset> Assets { get; set; } = new List<Asset>();
|
||||
[JsonPropertyName("published_at")]
|
||||
public string PublishedAt { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("assets")]
|
||||
public List<Asset> Assets { get; set; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
public string? PrimaryDownloadUrl => Assets?.FirstOrDefault()?.DownloadUrl;
|
||||
|
||||
public string PrimaryDownloadUrl => Assets?.FirstOrDefault()?.DownloadUrl;
|
||||
[JsonConstructor]
|
||||
public GitHubRelease()
|
||||
{
|
||||
}
|
||||
@@ -31,30 +32,37 @@ namespace Jellyfin2Samsung.Models
|
||||
|
||||
public class Asset
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string FileName { get; set; }
|
||||
[JsonPropertyName("name")]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("browser_download_url")]
|
||||
public string DownloadUrl { get; set; }
|
||||
[JsonPropertyName("browser_download_url")]
|
||||
public string DownloadUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("size")]
|
||||
[JsonPropertyName("size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsDefault => FileName.Equals("Jellyfin.wgt", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
public string DisplayText => $"{FileName} ({FormatFileSize(Size)})";
|
||||
|
||||
private string FormatFileSize(long bytes)
|
||||
private static string FormatFileSize(long bytes)
|
||||
{
|
||||
string[] sizes = { "B", "KB", "MB", "GB" };
|
||||
string[] sizes = ["B", "KB", "MB", "GB"];
|
||||
int order = 0;
|
||||
double len = bytes;
|
||||
|
||||
while (len >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
len /= 1024;
|
||||
}
|
||||
|
||||
return $"{len:0.##} {sizes[order]}";
|
||||
}
|
||||
[JsonConstructor]
|
||||
|
||||
public Asset()
|
||||
{
|
||||
}
|
||||
|
||||
53
Jellyfin2Samsung-CrossOS/Models/JellyTheme.cs
Normal file
53
Jellyfin2Samsung-CrossOS/Models/JellyTheme.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a JellyTheme CSS theme with its metadata.
|
||||
/// </summary>
|
||||
public class JellyTheme
|
||||
{
|
||||
/// <summary>
|
||||
/// Display name of the theme (e.g., "Obsidian").
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Emoji icon for the theme.
|
||||
/// </summary>
|
||||
public string Icon { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Color description (e.g., "Purple").
|
||||
/// </summary>
|
||||
public string ColorName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Hex color code for the button background.
|
||||
/// </summary>
|
||||
public string HexColor { get; init; } = "#6B5B95";
|
||||
|
||||
/// <summary>
|
||||
/// The CSS @import URL for this theme.
|
||||
/// </summary>
|
||||
public string CssImportUrl { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Preview image URL.
|
||||
/// </summary>
|
||||
public string PreviewUrl { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// GitHub README URL for this theme.
|
||||
/// </summary>
|
||||
public string ReadmeUrl { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full @import statement for this theme.
|
||||
/// </summary>
|
||||
public string CssImportStatement => $"@import url(\"{CssImportUrl}\");";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display text with icon.
|
||||
/// </summary>
|
||||
public string DisplayName => $"{Icon} {Name}";
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,23 @@
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class JellyfinUser
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public class JellyfinPublicSystemInfo
|
||||
{
|
||||
public string? LocalAddress { get; set; }
|
||||
public string? ServerName { get; set; }
|
||||
public string? Version { get; set; }
|
||||
public string? ProductName { get; set; }
|
||||
public string? OperatingSystem { get; set; }
|
||||
public string? Id { get; set; }
|
||||
public bool StartupWizardCompleted { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
32
Jellyfin2Samsung-CrossOS/Models/JellyfinPluginInfo.cs
Normal file
32
Jellyfin2Samsung-CrossOS/Models/JellyfinPluginInfo.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public class JellyfinPluginInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
public class PluginMatrixEntry
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public List<string> ExplicitServerFiles { get; set; }
|
||||
public List<string> FallbackUrls { get; set; }
|
||||
public bool UseBabel { get; set; }
|
||||
public string RawRoot { get; set; }
|
||||
}
|
||||
public class ExtractedDomBlocks
|
||||
{
|
||||
public List<string> HeadInjectBlocks { get; set; } = new();
|
||||
public List<string> BodyInjectBlocks { get; set; } = new();
|
||||
}
|
||||
public enum ServerAssetKind
|
||||
{
|
||||
Unknown = 0,
|
||||
PluginAsset = 1
|
||||
}
|
||||
|
||||
public sealed record ServerAssetRule(string pluginName, Func<string, bool> match, ServerAssetKind treatAs);
|
||||
}
|
||||
@@ -32,4 +32,12 @@
|
||||
public string Name = "";
|
||||
public bool Activated;
|
||||
}
|
||||
public class NetworkInterfaceOption
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
|
||||
public string DisplayText => $"{Name} - {IpAddress}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
9
Jellyfin2Samsung-CrossOS/Models/ProviderOption.cs
Normal file
9
Jellyfin2Samsung-CrossOS/Models/ProviderOption.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Jellyfin2Samsung.Models;
|
||||
|
||||
public sealed class ProviderOption
|
||||
{
|
||||
public string DisplayName { get; init; } = "";
|
||||
public IImage? PreviewImage { get; init; } // can be a Bitmap later
|
||||
}
|
||||
10
Jellyfin2Samsung-CrossOS/Models/TvLogConnectionStatus.cs
Normal file
10
Jellyfin2Samsung-CrossOS/Models/TvLogConnectionStatus.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public enum TvLogConnectionStatus
|
||||
{
|
||||
Stopped,
|
||||
Listening,
|
||||
Connected,
|
||||
NoConnections
|
||||
}
|
||||
}
|
||||
10
Jellyfin2Samsung-CrossOS/Models/TvLogMessage.cs
Normal file
10
Jellyfin2Samsung-CrossOS/Models/TvLogMessage.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public class TvLogMessage
|
||||
{
|
||||
public string level { get; set; }
|
||||
public object msg { get; set; }
|
||||
public object meta { get; set; }
|
||||
}
|
||||
}
|
||||
86
Jellyfin2Samsung-CrossOS/Models/UpdateCheckResult.cs
Normal file
86
Jellyfin2Samsung-CrossOS/Models/UpdateCheckResult.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Result of checking for application updates.
|
||||
/// </summary>
|
||||
public class UpdateCheckResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a newer version is available.
|
||||
/// </summary>
|
||||
public bool IsUpdateAvailable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current installed version.
|
||||
/// </summary>
|
||||
public string CurrentVersion { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The latest available version.
|
||||
/// </summary>
|
||||
public string LatestVersion { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// URL to download the update for the current platform.
|
||||
/// </summary>
|
||||
public string? DownloadUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to the GitHub releases page.
|
||||
/// </summary>
|
||||
public string ReleasesPageUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Release title/name.
|
||||
/// </summary>
|
||||
public string ReleaseTitle { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Release notes or description.
|
||||
/// </summary>
|
||||
public string ReleaseNotes { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the release was published.
|
||||
/// </summary>
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if the check failed.
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the check completed successfully.
|
||||
/// </summary>
|
||||
public bool IsSuccess => string.IsNullOrEmpty(ErrorMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed result with an error message.
|
||||
/// </summary>
|
||||
public static UpdateCheckResult Failed(string errorMessage, string currentVersion)
|
||||
{
|
||||
return new UpdateCheckResult
|
||||
{
|
||||
IsUpdateAvailable = false,
|
||||
CurrentVersion = currentVersion,
|
||||
ErrorMessage = errorMessage
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a result indicating no update is available.
|
||||
/// </summary>
|
||||
public static UpdateCheckResult NoUpdateAvailable(string currentVersion)
|
||||
{
|
||||
return new UpdateCheckResult
|
||||
{
|
||||
IsUpdateAvailable = false,
|
||||
CurrentVersion = currentVersion,
|
||||
LatestVersion = currentVersion
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user