mirror of
https://github.com/Jellyfin2Samsung/Samsung-Jellyfin-Installer.git
synced 2026-03-01 11:21:12 +03:00
Compare commits
363 Commits
v1.8.7.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 |
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/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/
|
||||
esbuild/
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
|
||||
@@ -5,6 +5,12 @@ 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;
|
||||
@@ -35,7 +41,6 @@ namespace Jellyfin2Samsung
|
||||
{
|
||||
DisableAvaloniaDataAnnotationValidation();
|
||||
|
||||
// Always use Dispatcher.Post for cross-platform safety
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
@@ -44,7 +49,10 @@ namespace Jellyfin2Samsung
|
||||
});
|
||||
}
|
||||
|
||||
RequestedThemeVariant = ThemeVariant.Light;
|
||||
// Apply saved theme on startup
|
||||
var themeService = _serviceProvider.GetRequiredService<IThemeService>();
|
||||
themeService.ApplyTheme();
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
@@ -54,20 +62,46 @@ 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<JellyfinApiClient>();
|
||||
services.AddSingleton<PluginManager>();
|
||||
services.AddSingleton<JellyfinWebPackagePatcher>();
|
||||
services.AddSingleton<IThemeService, ThemeService>();
|
||||
services.AddSingleton<IUpdaterService, UpdaterService>();
|
||||
services.AddSingleton<IUpdateDialogService, UpdateDialogService>();
|
||||
|
||||
// Other Helpers
|
||||
// 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<CertificateHelper>();
|
||||
@@ -75,22 +109,35 @@ namespace Jellyfin2Samsung
|
||||
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.AddTransient<TvLogsWindow>();
|
||||
services.AddTransient<JellyfinConfigViewModel>();
|
||||
services.AddSingleton<JellyfinConfigViewModel>();
|
||||
|
||||
// --------------------
|
||||
// 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 =>
|
||||
@@ -114,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);
|
||||
}
|
||||
@@ -126,10 +175,12 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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."
|
||||
}
|
||||
@@ -6,42 +6,24 @@
|
||||
"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",
|
||||
@@ -49,9 +31,7 @@
|
||||
"lblLanguage": "Sprog",
|
||||
"lblCustomWgt": "WGT-fil",
|
||||
"lblSettings": "Applikationsindstillinger",
|
||||
"lblRememberIp": "Husk IP'er",
|
||||
"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,34 +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",
|
||||
"lblSDB": "Fejlfinding",
|
||||
"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",
|
||||
@@ -99,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-indstillinger",
|
||||
"SelectSubtitleMode": "Vælg underteksttilstand",
|
||||
"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",
|
||||
@@ -135,15 +78,9 @@
|
||||
"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!",
|
||||
"deleteExistingVersion": "Sletter en eksisterende version...",
|
||||
"deleteExistingFailed": "Sletning af den eksisterende version mislykkedes. Slet den manuelt...",
|
||||
@@ -151,7 +88,7 @@
|
||||
"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": "Jellyfin-plugins",
|
||||
"lblUseServerScripts": "Download Jellyfin-plugins",
|
||||
"lblEnableDevLogs": "Aktivér tv-fejlfinding",
|
||||
"lblOpenDebugWindow": "Åbn TV-log",
|
||||
"lblStartLog": "Starte",
|
||||
@@ -161,5 +98,58 @@
|
||||
"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)"
|
||||
"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."
|
||||
}
|
||||
@@ -6,42 +6,24 @@
|
||||
"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.",
|
||||
"DownloadingPackage": "Paket wird heruntergeladen...",
|
||||
"ConnectingToDevice": "Verbindung zum Gerät wird hergestellt...",
|
||||
"RetrievingDeviceAddress": "Geräteadresse wird abgerufen...",
|
||||
"TvNameNotFound": "TV-Name konnte nicht gefunden werden...",
|
||||
"TvDuidNotFound": "TV-DUID konnte nicht gefunden werden...",
|
||||
"UpdatingCertificateProfile": "Zertifikatsprofil wird aktualisiert...",
|
||||
"CreatingCertificateProfile": "Neues Zertifikatsprofil wird erstellt...",
|
||||
"PackagingWgtWithCertificate": "WGT-Datei wird mit Zertifikat verpackt...",
|
||||
"InstallationSuccessful": "Installation erfolgreich",
|
||||
"InstallingPackage": "Paket wird auf Gerät installiert...",
|
||||
"InstallationMaybeFailed": "Installation möglicherweise fehlgeschlagen",
|
||||
"Output": "Ausgabe",
|
||||
"LoadingReleases": "Versionen werden geladen...",
|
||||
"ScanningNetwork": "Netzwerk wird nach Samsung TV durchsucht...",
|
||||
"CheckTizenOS": "Tizen OS-Version wird überprüft...",
|
||||
"SuccessAuthCode": "Autorisierungscode erfolgreich erhalten",
|
||||
"Ready": "Bereit zur Nutzung...",
|
||||
"SamsungLogin": "Anmeldung bei Samsung...",
|
||||
"FailedAuthCode": "Anmeldung wurde abgebrochen oder ist fehlgeschlagen...",
|
||||
"NoDevicesFound": "Keine Geräte gefunden",
|
||||
"OutputDir": "Sicherstellen, dass das Ausgabeverzeichnis existiert",
|
||||
"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...",
|
||||
"CreateNewCertificates": "Signierte P12-Dateien werden erstellt...",
|
||||
"ExportPfxCertificates": "PFX-Zertifikate werden exportiert...",
|
||||
"SettingCertificateManager": "Tizen-Zertifikatsmanager wird korrigiert...",
|
||||
"SettingsCaCerts": "CA-Zertifikate werden eingestellt...",
|
||||
"ChooseRelease": "Version wählen...",
|
||||
"ChooseVersion": "Version wählen...",
|
||||
"ChooseTV": "TV wählen...",
|
||||
"SelectLanguage": "Sprache auswählen",
|
||||
"SelectCertificate": "Zertifikat auswählen",
|
||||
"SelectWGT": "WGT- und/oder TPK-Datei(en) auswählen",
|
||||
"lblRelease": "Version",
|
||||
"lblVersion": "Version",
|
||||
@@ -49,9 +31,7 @@
|
||||
"lblLanguage": "Sprache",
|
||||
"lblCustomWgt": "WGT-Datei",
|
||||
"lblSettings": "Anwendungseinstellungen",
|
||||
"lblRememberIp": "IPs merken",
|
||||
"lblDeletePrevious": "Altes Jellyfin entfernen",
|
||||
"lblexpires": "Läuft ab",
|
||||
"IpWindowTitle": "TV-IP eingeben",
|
||||
"IpWindowDescription": "Bitte die IP-Adresse des Geräts eingeben:",
|
||||
"InvalidDeviceIp": "Ungültige Geräte-IP oder Gerät nicht gefunden.",
|
||||
@@ -62,34 +42,15 @@
|
||||
"NoPackageToInstall": "Kein Paket zu installieren",
|
||||
"NoDeviceSelected": "Kein Gerät ausgewählt",
|
||||
"UsingCustomWGT": "Benutzerdefinierte WGT-Datei wird verwendet",
|
||||
"FailedRemoveOld": "Entfernen der alten App-Version fehlgeschlagen",
|
||||
"FailedRemoveOldExtra": "Bitte entfernen Sie die alte Jellyfin-App manuell vom TV",
|
||||
"CheckingTizenSdb": "Tizen SDB wird überprüft...",
|
||||
"InitializationFailed": "Initialisierung fehlgeschlagen...",
|
||||
"DownloadingSetupFile": "Installationsprogramm wird heruntergeladen...",
|
||||
"InstallingSetupFile": "Tizen Studio CLI wird installiert...",
|
||||
"InstallingCertificateManager": "Zertifikatsmanager wird installiert...",
|
||||
"ReInstallingCertificateManager": "Installation des Zertifikatsmanager-Add-ons fehlgeschlagen, erneut versuchen?",
|
||||
"InstallingCertificateAddOn": "Zertifikatsmanager-Add-on wird installiert...",
|
||||
"CheckingPackageManagerList": "Tizen-Paketmanager wird überprüft",
|
||||
"PermissionDenied": "Zugriff verweigert",
|
||||
"AdminPrivRequired": "Administratorrechte sind erforderlich, um den Ordner zu erstellen. Bitte erstellen Sie ihn manuell oder führen Sie die Anwendung als Administrator neu aus.",
|
||||
"PathLengthWarning": "Windows-Pfadlänge überschritten",
|
||||
"PathLengthExceeded": "Der Standardpfad überschreitet die Windows-Pfadlänge. Möchten Sie ihn zu C:\\Tizen Studio Cli ändern?",
|
||||
"lblForceLogin": "Samsung-Anmeldung erzwingen",
|
||||
"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?",
|
||||
"lblRTL": "Rechts-nach-links (RTL) Lesen",
|
||||
"lblSDB": "Debug",
|
||||
"FailedToStopSdbServer": "Es läuft bereits ein SDB-Server, konnte nicht gestoppt werden...",
|
||||
"FailedToStopSdbServerTitle": "SDB-Server konnte nicht gestoppt werden!",
|
||||
"lblJellyfinServer": "Adresse",
|
||||
"lblModifyConfig": "Jellyfin-Konfiguration festlegen",
|
||||
"ConfigFailure": "config.json nicht im extrahierten Paket gefunden.",
|
||||
"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",
|
||||
"ServerPort": "Port",
|
||||
"Theme": "Theme",
|
||||
"SelectTheme": "Theme auswählen",
|
||||
"lblEnableBackdrops": "Hintergrundbilder aktivieren",
|
||||
"lblEnableThemeSongs": "Theme-Songs aktivieren",
|
||||
"lblEnableThemeVideos": "Theme-Videos aktivieren",
|
||||
@@ -99,31 +60,13 @@
|
||||
"lblNextUpEnabled": "Als Nächstes aktiviert",
|
||||
"lblEnableExternalVideoPlayers": "Externe Videoplayer aktivieren",
|
||||
"lblSkipIntros": "Intros überspringen",
|
||||
"lblAutoPlayNextEpisode": "Nächste Episode automatisch abspielen",
|
||||
"lblRememberAudioSelections": "Audioauswahl merken",
|
||||
"lblRememberSubtitleSelections": "Untertitelauswahl merken",
|
||||
"lblPlayDefaultAudioTrack": "Standard-Audiospur abspielen",
|
||||
"lblSubtitleMode": "Untertitelmodus",
|
||||
"lblAudioLanguagePreference": "Audiosprache-Präferenz",
|
||||
"lblSubtitleLanguagePreference": "Untertitelsprache-Präferenz",
|
||||
"lblJellyfinConfig": "Jellyfin-Einstellungen",
|
||||
"SelectSubtitleMode": "Untertitelmodus auswählen",
|
||||
"AudioLanguage": "z.B. de | en | fr",
|
||||
"lblOpenConfig": "Konfiguration öffnen",
|
||||
"lblValidateLogin": "Validieren",
|
||||
"lblValidateJellyfin": "Jellyfin-Server validieren",
|
||||
"NoJellyfinServer": "Bitte zuerst den Jellyfin-Server ausfüllen...",
|
||||
"JellyfinLoginFailed": "Anmeldung fehlgeschlagen: Ungültige Anmeldeinformationen oder Serverfehler.",
|
||||
"lblJellyfinServerApi": "API-Schlüssel",
|
||||
"UpdateMode": "Update-Modus",
|
||||
"lblServerSettings": "Servereinstellungen",
|
||||
"lblBrowserSettings": "Browser-Einstellungen",
|
||||
"lblUserSettings": "Benutzereinstellungen",
|
||||
"lbluserAutoLogin": "Auto-Login aktivieren",
|
||||
"lblJellyfinUser": "Jellyfin-Benutzer",
|
||||
"lblFailedUsers": "Konnte keine Jellyfin-Benutzer abrufen, keine Konfigurationseinstellungen werden geändert!",
|
||||
"lblUpdateMode": "Wählen Sie, was aktualisiert werden soll",
|
||||
"lblJellyfinAPIKey": "Jellyfin-API-Schlüssel",
|
||||
"lblSelectUsers": "Benutzer zum Aktualisieren auswählen",
|
||||
"lblValidation": "🍺 Kauf mir ein Bier",
|
||||
"btn_Close": "Schließen",
|
||||
@@ -135,15 +78,9 @@
|
||||
"keyContinue": "Weiter",
|
||||
"keyStop": "Stopp",
|
||||
"keyConfirm": "Bestätigen",
|
||||
"minimalCliTitle": "Tizen CLI & Zertifikatsmanager erforderlich",
|
||||
"minimalCliMessage": "Tizen CLI & Zertifikatsmanager sind erforderlich, um fortzufahren.\n\nWir werden jetzt Tizen CLI herunterladen und installieren, gefolgt vom Zertifikatsmanager.\nDies kann einige Minuten dauern. Bitte haben Sie Geduld während des Installationsprozesses.",
|
||||
"minimalCliStop": "Benutzer hat Installation von Tizen CLI abgelehnt",
|
||||
"packageAndSign": "Verpacken und Signieren...",
|
||||
"certRetryFailed": "Wiederholung fehlgeschlagen, bitte erstellen Sie ein Issue",
|
||||
"lblPermitInstall": "Installation erlauben (Tizen =< 4.0)",
|
||||
"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",
|
||||
"searchApp": "Überprüfen, ob App bereits installiert ist",
|
||||
"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...",
|
||||
@@ -151,15 +88,68 @@
|
||||
"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",
|
||||
"lblUseServerScripts": "Jellyfin-Plugins herunterladen",
|
||||
"lblEnableDevLogs": "TV-Debugging aktivieren",
|
||||
"lblOpenDebugWindow": "TV-Log öffnen",
|
||||
"lblStartLog": "Start",
|
||||
"lblStopLog": "Stoppen",
|
||||
"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)"
|
||||
"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."
|
||||
}
|
||||
@@ -6,42 +6,24 @@
|
||||
"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",
|
||||
@@ -49,9 +31,7 @@
|
||||
"lblLanguage": "Language",
|
||||
"lblCustomWgt": "WGT File",
|
||||
"lblSettings": "Application settings",
|
||||
"lblRememberIp": "Remember IPs",
|
||||
"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,34 +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",
|
||||
"lblSDB": "Debug",
|
||||
"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",
|
||||
@@ -99,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 settings",
|
||||
"SelectSubtitleMode": "Select Subtitle Mode",
|
||||
"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",
|
||||
@@ -135,15 +78,9 @@
|
||||
"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!",
|
||||
"deleteExistingVersion": "Deleting an existing version...",
|
||||
"deleteExistingFailed": "Deleting the existing version failed. Please delete it manually...",
|
||||
@@ -151,7 +88,7 @@
|
||||
"deleteExistingNotAllowed": "Deletion not allowed. Enable the setting to allow the tool to remove the existing app...",
|
||||
"lblLocalIP": "Local IP:",
|
||||
"lblTryOverwrite": "Overwrite existing version",
|
||||
"lblUseServerScripts": "Jellyfin plugins",
|
||||
"lblUseServerScripts": "Download Jellyfin plugins",
|
||||
"lblEnableDevLogs": "Enable TV Debug",
|
||||
"lblOpenDebugWindow": "Open TV Log",
|
||||
"lblStartLog": "Start",
|
||||
@@ -161,5 +98,58 @@
|
||||
"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)"
|
||||
"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."
|
||||
}
|
||||
@@ -6,42 +6,24 @@
|
||||
"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é.",
|
||||
"DownloadingPackage": "Téléchargement du paquet...",
|
||||
"ConnectingToDevice": "Connexion à l'appareil...",
|
||||
"RetrievingDeviceAddress": "Récupération de l'adresse de l'appareil...",
|
||||
"TvNameNotFound": "Le nom du téléviseur est introuvable...",
|
||||
"TvDuidNotFound": "Le DUID du téléviseur est introuvable...",
|
||||
"UpdatingCertificateProfile": "Mise à jour du profil de certificat...",
|
||||
"CreatingCertificateProfile": "Création d'un nouveau profil de certificat...",
|
||||
"PackagingWgtWithCertificate": "Empaquetage du fichier wgt avec le certificat...",
|
||||
"InstallationSuccessful": "Installation réussie",
|
||||
"InstallingPackage": "Installation du paquet sur l'appareil...",
|
||||
"InstallationMaybeFailed": "L'installation a peut-être échoué",
|
||||
"Output": "Sortie",
|
||||
"LoadingReleases": "Chargement des versions...",
|
||||
"ScanningNetwork": "Recherche de téléviseurs Samsung sur le réseau...",
|
||||
"CheckTizenOS": "Vérification de la version de Tizen OS...",
|
||||
"SuccessAuthCode": "Code d'autorisation obtenu avec succès",
|
||||
"Ready": "Prêt à l'emploi...",
|
||||
"SamsungLogin": "Connexion à Samsung...",
|
||||
"FailedAuthCode": "La connexion a été annulée ou a échoué...",
|
||||
"NoDevicesFound": "Aucun appareil trouvé",
|
||||
"OutputDir": "Vérification de l'existence du répertoire de sortie",
|
||||
"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...",
|
||||
"CreateNewCertificates": "Création des fichiers P12 signés...",
|
||||
"ExportPfxCertificates": "Exportation des certificats PFX...",
|
||||
"SettingCertificateManager": "Correction du gestionnaire de certificats Tizen...",
|
||||
"SettingsCaCerts": "Configuration des certificats CA...",
|
||||
"ChooseRelease": "Choisir la version...",
|
||||
"ChooseVersion": "Choisir la version...",
|
||||
"ChooseTV": "Choisir le téléviseur...",
|
||||
"SelectLanguage": "Sélectionner la langue",
|
||||
"SelectCertificate": "Sélectionner le certificat",
|
||||
"SelectWGT": "Sélectionner le(s) fichier(s) WGT et/ou TPK",
|
||||
"lblRelease": "Version",
|
||||
"lblVersion": "Version",
|
||||
@@ -49,9 +31,7 @@
|
||||
"lblLanguage": "Langue",
|
||||
"lblCustomWgt": "Fichier WGT",
|
||||
"lblSettings": "Paramètres de l'application",
|
||||
"lblRememberIp": "Mémoriser les adresses IP",
|
||||
"lblDeletePrevious": "Supprimer l'ancien Jellyfin",
|
||||
"lblexpires": "Expire",
|
||||
"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.",
|
||||
@@ -62,34 +42,15 @@
|
||||
"NoPackageToInstall": "Aucun paquet à installer",
|
||||
"NoDeviceSelected": "Aucun appareil sélectionné",
|
||||
"UsingCustomWGT": "Utilisation d'un fichier WGT personnalisé",
|
||||
"FailedRemoveOld": "Échec de la suppression de l'ancienne version de l'application",
|
||||
"FailedRemoveOldExtra": "Veuillez supprimer manuellement l'ancienne application Jellyfin depuis le téléviseur",
|
||||
"CheckingTizenSdb": "Vérification de Tizen SDB...",
|
||||
"InitializationFailed": "Échec de l'initialisation...",
|
||||
"DownloadingSetupFile": "Téléchargement du programme d'installation...",
|
||||
"InstallingSetupFile": "Installation de Tizen Studio CLI...",
|
||||
"InstallingCertificateManager": "Installation du gestionnaire de certificats...",
|
||||
"ReInstallingCertificateManager": "L'installation du module complémentaire du gestionnaire de certificats a échoué. Voulez-vous réessayer ?",
|
||||
"InstallingCertificateAddOn": "Installation du module complémentaire du gestionnaire de certificats...",
|
||||
"CheckingPackageManagerList": "Vérification du gestionnaire de paquets Tizen",
|
||||
"PermissionDenied": "Permission refusée",
|
||||
"AdminPrivRequired": "Les privilèges administrateur sont requis pour créer le dossier. Veuillez le créer manuellement ou relancer l'application en tant qu'administrateur.",
|
||||
"PathLengthWarning": "Longueur de chemin Windows dépassée",
|
||||
"PathLengthExceeded": "Le chemin par défaut dépasse la longueur de chemin Windows. Souhaitez-vous le modifier en C:\\Tizen Studio Cli ?",
|
||||
"lblForceLogin": "Forcer la connexion Samsung",
|
||||
"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 ?",
|
||||
"lblRTL": "Lecture de droite à gauche (RTL)",
|
||||
"lblSDB": "Déboguer",
|
||||
"FailedToStopSdbServer": "Un serveur SDB est déjà en cours d'exécution, échec de l'arrêt...",
|
||||
"FailedToStopSdbServerTitle": "Échec de l'arrêt du serveur SDB !",
|
||||
"lblJellyfinServer": "Adresse",
|
||||
"lblModifyConfig": "Configurer Jellyfin",
|
||||
"ConfigFailure": "config.json introuvable dans le paquet extrait.",
|
||||
"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",
|
||||
"ServerPort": "Port",
|
||||
"Theme": "Thème",
|
||||
"SelectTheme": "Sélectionner le thème",
|
||||
"lblEnableBackdrops": "Activer les arrière-plans",
|
||||
"lblEnableThemeSongs": "Activer les musiques de thème",
|
||||
"lblEnableThemeVideos": "Activer les vidéos de thème",
|
||||
@@ -99,31 +60,13 @@
|
||||
"lblNextUpEnabled": "Activer « À suivre »",
|
||||
"lblEnableExternalVideoPlayers": "Activer les lecteurs vidéo externes",
|
||||
"lblSkipIntros": "Ignorer les génériques",
|
||||
"lblAutoPlayNextEpisode": "Lire automatiquement l'épisode suivant",
|
||||
"lblRememberAudioSelections": "Mémoriser les sélections audio",
|
||||
"lblRememberSubtitleSelections": "Mémoriser les sélections de sous-titres",
|
||||
"lblPlayDefaultAudioTrack": "Lire la piste audio par défaut",
|
||||
"lblSubtitleMode": "Mode sous-titres",
|
||||
"lblAudioLanguagePreference": "Préférence de langue audio",
|
||||
"lblSubtitleLanguagePreference": "Préférence de langue des sous-titres",
|
||||
"lblJellyfinConfig": "Paramètres Jellyfin",
|
||||
"SelectSubtitleMode": "Sélectionner le mode sous-titres",
|
||||
"AudioLanguage": "ex. fr | en | es",
|
||||
"lblOpenConfig": "Ouvrir la configuration",
|
||||
"lblValidateLogin": "Valider",
|
||||
"lblValidateJellyfin": "Valider le serveur Jellyfin",
|
||||
"NoJellyfinServer": "Veuillez d'abord renseigner le serveur Jellyfin...",
|
||||
"JellyfinLoginFailed": "Échec de la connexion : identifiants invalides ou erreur du serveur.",
|
||||
"lblJellyfinServerApi": "Clé API",
|
||||
"UpdateMode": "Mode de mise à jour",
|
||||
"lblServerSettings": "Paramètres du serveur",
|
||||
"lblBrowserSettings": "Paramètres du navigateur",
|
||||
"lblUserSettings": "Paramètres utilisateur",
|
||||
"lbluserAutoLogin": "Activer la connexion automatique",
|
||||
"lblJellyfinUser": "Utilisateur(s) Jellyfin",
|
||||
"lblFailedUsers": "Échec de la récupération des utilisateurs Jellyfin, aucun paramètre de configuration ne sera modifié !",
|
||||
"lblUpdateMode": "Choisir quoi mettre à jour",
|
||||
"lblJellyfinAPIKey": "Clé API Jellyfin",
|
||||
"lblSelectUsers": "Sélectionner le(s) utilisateur(s) à mettre à jour",
|
||||
"lblValidation": "🍺 Offrez-moi une bière",
|
||||
"btn_Close": "Fermer",
|
||||
@@ -135,15 +78,9 @@
|
||||
"keyContinue": "Continuer",
|
||||
"keyStop": "Arrêter",
|
||||
"keyConfirm": "Confirmer",
|
||||
"minimalCliTitle": "Tizen CLI et gestionnaire de certificats requis",
|
||||
"minimalCliMessage": "Tizen CLI et le gestionnaire de certificats sont nécessaires pour continuer.\n\nNous allons maintenant télécharger et installer Tizen CLI suivi du gestionnaire de certificats.\nCela peut prendre quelques minutes. Veuillez patienter pendant l'installation.",
|
||||
"minimalCliStop": "L'utilisateur a refusé d'installer Tizen CLI",
|
||||
"packageAndSign": "Empaquetage et signature...",
|
||||
"certRetryFailed": "Échec de la nouvelle tentative, veuillez créer un ticket",
|
||||
"lblPermitInstall": "Autoriser l'installation (Tizen <= 4.0)",
|
||||
"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",
|
||||
"searchApp": "Vérification si l'application est déjà installée",
|
||||
"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...",
|
||||
@@ -151,15 +88,68 @@
|
||||
"deleteExistingNotAllowed": "Suppression impossible. Activez l'option pour autoriser l'outil à supprimer l'application existante...",
|
||||
"lblLocalIP": "IP locale :",
|
||||
"lblTryOverwrite": "Écraser la version existante",
|
||||
"lblUseServerScripts": "plugins Jellyfin",
|
||||
"lblUseServerScripts": "Télécharger les plugins Jellyfin",
|
||||
"lblEnableDevLogs": "Activer le débogage TV",
|
||||
"lblOpenDebugWindow": "Ouvrir le journal TV",
|
||||
"lblStartLog": "Commencer",
|
||||
"lblStopLog": "Arrêt",
|
||||
"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)"
|
||||
}
|
||||
"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."
|
||||
}
|
||||
@@ -6,42 +6,24 @@
|
||||
"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",
|
||||
@@ -49,9 +31,7 @@
|
||||
"lblLanguage": "Taal",
|
||||
"lblCustomWgt": "WGT Bestand",
|
||||
"lblSettings": "Applicatie-instellingen",
|
||||
"lblRememberIp": "IP-adressen onthouden",
|
||||
"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,34 +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",
|
||||
"lblSDB": "Foutopsporing",
|
||||
"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",
|
||||
@@ -99,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-instellingen",
|
||||
"SelectSubtitleMode": "Ondertitelingmodus selecteren",
|
||||
"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",
|
||||
@@ -135,15 +78,9 @@
|
||||
"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!",
|
||||
"deleteExistingVersion": "Bestaande versie verwijderen...",
|
||||
"deleteExistingFailed": "Het verwijderen van de bestaande versie is mislukt. Verwijder de versie handmatig....",
|
||||
@@ -151,7 +88,7 @@
|
||||
"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",
|
||||
"lblUseServerScripts": "Jellyfin plugins downloaden",
|
||||
"lblEnableDevLogs": "TV-foutopsporing inschakelen",
|
||||
"lblOpenDebugWindow": "Open TV-logboek",
|
||||
"lblStartLog": "Start",
|
||||
@@ -161,5 +98,58 @@
|
||||
"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)"
|
||||
"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.
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,12 +33,20 @@ namespace Jellyfin2Samsung.Helpers
|
||||
// ----- 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;
|
||||
@@ -50,16 +58,11 @@ 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 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";
|
||||
@@ -69,20 +72,50 @@ namespace Jellyfin2Samsung.Helpers
|
||||
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.7.3-beta";
|
||||
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/Tizen/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
|
||||
|
||||
@@ -4,7 +4,7 @@ using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers.Converters
|
||||
{
|
||||
public class TvLogStatusToBrushConverter : IValueConverter
|
||||
{
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public static class EsbuildHelper
|
||||
{
|
||||
@@ -14,27 +13,7 @@ namespace Jellyfin2Samsung.Helpers
|
||||
try
|
||||
{
|
||||
string baseDir = AppContext.BaseDirectory;
|
||||
string relPath;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
relPath = Path.Combine(AppSettings.EsbuildPath, "win-x64", "esbuild.exe");
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
relPath = Path.Combine(AppSettings.EsbuildPath, "linux-x64", "esbuild");
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
relPath = Path.Combine(AppSettings.EsbuildPath, "osx-universal", "esbuild");
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string fullPath = Path.Combine(baseDir, relPath);
|
||||
return File.Exists(fullPath) ? fullPath : null;
|
||||
return PlatformService.GetEsbuildPath(Path.Combine(baseDir, AppSettings.EsbuildPath));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -53,22 +32,22 @@ namespace Jellyfin2Samsung.Helpers
|
||||
string? esbuildPath = GetEsbuildPath();
|
||||
if (string.IsNullOrEmpty(esbuildPath))
|
||||
{
|
||||
Trace.WriteLine($"⚠ esbuild binary not found, skipping transpile for {relPathForLog ?? "unknown"}");
|
||||
Trace.WriteLine($"esbuild binary not found, skipping transpile for {relPathForLog ?? "unknown"}");
|
||||
return js;
|
||||
}
|
||||
|
||||
string tempRoot = Path.Combine(Path.GetTempPath(), "J2S_Esbuild");
|
||||
string tempRoot = Path.Combine(Path.GetTempPath(), Constants.Esbuild.TempFolderName);
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
|
||||
string inputPath = Path.Combine(tempRoot, Guid.NewGuid().ToString("N") + ".js");
|
||||
string outputPath = Path.Combine(tempRoot, Guid.NewGuid().ToString("N") + ".js");
|
||||
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=es2015",
|
||||
Arguments = $"\"{inputPath}\" --outfile=\"{outputPath}\" --target={Constants.Esbuild.TargetEs2015}",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
@@ -85,7 +64,7 @@ namespace Jellyfin2Samsung.Helpers
|
||||
|
||||
if (proc.ExitCode != 0 || !File.Exists(outputPath))
|
||||
{
|
||||
Trace.WriteLine($"⚠ esbuild failed for {relPathForLog ?? "unknown"} (exit {proc.ExitCode}): {stderr}");
|
||||
Trace.WriteLine($"esbuild failed for {relPathForLog ?? "unknown"} (exit {proc.ExitCode}): {stderr}");
|
||||
return js;
|
||||
}
|
||||
|
||||
@@ -101,14 +80,14 @@ namespace Jellyfin2Samsung.Helpers
|
||||
// ignore cleanup errors
|
||||
}
|
||||
|
||||
Trace.WriteLine($" ✓ Transpiled {relPathForLog ?? "unknown"} via esbuild");
|
||||
Trace.WriteLine($"Transpiled {relPathForLog ?? "unknown"} via esbuild");
|
||||
return transpiled;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ esbuild transpile error for {relPathForLog ?? "unknown"}: {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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,12 @@ using Jellyfin2Samsung.Models;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public class PackageHelper(
|
||||
ITizenInstallerService tizenInstaller,
|
||||
@@ -22,9 +21,10 @@ namespace Jellyfin2Samsung.Helpers
|
||||
private readonly IDialogService _dialogService = dialogService;
|
||||
private readonly INetworkService _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
|
||||
{
|
||||
@@ -39,13 +39,30 @@ namespace Jellyfin2Samsung.Helpers
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public async Task<bool> InstallPackageAsync(string packagePath, NetworkDevice selectedDevice, CancellationToken cancellationToken, ProgressCallback? progress = null, Action? onSamsungLoginStarted = 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))
|
||||
{
|
||||
@@ -80,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;
|
||||
}
|
||||
@@ -127,8 +163,10 @@ namespace Jellyfin2Samsung.Helpers
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public async Task<bool> InstallCustomPackagesAsync(string[] packagePaths, NetworkDevice device, CancellationToken cancellationToken, Action<string> onProgress, Action? onSamsungLoginStarted = null)
|
||||
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;
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public sealed class PackageWorkspace : IDisposable
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
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
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
public class ProcessHelper
|
||||
{
|
||||
@@ -37,7 +36,7 @@ namespace Jellyfin2Samsung.Helpers
|
||||
if (string.IsNullOrWhiteSpace(arguments))
|
||||
return string.Empty;
|
||||
|
||||
var matches = System.Text.RegularExpressions.Regex.Matches(arguments, @"[\""].+?[\""]|[^ ]+");
|
||||
var matches = RegexPatterns.CommandLine.Arguments.Matches(arguments);
|
||||
|
||||
var firstTwo = matches.Cast<System.Text.RegularExpressions.Match>()
|
||||
.Take(1)
|
||||
@@ -71,7 +70,8 @@ namespace Jellyfin2Samsung.Helpers
|
||||
if (string.IsNullOrEmpty(sanitizedArguments))
|
||||
sanitizedArguments = "unknown";
|
||||
|
||||
string logFilePath = Path.Combine(exeDir, $"process_{sanitizedArguments}_{timestamp}.log");
|
||||
string logFolder = Path.Combine(exeDir, "Logs");
|
||||
string logFilePath = Path.Combine(logFolder, $"process_{sanitizedArguments}_{timestamp}.log");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -122,7 +122,7 @@ namespace Jellyfin2Samsung.Helpers
|
||||
|
||||
public async Task MakeExecutableAsync(string filePath)
|
||||
{
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
if (PlatformService.RequiresExecutablePermissions())
|
||||
{
|
||||
try
|
||||
{
|
||||
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,121 +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}");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Error parsing JSON response: {ex}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Unexpected error: {ex}");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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,182 +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<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();
|
||||
|
||||
// Regex to extract the package prefix before .Jellyfin
|
||||
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);
|
||||
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 regex = new Regex(
|
||||
$@"<tizen:application\s+id=""{oldPkg}\.Jellyfin""\s+package=""{oldPkg}""",
|
||||
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)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public static class HtmlUtils
|
||||
{
|
||||
public static string EnsureBaseHref(string html)
|
||||
{
|
||||
if (html.Contains("<base", System.StringComparison.OrdinalIgnoreCase))
|
||||
return Regex.Replace(html, @"<base[^>]+>", "<base href=\".\">");
|
||||
|
||||
return html.Replace("<head>", "<head><base href=\".\">");
|
||||
}
|
||||
|
||||
public static string RewriteLocalPaths(string html)
|
||||
{
|
||||
html = Regex.Replace(html, @"(src|href)=""[^""]*/web/([^""]+)""", "$1=\"$2\"");
|
||||
return html;
|
||||
}
|
||||
|
||||
public static string CleanAndApplyCsp(string html)
|
||||
{
|
||||
html = Regex.Replace(html, @"<meta[^>]*Content-Security-Policy[^>]*>", "");
|
||||
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>");
|
||||
}
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.IO;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers.Jellyfin.Diagnostic
|
||||
{
|
||||
public class JellyfinBootloaderInjector
|
||||
public class JellyfinDiagnostic
|
||||
{
|
||||
public async Task InjectDevLogsAsync(PackageWorkspace ws)
|
||||
{
|
||||
@@ -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,180 +0,0 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
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 JellyfinApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public JellyfinApiClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public static bool IsValidJellyfinConfiguration()
|
||||
{
|
||||
return !string.IsNullOrEmpty(AppSettings.Default.JellyfinIP) &&
|
||||
!string.IsNullOrEmpty(AppSettings.Default.JellyfinApiKey) &&
|
||||
AppSettings.Default.JellyfinApiKey.Length == 32 &&
|
||||
IsValidUrl($"{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>> LoadUsersAsync()
|
||||
{
|
||||
var users = new List<JellyfinAuth>();
|
||||
if (!IsValidJellyfinConfiguration()) return users;
|
||||
|
||||
try
|
||||
{
|
||||
SetupHeaders();
|
||||
using var response = await _httpClient.GetAsync($"{AppSettings.Default.JellyfinIP}/Users");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var jellyfinUsers = JsonSerializer.Deserialize<List<JellyfinAuth>>(json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (jellyfinUsers != null)
|
||||
users.AddRange(jellyfinUsers);
|
||||
|
||||
if (users.Count > 1)
|
||||
users.Add(new JellyfinAuth { Id = "everyone", Name = "Everyone" });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Error loading users: {ex}");
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
public async Task<List<JellyfinPluginInfo>> GetInstalledPluginsAsync(string serverUrl)
|
||||
{
|
||||
var list = new List<JellyfinPluginInfo>();
|
||||
try
|
||||
{
|
||||
string url = serverUrl.TrimEnd('/') + "/Plugins";
|
||||
Trace.WriteLine("▶ Fetching installed plugins from: " + url);
|
||||
var json = await _httpClient.GetStringAsync(url);
|
||||
var parsed = JsonSerializer.Deserialize<List<JellyfinPluginInfo>>(json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
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 = serverUrl.TrimEnd('/') + "/System/Info/Public";
|
||||
Trace.WriteLine("▶ Fetching Jellyfin public system info from: " + url);
|
||||
|
||||
var json = await _httpClient.GetStringAsync(url);
|
||||
|
||||
var info = JsonSerializer.Deserialize<JellyfinPublicSystemInfo>(
|
||||
json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
return info;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine("⚠ Failed to fetch /System/Info/Public: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateUserConfigurationsAsync(string[] userIds)
|
||||
{
|
||||
if (userIds == null || userIds.Length == 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
SetupHeaders();
|
||||
|
||||
foreach (string userId in userIds.Where(u => !string.IsNullOrWhiteSpace(u)))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Fetch user info
|
||||
var getUserResponse = await _httpClient.GetAsync($"{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($"{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($"{AppSettings.Default.JellyfinIP}/Users/Configuration?userId={userId}", configContent);
|
||||
configResponse.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception userEx)
|
||||
{
|
||||
Trace.WriteLine($"Failed to update configuration for user {userId}: {userEx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"General error updating user configurations: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupHeaders()
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("SamsungJellyfinInstaller/1.0");
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization", $"MediaBrowser Token=\"{AppSettings.Default.JellyfinApiKey}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
using System;
|
||||
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;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class JellyfinHtmlPatcher
|
||||
{
|
||||
private readonly JellyfinPluginPatcher _plugins;
|
||||
|
||||
public JellyfinHtmlPatcher(
|
||||
HttpClient http,
|
||||
JellyfinApiClient api,
|
||||
PluginManager plugins)
|
||||
{
|
||||
_plugins = new JellyfinPluginPatcher(http, api, plugins);
|
||||
}
|
||||
|
||||
public async Task PatchServerIndexAsync(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 js = new StringBuilder();
|
||||
|
||||
await _plugins.PatchPluginsAsync(ws, serverUrl, css, js);
|
||||
|
||||
html = html.Replace("</head>", css + "\n</head>");
|
||||
html = html.Replace("</body>", js + "\n</body>");
|
||||
|
||||
html = HtmlUtils.CleanAndApplyCsp(html);
|
||||
html = HtmlUtils.EnsurePublicJsIsLast(html);
|
||||
|
||||
await File.WriteAllTextAsync(index, html);
|
||||
}
|
||||
public async Task UpdateMultiServerConfigAsync(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 = AppSettings.Default.JellyfinIP.TrimEnd('/');
|
||||
|
||||
// Avoid duplicates
|
||||
if (!servers.Any(s => s?.GetValue<string>() == serverUrl))
|
||||
{
|
||||
servers.Add(serverUrl);
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(path, config.ToJsonString());
|
||||
|
||||
}
|
||||
public async Task InjectUserSettingsAsync(PackageWorkspace ws, string[] userIds)
|
||||
{
|
||||
if (userIds == null || userIds.Length == 0) return;
|
||||
|
||||
string index = Path.Combine(ws.Root, "www", "index.html");
|
||||
if (!File.Exists(index)) return;
|
||||
|
||||
var html = await File.ReadAllTextAsync(index);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<script>");
|
||||
sb.AppendLine("window.JellyfinUserSettings={SelectedUsers:[");
|
||||
sb.AppendLine(string.Join(",", userIds));
|
||||
sb.AppendLine("]};</script>");
|
||||
|
||||
html = html.Replace("</body>", sb + "\n</body>");
|
||||
await File.WriteAllTextAsync(index, html);
|
||||
}
|
||||
public async Task EnsureTizenCorsAsync(PackageWorkspace ws)
|
||||
{
|
||||
string path = Path.Combine(ws.Root, "config.xml");
|
||||
Trace.WriteLine($"[EnsureTizenCors] config.xml path = {path}");
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Trace.WriteLine("[EnsureTizenCors] ERROR: config.xml not found");
|
||||
throw new FileNotFoundException("config.xml not found", path);
|
||||
}
|
||||
|
||||
XDocument doc;
|
||||
await using (var stream = File.OpenRead(path))
|
||||
{
|
||||
doc = await XDocument.LoadAsync(stream, LoadOptions.PreserveWhitespace, default);
|
||||
}
|
||||
|
||||
Trace.WriteLine("[EnsureTizenCors] config.xml loaded");
|
||||
|
||||
var widget = doc.Root;
|
||||
if (widget == null || widget.Name.LocalName != "widget")
|
||||
{
|
||||
Trace.WriteLine("[EnsureTizenCors] ERROR: Invalid root element");
|
||||
throw new InvalidOperationException("Invalid Tizen config.xml");
|
||||
}
|
||||
|
||||
XNamespace widgetNs = widget.Name.Namespace;
|
||||
XNamespace tizenNs =
|
||||
widget.GetNamespaceOfPrefix("tizen")
|
||||
?? "http://tizen.org/ns/widgets";
|
||||
|
||||
Trace.WriteLine($"[EnsureTizenCors] widget namespace = {widgetNs}");
|
||||
Trace.WriteLine($"[EnsureTizenCors] tizen namespace = {tizenNs}");
|
||||
|
||||
// --------------------------------
|
||||
// Ensure <access origin="*" />
|
||||
// --------------------------------
|
||||
var accessElements = widget.Elements(widgetNs + "access").ToList();
|
||||
Trace.WriteLine($"[EnsureTizenCors] access elements found = {accessElements.Count}");
|
||||
|
||||
bool hasAccess = accessElements
|
||||
.Any(e => (string?)e.Attribute("origin") == "*");
|
||||
|
||||
Trace.WriteLine($"[EnsureTizenCors] has access origin=\"*\" = {hasAccess}");
|
||||
|
||||
if (!hasAccess)
|
||||
{
|
||||
widget.AddFirst(
|
||||
new XElement(widgetNs + "access",
|
||||
new XAttribute("origin", "*"),
|
||||
new XAttribute("subdomains", "true")));
|
||||
|
||||
Trace.WriteLine("[EnsureTizenCors] Added <access origin=\"*\" subdomains=\"true\" />");
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// Ensure internet privilege
|
||||
// --------------------------------
|
||||
var privilegeElements = widget.Elements(tizenNs + "privilege").ToList();
|
||||
Trace.WriteLine($"[EnsureTizenCors] privilege elements found = {privilegeElements.Count}");
|
||||
|
||||
bool hasInternetPrivilege = privilegeElements.Any(e =>
|
||||
(string?)e.Attribute("name") ==
|
||||
"http://tizen.org/privilege/internet");
|
||||
|
||||
Trace.WriteLine($"[EnsureTizenCors] has internet privilege = {hasInternetPrivilege}");
|
||||
|
||||
if (!hasInternetPrivilege)
|
||||
{
|
||||
var inputDevicePrivilege = privilegeElements.FirstOrDefault(e =>
|
||||
(string?)e.Attribute("name") ==
|
||||
"http://tizen.org/privilege/tv.inputdevice");
|
||||
|
||||
var internetPrivilege = new XElement(
|
||||
tizenNs + "privilege",
|
||||
new XAttribute("name", "http://tizen.org/privilege/internet"));
|
||||
|
||||
if (inputDevicePrivilege != null)
|
||||
{
|
||||
inputDevicePrivilege.AddAfterSelf(internetPrivilege);
|
||||
Trace.WriteLine("[EnsureTizenCors] Inserted internet privilege after tv.inputdevice");
|
||||
}
|
||||
else
|
||||
{
|
||||
var access = widget.Elements(widgetNs + "access").FirstOrDefault();
|
||||
if (access != null)
|
||||
{
|
||||
access.AddAfterSelf(internetPrivilege);
|
||||
Trace.WriteLine("[EnsureTizenCors] Inserted internet privilege after <access>");
|
||||
}
|
||||
else
|
||||
{
|
||||
widget.AddFirst(internetPrivilege);
|
||||
Trace.WriteLine("[EnsureTizenCors] Inserted internet privilege at top of <widget>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await using (var stream = File.Create(path))
|
||||
{
|
||||
await doc.SaveAsync(stream, SaveOptions.None, default);
|
||||
}
|
||||
|
||||
Trace.WriteLine("[EnsureTizenCors] config.xml saved successfully");
|
||||
}
|
||||
public async Task PatchYoutubePlayerAsync(PackageWorkspace ws)
|
||||
{
|
||||
var www = Path.Combine(ws.Root, "www");
|
||||
if (!Directory.Exists(www))
|
||||
return;
|
||||
|
||||
var files = Directory.GetFiles(www, "youtubePlayer-plugin.*.js");
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var js = await File.ReadAllTextAsync(file);
|
||||
|
||||
if (js.Contains("origin:\"https://www.youtube.com\""))
|
||||
continue;
|
||||
|
||||
const string marker = "playerVars:{";
|
||||
var idx = js.IndexOf(marker, StringComparison.Ordinal);
|
||||
|
||||
if (idx == -1)
|
||||
continue;
|
||||
|
||||
var start = idx + marker.Length;
|
||||
var end = js.IndexOf('}', start);
|
||||
|
||||
if (end == -1)
|
||||
continue;
|
||||
|
||||
var playerVars = js.Substring(start, end - start);
|
||||
|
||||
var patchedPlayerVars =
|
||||
playerVars +
|
||||
",origin:\"https://www.youtube.com\"" +
|
||||
",host:\"https://www.youtube.com\"";
|
||||
|
||||
js =
|
||||
js.Substring(0, start) +
|
||||
patchedPlayerVars +
|
||||
js.Substring(end);
|
||||
|
||||
await File.WriteAllTextAsync(file, js);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,387 +0,0 @@
|
||||
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.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class JellyfinPluginPatcher
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly JellyfinApiClient _apiClient;
|
||||
private readonly PluginManager _pluginManager;
|
||||
|
||||
// Track by the actual src/href string that will appear in HTML
|
||||
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 jsBuilder)
|
||||
{
|
||||
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,
|
||||
jsBuilder);
|
||||
}
|
||||
|
||||
await ProcessApiPluginsAsync(
|
||||
serverUrl,
|
||||
pluginCacheDir,
|
||||
jsBuilder,
|
||||
cssBuilder);
|
||||
}
|
||||
|
||||
private async Task<string> FetchServerIndexAsync(string serverUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = serverUrl.TrimEnd('/') + "/web/index.html";
|
||||
Trace.WriteLine($"▶ Fetching server index.html: {url}");
|
||||
|
||||
using var cts = new System.Threading.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 = Regex.Matches(
|
||||
serverHtml,
|
||||
@"<link[^>]+href=[""']([^""']+)[""'][^>]*>",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
foreach (Match m in cssMatches)
|
||||
{
|
||||
string href = m.Groups[1].Value;
|
||||
if (!IsLikelyPluginAsset(href)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
Uri uri = GetAbsoluteUri(serverUrl, href);
|
||||
string fileName = Path.GetFileName(uri.AbsolutePath);
|
||||
|
||||
if (!fileName.EndsWith(".css", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
string localPath = Path.Combine(pluginCacheDir, fileName);
|
||||
var bytes = await _httpClient.GetByteArrayAsync(uri);
|
||||
await File.WriteAllBytesAsync(localPath, bytes);
|
||||
|
||||
// Dedup by the actual href we will inject
|
||||
string outHref = $"plugin_cache/{fileName}";
|
||||
AppendStyleOnce(cssBuilder, outHref);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ Failed to cache plugin CSS '{href}': {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- JS ---
|
||||
var jsMatches = Regex.Matches(
|
||||
serverHtml,
|
||||
@"<script[^>]+src=[""']([^""']+)[""'][^>]*>[\s\S]*?<\/script>",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
foreach (Match m in jsMatches)
|
||||
{
|
||||
string jsUrl = m.Groups[1].Value;
|
||||
if (string.IsNullOrWhiteSpace(jsUrl)) continue;
|
||||
|
||||
string lower = jsUrl.ToLowerInvariant();
|
||||
if (IsCoreBundle(lower)) continue;
|
||||
|
||||
bool isPlugin = IsLikelyPluginAsset(jsUrl);
|
||||
|
||||
// API-managed plugins are injected in ProcessApiPluginsAsync.
|
||||
if (IsApiManagedPlugin(jsUrl))
|
||||
continue;
|
||||
|
||||
if (lower.Contains("jellyfinenhanced"))
|
||||
continue;
|
||||
|
||||
if (!jsUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase) && isPlugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri uri = GetAbsoluteUri(serverUrl, jsUrl);
|
||||
string fileName = Path.GetFileName(uri.AbsolutePath);
|
||||
if (!fileName.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
|
||||
fileName += ".js";
|
||||
|
||||
string localPath = Path.Combine(pluginCacheDir, fileName);
|
||||
string jsContent = await _httpClient.GetStringAsync(uri);
|
||||
jsContent = await EsbuildHelper.TranspileAsync(jsContent, uri.ToString());
|
||||
|
||||
await File.WriteAllTextAsync(localPath, jsContent);
|
||||
|
||||
string outSrc = $"plugin_cache/{fileName}";
|
||||
AppendScriptOnce(jsBuilder, $"<script src=\"{outSrc}\"></script>", outSrc);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ Failed to cache plugin JS '{jsUrl}': {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// External plugin JS
|
||||
if (jsUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase) && isPlugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
string jsContent = await _httpClient.GetStringAsync(jsUrl);
|
||||
jsContent = await EsbuildHelper.TranspileAsync(jsContent, jsUrl);
|
||||
|
||||
var wrapped = new StringBuilder();
|
||||
wrapped.AppendLine("window.WaitForApiClient(function(){ try {");
|
||||
wrapped.AppendLine(jsContent);
|
||||
wrapped.AppendLine("} catch(e) { console.error('Plugin Error:', e); } });");
|
||||
|
||||
string fileName = $"plugin_{Guid.NewGuid():N}.js";
|
||||
string localPath = Path.Combine(pluginCacheDir, fileName);
|
||||
await File.WriteAllTextAsync(localPath, wrapped.ToString());
|
||||
|
||||
string outSrc = $"plugin_cache/{fileName}";
|
||||
AppendScriptOnce(jsBuilder, $"<script src=\"{outSrc}\"></script>", outSrc);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ Plugin JS (external) failed: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessApiPluginsAsync(
|
||||
string serverUrl,
|
||||
string pluginCacheDir,
|
||||
StringBuilder jsBuilder,
|
||||
StringBuilder cssBuilder)
|
||||
{
|
||||
var apiPlugins = await _apiClient.GetInstalledPluginsAsync(serverUrl);
|
||||
var apiJsBuilder = new StringBuilder();
|
||||
string? enhancedMainScript = null;
|
||||
|
||||
foreach (var plugin in apiPlugins)
|
||||
{
|
||||
Trace.WriteLine($"⚙ Processing plugin: {plugin.Name} ({plugin.Id})");
|
||||
var entry = _pluginManager.FindPluginEntry(plugin);
|
||||
if (entry == null) continue;
|
||||
|
||||
string name = entry.Name.ToLowerInvariant();
|
||||
bool isMediaBar = name.Contains("media bar");
|
||||
bool isHomeScreenSections = name.Contains("home screen");
|
||||
|
||||
// --- Explicit server files (Enhanced) ---
|
||||
if (entry.ExplicitServerFiles?.Count > 0)
|
||||
{
|
||||
enhancedMainScript =
|
||||
entry.ExplicitServerFiles
|
||||
.Find(p => p.EndsWith("/script"));
|
||||
|
||||
await _pluginManager.DownloadExplicitFilesAsync(
|
||||
serverUrl,
|
||||
pluginCacheDir,
|
||||
entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- Fallback JS ---
|
||||
foreach (string url in entry.FallbackUrls)
|
||||
{
|
||||
string cleanName = Regex.Replace(
|
||||
entry.Name.ToLowerInvariant(),
|
||||
"[^a-z0-9]",
|
||||
"");
|
||||
|
||||
string fileName = Path.GetFileName(new Uri(url).AbsolutePath);
|
||||
string relPath = Path.Combine(cleanName, fileName);
|
||||
|
||||
string? path = await _pluginManager.DownloadAndTranspileAsync(
|
||||
url,
|
||||
pluginCacheDir,
|
||||
relPath);
|
||||
|
||||
if (path != null)
|
||||
{
|
||||
string injectedSrc = $"plugin_cache/{cleanName}/{fileName}";
|
||||
AppendScriptOnce(jsBuilder, $"<script src=\"{injectedSrc}\"></script>", injectedSrc);
|
||||
//string tag = _pluginManager.GenerateInjectorLoader(entry.Name, injectedSrc);
|
||||
|
||||
// Dedup by the injected src (not the whole tag)
|
||||
//AppendScriptOnce(apiJsBuilder, tag, injectedSrc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- CSS injection ---
|
||||
if (isMediaBar)
|
||||
{
|
||||
try
|
||||
{
|
||||
string cssUrl =
|
||||
"https://cdn.jsdelivr.net/gh/IAmParadox27/jellyfin-plugin-media-bar@main/slideshowpure.css";
|
||||
|
||||
string fileName = "slideshowpure.css";
|
||||
string localPath = Path.Combine(pluginCacheDir, "mediabar", fileName);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(localPath)!);
|
||||
var bytes = await _httpClient.GetByteArrayAsync(cssUrl);
|
||||
await File.WriteAllBytesAsync(localPath, bytes);
|
||||
|
||||
string href = $"plugin_cache/mediabar/{fileName}";
|
||||
AppendStyleOnce(cssBuilder, href);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ MediaBar CSS failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
if (isHomeScreenSections)
|
||||
{
|
||||
try
|
||||
{
|
||||
string cssUrl =
|
||||
"https://raw.githubusercontent.com/IAmParadox27/jellyfin-plugin-home-sections/main/src/Jellyfin.Plugin.HomeScreenSections/Inject/HomeScreenSections.css";
|
||||
|
||||
string fileName = "HomeScreenSections.css";
|
||||
string localPath = Path.Combine(pluginCacheDir, "homescreensections", fileName);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(localPath)!);
|
||||
var bytes = await _httpClient.GetByteArrayAsync(cssUrl);
|
||||
await File.WriteAllBytesAsync(localPath, bytes);
|
||||
|
||||
string href = $"plugin_cache/homescreensections/{fileName}";
|
||||
AppendStyleOnce(cssBuilder, href);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ HomeScreenSections CSS failed: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced script last (dedup by src)
|
||||
if (!string.IsNullOrEmpty(enhancedMainScript))
|
||||
{
|
||||
string rel = enhancedMainScript.TrimStart('/');
|
||||
if (!rel.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) rel += ".js";
|
||||
|
||||
string outSrc = $"plugin_cache/{rel}";
|
||||
AppendScriptOnce(jsBuilder, $"<script src=\"{outSrc}\"></script>", outSrc);
|
||||
}
|
||||
|
||||
if (apiJsBuilder.Length > 0)
|
||||
{
|
||||
// This whole block might be appended multiple times by caller;
|
||||
// individual scripts inside are already deduped above.
|
||||
jsBuilder.AppendLine(apiJsBuilder.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private Uri GetAbsoluteUri(string serverUrl, string rel)
|
||||
{
|
||||
if (Uri.IsWellFormedUriString(rel, UriKind.Absolute))
|
||||
return new Uri(rel);
|
||||
|
||||
return new Uri(new Uri(serverUrl.TrimEnd('/') + "/"), rel.TrimStart('/'));
|
||||
}
|
||||
|
||||
private bool IsLikelyPluginAsset(string url)
|
||||
{
|
||||
string lower = url.ToLowerInvariant();
|
||||
return lower.Contains("/plugins/")
|
||||
|| lower.Contains("javascriptinjector")
|
||||
|| lower.Contains("filetransformation")
|
||||
|| lower.Contains("jellyfinenhanced")
|
||||
|| lower.Contains("mediabar")
|
||||
|| lower.Contains("kefin")
|
||||
|| lower.Contains("homescreensections");
|
||||
}
|
||||
|
||||
private bool IsCoreBundle(string lower)
|
||||
{
|
||||
return lower.Contains("main.")
|
||||
|| lower.Contains("runtime")
|
||||
|| lower.Contains("react")
|
||||
|| lower.Contains("mui")
|
||||
|| lower.Contains("tanstack");
|
||||
}
|
||||
|
||||
private bool IsApiManagedPlugin(string url)
|
||||
{
|
||||
string lower = url.ToLowerInvariant();
|
||||
|
||||
return lower.Contains("homescreensections")
|
||||
|| lower.Contains("mediabar")
|
||||
|| lower.Contains("kefin")
|
||||
|| lower.Contains("editorschoice")
|
||||
|| lower.Contains("pluginpages");
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class JellyfinWebPackagePatcher
|
||||
{
|
||||
private readonly JellyfinHtmlPatcher _html;
|
||||
private readonly JellyfinBootloaderInjector _boot;
|
||||
|
||||
public JellyfinWebPackagePatcher(HttpClient http)
|
||||
{
|
||||
var api = new JellyfinApiClient(http);
|
||||
var plugins = new PluginManager(http, api);
|
||||
|
||||
_html = new JellyfinHtmlPatcher(http, api, plugins);
|
||||
_boot = new JellyfinBootloaderInjector();
|
||||
}
|
||||
|
||||
public async Task<InstallResult> ApplyJellyfinConfigAsync(string packagePath, string[] userIds)
|
||||
{
|
||||
using var ws = PackageWorkspace.Extract(packagePath);
|
||||
|
||||
if (AppSettings.Default.ConfigUpdateMode.Contains("Server") ||
|
||||
AppSettings.Default.ConfigUpdateMode.Contains("All"))
|
||||
{
|
||||
await _html.EnsureTizenCorsAsync(ws);
|
||||
|
||||
if (AppSettings.Default.UseServerScripts)
|
||||
await _html.PatchServerIndexAsync(ws, AppSettings.Default.JellyfinIP);
|
||||
|
||||
if(AppSettings.Default.PatchYoutubePlugin)
|
||||
await _html.PatchYoutubePlayerAsync(ws);
|
||||
|
||||
await _html.UpdateMultiServerConfigAsync(ws);
|
||||
}
|
||||
|
||||
if (AppSettings.Default.ConfigUpdateMode.Contains("Browser") ||
|
||||
AppSettings.Default.ConfigUpdateMode.Contains("All"))
|
||||
{
|
||||
Trace.WriteLine("Injecting user settings into browser index.html...");
|
||||
await _html.InjectUserSettingsAsync(ws, userIds);
|
||||
}
|
||||
|
||||
if (AppSettings.Default.EnableDevLogs)
|
||||
{
|
||||
Trace.WriteLine("Injecting dev logs...");
|
||||
await _boot.InjectDevLogsAsync(ws);
|
||||
}
|
||||
|
||||
ws.Repack();
|
||||
return InstallResult.SuccessResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,675 +0,0 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class PluginManager
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly JellyfinApiClient _apiClient;
|
||||
|
||||
public PluginManager(HttpClient httpClient, JellyfinApiClient apiClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_apiClient = apiClient;
|
||||
}
|
||||
|
||||
private const string KefinTweaksRawRoot = "https://raw.githubusercontent.com/ranaldsgift/KefinTweaks/v0.4.5/";
|
||||
|
||||
private static readonly Regex kefinScriptRegex = new Regex(
|
||||
@"script\.src\s*=\s*([`""])[^`""']*kefinTweaks-plugin\.js\1",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled
|
||||
);
|
||||
|
||||
|
||||
private static readonly List<PluginMatrixEntry> PluginMatrix = new()
|
||||
{
|
||||
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(),
|
||||
UseBabel = true
|
||||
},
|
||||
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"
|
||||
},
|
||||
UseBabel = true
|
||||
}
|
||||
};
|
||||
|
||||
public PluginMatrixEntry? FindPluginEntry(JellyfinPluginInfo plugin)
|
||||
{
|
||||
if (plugin?.Name == null)
|
||||
return null;
|
||||
|
||||
string pluginName = plugin.Name.ToLowerInvariant();
|
||||
|
||||
return PluginMatrix.FirstOrDefault(entry =>
|
||||
pluginName.Contains(entry.Name.ToLowerInvariant()));
|
||||
}
|
||||
|
||||
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 = serverUrl.TrimEnd('/') + rel;
|
||||
Trace.WriteLine($" → Fetch Enhanced JS: {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);
|
||||
|
||||
string logPath = outPath.Replace(pluginCacheDir + Path.DirectorySeparatorChar, "plugin_cache/");
|
||||
Trace.WriteLine($" ✓ Saved {logPath}");
|
||||
if (rel.Equals("/JellyfinEnhanced/script", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Trace.WriteLine(" 🔧 Patching Enhanced main script (script.js)...");
|
||||
await PatchEnhancedMainScript(outPath);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($" ⚠ Failed Enhanced JS '{rel}': {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
private string PatchHomeScreenSections(string js)
|
||||
{
|
||||
// Idempotent safety
|
||||
if (js.Contains("__HSS_INIT__"))
|
||||
return js;
|
||||
|
||||
return js + @"
|
||||
|
||||
(function () {
|
||||
function waitForHome() {
|
||||
if (!window.ApiClient) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var home =
|
||||
document.querySelector('.homePage') ||
|
||||
document.querySelector('[data-page=""home""]');
|
||||
|
||||
if (!home) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (window.__HSS_INIT__) {
|
||||
console.log('[HSS] already initialized, skipping');
|
||||
return true;
|
||||
}
|
||||
|
||||
window.__HSS_INIT__ = true;
|
||||
console.log('[HSS] init (home detected)');
|
||||
HomeScreenSectionsHandler2.init();
|
||||
return true;
|
||||
}
|
||||
|
||||
var tries = 0;
|
||||
var timer = setInterval(function () {
|
||||
tries++;
|
||||
|
||||
if (waitForHome()) {
|
||||
clearInterval(timer);
|
||||
console.log('[HSS] init complete');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tries > 50) {
|
||||
clearInterval(timer);
|
||||
console.warn('[HSS] init aborted (home not detected)');
|
||||
}
|
||||
}, 100);
|
||||
})();
|
||||
";
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (relPath.Contains("HomeScreenSections", StringComparison.OrdinalIgnoreCase))
|
||||
js = PatchHomeScreenSections(js);
|
||||
|
||||
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 PatchJavaScriptInjectorPublicJsAsync(string pluginCacheDir, string serverUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
string publicJsPath = Path.Combine(pluginCacheDir, "public.js");
|
||||
if (!File.Exists(publicJsPath))
|
||||
{
|
||||
Trace.WriteLine("▶ JavaScript Injector: public.js not found, skipping patch.");
|
||||
return;
|
||||
}
|
||||
|
||||
string js = await File.ReadAllTextAsync(publicJsPath, Encoding.UTF8);
|
||||
|
||||
// 1. Detect the original CDN root
|
||||
if (!kefinScriptRegex.IsMatch(js))
|
||||
{
|
||||
Trace.WriteLine("▶ JavaScript Injector: No KefinTweaks script loader found in public.js, skipping patch.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Ensure kefinTweaks folder exists
|
||||
Directory.CreateDirectory(Path.Combine(pluginCacheDir, "kefinTweaks"));
|
||||
|
||||
// 2. Download KefinTweaks main files
|
||||
string kefinTweaksPluginUrl = KefinTweaksRawRoot + "kefinTweaks-plugin.js";
|
||||
await DownloadAndTranspileAsync(
|
||||
kefinTweaksPluginUrl,
|
||||
pluginCacheDir,
|
||||
Path.Combine("kefinTweaks", "kefinTweaks-plugin.js"));
|
||||
|
||||
var pluginJsPath = Path.Combine(pluginCacheDir, "kefinTweaks", "kefinTweaks-plugin.js");
|
||||
if (File.Exists(pluginJsPath))
|
||||
{
|
||||
var pluginJs = await File.ReadAllTextAsync(pluginJsPath, Encoding.UTF8);
|
||||
|
||||
pluginJs = pluginJs.Replace(
|
||||
"https://cdn.jsdelivr.net/gh/ranaldsgift/KefinTweaks",
|
||||
"plugin_cache/kefinTweaks"
|
||||
);
|
||||
|
||||
await File.WriteAllTextAsync(pluginJsPath, pluginJs, Encoding.UTF8);
|
||||
}
|
||||
|
||||
|
||||
string injectorUrl = KefinTweaksRawRoot + "injector.js";
|
||||
string? injectorPath = await DownloadAndTranspileAsync(
|
||||
injectorUrl,
|
||||
pluginCacheDir,
|
||||
Path.Combine("kefinTweaks", "injector.js"));
|
||||
|
||||
if (injectorPath != null)
|
||||
{
|
||||
await ProcessKefinTweaksModulesAsync(pluginCacheDir, KefinTweaksRawRoot);
|
||||
await ProcessKefinTweaksCssAsync(pluginCacheDir, KefinTweaksRawRoot);
|
||||
|
||||
// 3. Resolve selected skin
|
||||
var skin = GetKefinDefaultSkin(pluginCacheDir);
|
||||
Trace.WriteLine($"⚙ KefinTweaks: detected default skin: {skin ?? "null"}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(skin))
|
||||
{
|
||||
var info = await _apiClient.GetPublicSystemInfoAsync(serverUrl);
|
||||
var version = info?.Version ?? "0.0.0";
|
||||
var majorMinor = string.Join(".", version.Split('.').Take(2));
|
||||
|
||||
string skinLower = skin.ToLowerInvariant();
|
||||
string skinsDir = Path.Combine(pluginCacheDir, "kefinTweaks", "skins", "css");
|
||||
Directory.CreateDirectory(skinsDir);
|
||||
|
||||
string localTheme = Path.Combine(skinsDir, $"{skinLower}-kefin.css");
|
||||
string localFixes = Path.Combine(skinsDir, $"{skinLower}-kefin-{majorMinor}.css");
|
||||
|
||||
string themeHref =
|
||||
$"plugin_cache/kefinTweaks/skins/css/{skinLower}-kefin.css";
|
||||
string fixesHref =
|
||||
$"plugin_cache/kefinTweaks/skins/css/{skinLower}-kefin-{majorMinor}.css";
|
||||
|
||||
// Theme.css
|
||||
if (!File.Exists(localTheme))
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"https://cdn.jsdelivr.net/gh/n00bcodr/{skinLower}@main/theme.css";
|
||||
Trace.WriteLine($"▶ KefinTweaks: downloading theme CSS: {url}");
|
||||
|
||||
var css = await _httpClient.GetStringAsync(url);
|
||||
await File.WriteAllTextAsync(localTheme, css);
|
||||
|
||||
js = EnsureCssLinked(js, themeHref);
|
||||
}
|
||||
catch (Exception ex) when (ex is System.Net.Http.HttpRequestException || ex is System.IO.IOException)
|
||||
{
|
||||
Trace.WriteLine($"⚠ KefinTweaks: Failed to download theme CSS: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!File.Exists(localFixes))
|
||||
{
|
||||
try
|
||||
{
|
||||
var fixesUrl =
|
||||
$"https://cdn.jsdelivr.net/gh/n00bcodr/{skinLower}@main/{majorMinor}_fixes.css";
|
||||
Trace.WriteLine($"▶ KefinTweaks: downloading fixes CSS: {fixesUrl}");
|
||||
var css = await _httpClient.GetStringAsync(fixesUrl);
|
||||
await File.WriteAllTextAsync(localFixes, css);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Trace.WriteLine("ℹ KefinTweaks: no version-specific fixes found");
|
||||
}
|
||||
}
|
||||
|
||||
if (File.Exists(localFixes))
|
||||
{
|
||||
js = EnsureCssLinked(js, fixesHref);
|
||||
}
|
||||
|
||||
Trace.WriteLine($"✓ KefinTweaks skin cached & injected: {skin} ({majorMinor})");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Rewrite CDN → local
|
||||
js = kefinScriptRegex.Replace(
|
||||
js,
|
||||
"script.src = 'plugin_cache/kefinTweaks/kefinTweaks-plugin.js';"
|
||||
);
|
||||
|
||||
// Rewrite KefinTweaks config root → local cache
|
||||
js = Regex.Replace(
|
||||
js,
|
||||
@"""kefinTweaksRoot""\s*:\s*""https:\/\/cdn\.jsdelivr\.net\/gh\/ranaldsgift\/KefinTweaks@latest\/""",
|
||||
@"""kefinTweaksRoot"": ""plugin_cache/kefinTweaks/""",
|
||||
RegexOptions.IgnoreCase
|
||||
);
|
||||
|
||||
|
||||
// 5. Write ONCE
|
||||
await File.WriteAllTextAsync(publicJsPath, js, Encoding.UTF8);
|
||||
Trace.WriteLine("✓ JavaScript Injector: public.js patched successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ JavaScript Injector error: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessKefinTweaksModulesAsync(string pluginCacheDir, string downloadRoot)
|
||||
{
|
||||
try
|
||||
{
|
||||
string injectorPath = Path.Combine(pluginCacheDir, "kefinTweaks", "injector.js");
|
||||
if (!File.Exists(injectorPath))
|
||||
{
|
||||
Trace.WriteLine("▶ KefinTweaks: injector.js not found in plugin_cache, skipping module prefetch.");
|
||||
return;
|
||||
}
|
||||
|
||||
string injectorSource = await File.ReadAllTextAsync(injectorPath, Encoding.UTF8);
|
||||
|
||||
// Grab all `script: "something.js"` entries from SCRIPT_DEFINITIONS
|
||||
var scriptRegex = new Regex(@"script\s*:\s*""([^""]+)""", RegexOptions.IgnoreCase);
|
||||
var matches = scriptRegex.Matches(injectorSource);
|
||||
|
||||
// ... (rest of ProcessKefinTweaksModulesAsync remains unchanged) ...
|
||||
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
Trace.WriteLine("▶ KefinTweaks: no SCRIPT_DEFINITIONS scripts found in injector.js, nothing to prefetch.");
|
||||
return;
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (Match m in matches)
|
||||
{
|
||||
string scriptName = m.Groups[1].Value.Trim();
|
||||
|
||||
// 1. Skip empty strings.
|
||||
if (string.IsNullOrEmpty(scriptName))
|
||||
continue;
|
||||
|
||||
// 2. NEW FIX: Only process files that end with a .js extension to avoid malformed entries.
|
||||
if (!scriptName.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Trace.WriteLine($" ⚠ KefinTweaks: Skipping non-JS module or malformed entry: {scriptName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Avoid duplicates
|
||||
if (!seen.Add(scriptName))
|
||||
continue;
|
||||
|
||||
// Explicitly skip files located in the root (kefinTweaks-plugin.js and injector.js)
|
||||
if (string.Equals(scriptName, "kefinTweaks-plugin.js", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(scriptName, "injector.js", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string modulePath = scriptName;
|
||||
|
||||
// --- Logic to handle nested files in GitHub repo ---
|
||||
|
||||
// 1. Check for the single deeply nested file in 'scripts/third party/'
|
||||
if (scriptName.Equals("jquery.flurry.min.js", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Use URL encoding for the space in the folder name for the download URL
|
||||
modulePath = "scripts/third%20party/" + scriptName;
|
||||
}
|
||||
// 2. All other module files (e.g., utils.js, collections.js, etc.)
|
||||
// are assumed to be in the top-level 'scripts/' directory of the repo.
|
||||
else
|
||||
{
|
||||
modulePath = "scripts/" + scriptName;
|
||||
}
|
||||
|
||||
// Use the reliable downloadRoot (raw GitHub)
|
||||
string url = downloadRoot + modulePath;
|
||||
|
||||
// The local path for the WGT must handle the 'third party' folder correctly.
|
||||
string localModulePath = modulePath.Replace("%20", " ");
|
||||
string relPath = Path.Combine("kefinTweaks", localModulePath.Replace("/", Path.DirectorySeparatorChar.ToString()));
|
||||
|
||||
await DownloadAndTranspileAsync(url, pluginCacheDir, relPath);
|
||||
}
|
||||
|
||||
Trace.WriteLine(" ✓ KefinTweaks: module scripts downloaded & transpiled into plugin_cache/kefinTweaks/");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ KefinTweaks: error while processing modules: {ex}");
|
||||
}
|
||||
}
|
||||
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 async Task ProcessKefinTweaksCssAsync(string pluginCacheDir, string downloadRoot)
|
||||
{
|
||||
// List of CSS files found in the KefinTweaks 'skins' directory
|
||||
var cssFiles = new List<string>
|
||||
{
|
||||
"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: Pre-fetching CSS skins...");
|
||||
|
||||
foreach (var fileName in cssFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// The CSS files are in the 'skins/' directory of the repo
|
||||
string repoPath = $"skins/{fileName}";
|
||||
string url = downloadRoot + repoPath;
|
||||
|
||||
// The local path must preserve the 'optional/ElegantFin/' structure
|
||||
string localRelPath = Path.Combine("kefinTweaks","skins","css",fileName);
|
||||
string localFullPath = Path.Combine(pluginCacheDir, localRelPath);
|
||||
|
||||
// Ensure directory exists
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(localFullPath)!);
|
||||
|
||||
// Download CSS directly (no transpile needed)
|
||||
var cssBytes = await _httpClient.GetByteArrayAsync(url);
|
||||
await File.WriteAllBytesAsync(localFullPath, cssBytes);
|
||||
|
||||
Trace.WriteLine($" ✓ Downloaded CSS skin: {fileName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($" ⚠ Failed to fetch KefinTweaks CSS '{fileName}': {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PatchEnhancedMainScript(string scriptPath)
|
||||
{
|
||||
if (!File.Exists(scriptPath))
|
||||
return;
|
||||
|
||||
string original = await File.ReadAllTextAsync(scriptPath);
|
||||
|
||||
string patch = @"
|
||||
// ---- J2S SCRIPT PATCH: FORCE LOCAL ENHANCED MODULE LOADING ----
|
||||
(function () {
|
||||
|
||||
// Rewrite only Enhanced JS URLs (absolute or relative) to plugin_cache
|
||||
function rewriteEnhancedUrl(url) {
|
||||
try {
|
||||
if (typeof url !== 'string') return url;
|
||||
|
||||
// Strip ?v=timestamp etc
|
||||
var base = url.split('?')[0];
|
||||
|
||||
// ---- ONLY REWRITE .js FILES ----
|
||||
if (
|
||||
base.endsWith('.js') &&
|
||||
base.indexOf('/JellyfinEnhanced/') !== -1
|
||||
) {
|
||||
// Handle both absolute and relative URLs
|
||||
var idx = base.indexOf('/JellyfinEnhanced/');
|
||||
var sub = base.substring(idx + '/JellyfinEnhanced/'.length);
|
||||
return 'plugin_cache/JellyfinEnhanced/' + sub;
|
||||
}
|
||||
|
||||
// ---- DO NOT REWRITE JSON OR CONFIG ----
|
||||
return url;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('J2S rewriteEnhancedUrl failed', e);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// Patch document.createElement so any script.src is rewritten ONLY for JS modules
|
||||
var _createElement = document.createElement;
|
||||
document.createElement = function (tag) {
|
||||
var el = _createElement.call(document, tag);
|
||||
|
||||
if (tag && tag.toLowerCase() === 'script') {
|
||||
|
||||
var _setAttribute = el.setAttribute;
|
||||
|
||||
// Intercept setAttribute('src', ...)
|
||||
el.setAttribute = function (name, value) {
|
||||
if (name === 'src') {
|
||||
value = rewriteEnhancedUrl(value);
|
||||
}
|
||||
return _setAttribute.call(el, name, value);
|
||||
};
|
||||
|
||||
// Intercept direct assignment: script.src = '...'
|
||||
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;
|
||||
};
|
||||
|
||||
// Optional: rewrite fetch() requests ONLY for JS Enhanced modules
|
||||
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: script.js loader patched to use plugin_cache for Enhanced JS modules');
|
||||
})();
|
||||
";
|
||||
string combined = patch + "\n\n" + original;
|
||||
await File.WriteAllTextAsync(scriptPath, combined);
|
||||
}
|
||||
private static string? GetKefinDefaultSkin(string pluginCacheDir)
|
||||
{
|
||||
var publicJs = Path.Combine(pluginCacheDir, "public.js");
|
||||
Trace.WriteLine($"SEARCHRING FOR {publicJs}");
|
||||
if (!File.Exists(publicJs))
|
||||
return null;
|
||||
|
||||
var text = File.ReadAllText(publicJs);
|
||||
|
||||
Trace.WriteLine(publicJs);
|
||||
|
||||
var match = Regex.Match(
|
||||
text,
|
||||
@"""defaultSkin""\s*:\s*""([^""]+)""",
|
||||
RegexOptions.IgnoreCase
|
||||
);
|
||||
|
||||
return match.Success ? match.Groups[1].Value : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
@@ -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,6 +15,7 @@ namespace Jellyfin2Samsung.Interfaces
|
||||
string GetLocalIPAddress();
|
||||
string InvertIPAddress(string ipAddress);
|
||||
Task<string?> GetManufacturerFromIp(string ipAddress);
|
||||
Task<string?> GetPrimaryOutboundIPAddressAsync();
|
||||
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();
|
||||
}
|
||||
}
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
||||
<AvaloniaResource Update="Assets\esbuild\linux-x64\esbuild">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AvaloniaResource>
|
||||
|
||||
@@ -6,5 +6,6 @@ namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
[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,6 +5,15 @@
|
||||
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; }
|
||||
|
||||
@@ -12,18 +12,21 @@ namespace Jellyfin2Samsung.Models
|
||||
public class PluginMatrixEntry
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string IdContains { get; set; }
|
||||
public string ServerPath { get; set; }
|
||||
public List<string> ExplicitServerFiles { get; set; }
|
||||
public List<string> FallbackUrls { get; set; }
|
||||
public bool UseBabel { get; set; }
|
||||
public bool RequiresModuleBundle { get; set; }
|
||||
public string ModuleRepoApiRoot { get; set; }
|
||||
public string ModuleBundleFileName { 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
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,10 @@ namespace Jellyfin2Samsung
|
||||
{
|
||||
// Register trace listener BEFORE Avalonia starts
|
||||
var logDir = AppContext.BaseDirectory;
|
||||
|
||||
string logFolder = Path.Combine(logDir, "Logs");
|
||||
Directory.CreateDirectory(logFolder);
|
||||
var dtg = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss-fff");
|
||||
var logFile = Path.Combine(logDir, $"debug_{dtg}.log");
|
||||
var logFile = Path.Combine(logFolder, $"debug_{dtg}.log");
|
||||
|
||||
Trace.Listeners.Add(new FileTraceListener(logFile));
|
||||
Trace.AutoFlush = true;
|
||||
@@ -31,4 +32,4 @@ namespace Jellyfin2Samsung
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,31 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Markup.Xaml.Styling;
|
||||
using Avalonia.Media;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using Avalonia.Styling;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class DialogService : IDialogService
|
||||
{
|
||||
private static IBrush GetThemeBrush(string resourceKey, bool isDarkMode)
|
||||
{
|
||||
var themeVariant = isDarkMode ? ThemeVariant.Dark : ThemeVariant.Light;
|
||||
if (Application.Current?.TryFindResource(resourceKey, themeVariant, out var resource) == true && resource is IBrush brush)
|
||||
{
|
||||
return brush;
|
||||
}
|
||||
// Ultimate fallback
|
||||
return resourceKey.Contains("Background")
|
||||
? (isDarkMode ? Brushes.Black : Brushes.White)
|
||||
: (isDarkMode ? Brushes.White : Brushes.Black);
|
||||
}
|
||||
|
||||
private Window? GetMainWindow()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
|
||||
@@ -27,6 +42,9 @@ namespace Jellyfin2Samsung.Services
|
||||
string yesText = "Yes",
|
||||
string noText = "No")
|
||||
{
|
||||
// Get theme from AppSettings
|
||||
var isDarkMode = AppSettings.Default.DarkMode;
|
||||
|
||||
var dialog = new Window
|
||||
{
|
||||
Title = title,
|
||||
@@ -35,11 +53,22 @@ namespace Jellyfin2Samsung.Services
|
||||
MaxWidth = 600,
|
||||
CanResize = false,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Background = Brushes.White,
|
||||
CornerRadius = new CornerRadius(12),
|
||||
SizeToContent = SizeToContent.Height, // dynamic height
|
||||
RequestedThemeVariant = isDarkMode ? ThemeVariant.Dark : ThemeVariant.Light
|
||||
};
|
||||
|
||||
// Apply FluentTheme
|
||||
dialog.Styles.Add(new StyleInclude(new Uri("avares://Jellyfin2Samsung"))
|
||||
{
|
||||
Source = new Uri("avares://Avalonia.Themes.Fluent/FluentTheme.xaml")
|
||||
});
|
||||
|
||||
// Get colors from theme resources (same as main UI)
|
||||
var backgroundBrush = GetThemeBrush("SystemControlBackgroundAltHighBrush", isDarkMode);
|
||||
var foregroundBrush = GetThemeBrush("SystemControlForegroundBaseHighBrush", isDarkMode);
|
||||
dialog.Background = backgroundBrush;
|
||||
|
||||
var mainPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Vertical,
|
||||
@@ -52,6 +81,7 @@ namespace Jellyfin2Samsung.Services
|
||||
Text = title,
|
||||
FontSize = 18,
|
||||
FontWeight = FontWeight.Bold,
|
||||
Foreground = foregroundBrush,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 0, 10)
|
||||
});
|
||||
@@ -117,10 +147,14 @@ namespace Jellyfin2Samsung.Services
|
||||
public async Task ShowMessageAsync(string title, string message)
|
||||
{
|
||||
var window = GetMainWindow();
|
||||
var isDarkMode = AppSettings.Default.DarkMode;
|
||||
var foregroundBrush = GetThemeBrush("SystemControlForegroundBaseHighBrush", isDarkMode);
|
||||
|
||||
var dialog = CreateStyledDialog(title, new TextBlock
|
||||
{
|
||||
Text = message,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = foregroundBrush,
|
||||
FontSize = 14,
|
||||
Margin = new Thickness(0, 5, 0, 0)
|
||||
});
|
||||
@@ -149,11 +183,14 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
var window = owner ?? GetMainWindow();
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
var isDarkMode = AppSettings.Default.DarkMode;
|
||||
var foregroundBrush = GetThemeBrush("SystemControlForegroundBaseHighBrush", isDarkMode);
|
||||
|
||||
var dialog = CreateStyledDialog(title, new TextBlock
|
||||
{
|
||||
Text = message,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = foregroundBrush,
|
||||
FontSize = 14,
|
||||
Margin = new Thickness(0, 5, 0, 0)
|
||||
}, showButtons: true, tcs: tcs, yesText: yesText, noText: noText);
|
||||
|
||||
@@ -5,100 +5,120 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class LocalizationService : ILocalizationService
|
||||
{
|
||||
private const string DefaultLanguage = "en";
|
||||
private const string LocalizationFolderUri = "avares://Jellyfin2Samsung/Assets/Localization/";
|
||||
|
||||
private Dictionary<string, string> _currentStrings = new();
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _allStrings = new();
|
||||
private string _currentLanguage = "en";
|
||||
private string _currentLanguage = DefaultLanguage;
|
||||
|
||||
public string CurrentLanguage => _currentLanguage;
|
||||
public IEnumerable<string> AvailableLanguages => _allStrings.Keys;
|
||||
public IEnumerable<string> AvailableLanguages => _allStrings.Keys.OrderBy(x => x);
|
||||
public event EventHandler? LanguageChanged;
|
||||
|
||||
public LocalizationService()
|
||||
{
|
||||
LoadLanguagesAsync();
|
||||
LoadLanguages();
|
||||
}
|
||||
|
||||
private void LoadLanguagesAsync()
|
||||
private void LoadLanguages()
|
||||
{
|
||||
var languages = new[] { "en", "da", "nl", "fr", "de" };
|
||||
_allStrings.Clear();
|
||||
|
||||
foreach (var lang in languages)
|
||||
var folderUri = new Uri(LocalizationFolderUri);
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri($"avares://Jellyfin2Samsung/Assets/Localization/{lang}.json");
|
||||
var asset = AssetLoader.Open(uri);
|
||||
var assetUris = AssetLoader.GetAssets(folderUri, null);
|
||||
|
||||
using var reader = new StreamReader(asset);
|
||||
var json = reader.ReadToEnd();
|
||||
var strings = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
|
||||
if (strings != null)
|
||||
{
|
||||
_allStrings[lang] = strings;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
foreach (var assetUri in assetUris)
|
||||
{
|
||||
System.Diagnostics.Trace.WriteLine($"Failed to load language {lang}: {ex}");
|
||||
if (!assetUri.AbsolutePath.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(assetUri.AbsolutePath);
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
continue;
|
||||
|
||||
TryLoadLanguage(fileName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Trace.WriteLine($"Failed to enumerate localization assets: {ex}");
|
||||
}
|
||||
|
||||
// Always make sure English is attempted as fallback language
|
||||
if (!_allStrings.ContainsKey(DefaultLanguage))
|
||||
{
|
||||
TryLoadLanguage(DefaultLanguage);
|
||||
}
|
||||
|
||||
// Set initial language based on system culture
|
||||
var systemLang = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
|
||||
string configLang = AppSettings.Default.Language;
|
||||
var configLang = AppSettings.Default.Language;
|
||||
|
||||
if (string.IsNullOrEmpty(configLang))
|
||||
var initialLang =
|
||||
!string.IsNullOrWhiteSpace(configLang) && _allStrings.ContainsKey(configLang)
|
||||
? configLang
|
||||
: _allStrings.ContainsKey(systemLang)
|
||||
? systemLang
|
||||
: DefaultLanguage;
|
||||
|
||||
SetLanguage(initialLang);
|
||||
}
|
||||
|
||||
private void TryLoadLanguage(string lang)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_allStrings.ContainsKey(systemLang))
|
||||
var uri = new Uri($"{LocalizationFolderUri}{lang}.json");
|
||||
|
||||
using var asset = AssetLoader.Open(uri);
|
||||
using var reader = new StreamReader(asset);
|
||||
var json = reader.ReadToEnd();
|
||||
|
||||
var strings = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (strings != null)
|
||||
{
|
||||
SetLanguage(systemLang);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLanguage("en");
|
||||
_allStrings[lang] = strings;
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetLanguage(configLang);
|
||||
System.Diagnostics.Trace.WriteLine($"Failed to load language {lang}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public string GetString(string key)
|
||||
{
|
||||
if (_currentStrings.TryGetValue(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// Fallback to English if key not found in current language
|
||||
if (_currentLanguage != "en" && _allStrings.TryGetValue("en", out var englishStrings))
|
||||
{
|
||||
if (englishStrings.TryGetValue(key, out var englishValue))
|
||||
{
|
||||
return englishValue;
|
||||
}
|
||||
}
|
||||
if (_allStrings.TryGetValue(DefaultLanguage, out var englishStrings) &&
|
||||
englishStrings.TryGetValue(key, out var englishValue))
|
||||
return englishValue;
|
||||
|
||||
// Return the key itself if no translation found
|
||||
return key;
|
||||
}
|
||||
|
||||
public void SetLanguage(string languageCode)
|
||||
{
|
||||
if (_allStrings.TryGetValue(languageCode, out var strings))
|
||||
if (!_allStrings.TryGetValue(languageCode, out var strings))
|
||||
{
|
||||
_currentLanguage = languageCode;
|
||||
_currentStrings = strings;
|
||||
LanguageChanged?.Invoke(this, EventArgs.Empty);
|
||||
languageCode = DefaultLanguage;
|
||||
strings = _allStrings.GetValueOrDefault(DefaultLanguage, new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
_currentLanguage = languageCode;
|
||||
_currentStrings = strings;
|
||||
LanguageChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
@@ -9,7 +10,6 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -19,8 +19,6 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
private readonly ITizenInstallerService _tizenInstaller;
|
||||
private static readonly HttpClient _httpClient = new HttpClient();
|
||||
private const int tvPort = 26101;
|
||||
private const int scanTimeoutMs = 1000;
|
||||
|
||||
public NetworkService(ITizenInstallerService tizenInstaller)
|
||||
{
|
||||
@@ -36,13 +34,13 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(scanTimeoutMs);
|
||||
using var cts = new CancellationTokenSource(Constants.Defaults.NetworkScanTimeoutMs);
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cts.Token, cancellationToken);
|
||||
|
||||
if (await IsPortOpenAsync(ip, tvPort, linkedCts.Token))
|
||||
if (await IsPortOpenAsync(ip, Constants.Ports.TizenDevPort, linkedCts.Token))
|
||||
{
|
||||
if (await IsPortOpenAsync(ip, 8001, linkedCts.Token))
|
||||
if (await IsPortOpenAsync(ip, Constants.Ports.SamsungTvApiPort, linkedCts.Token))
|
||||
{
|
||||
var manufacturer = await GetManufacturerFromIp(ip);
|
||||
var device = new NetworkDevice
|
||||
@@ -71,26 +69,28 @@ namespace Jellyfin2Samsung.Services
|
||||
public async Task<IEnumerable<NetworkDevice>> FindTizenTvsAsync(CancellationToken cancellationToken = default, bool virtualScan = false)
|
||||
{
|
||||
var foundDevices = new List<NetworkDevice>();
|
||||
var localIps = GetRelevantLocalIPs(virtualScan);
|
||||
var localInfos = GetLocalNetworkInfos(virtualScan);
|
||||
var lockObject = new object();
|
||||
|
||||
// Group by network prefix to avoid scanning the same network multiple times
|
||||
var uniqueNetworks = localIps
|
||||
.Select(ip => GetNetworkPrefix(ip))
|
||||
.Distinct()
|
||||
// Deduplicate by actual network address so overlapping interfaces don't double-scan
|
||||
var uniqueNetworks = localInfos
|
||||
.Select(info => (
|
||||
Network: GetNetworkAddress(info.Address, info.Mask),
|
||||
Broadcast: GetBroadcastAddress(info.Address, info.Mask)
|
||||
))
|
||||
.DistinctBy(r => r.Network.ToString())
|
||||
.ToList();
|
||||
|
||||
await Task.WhenAll(uniqueNetworks.SelectMany(networkPrefix =>
|
||||
Enumerable.Range(1, 254)
|
||||
.Select(i => $"{networkPrefix}.{i}")
|
||||
await Task.WhenAll(uniqueNetworks.SelectMany(range =>
|
||||
GetHostAddresses(range.Network, range.Broadcast)
|
||||
.Select(async ip =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(scanTimeoutMs);
|
||||
using var cts = new CancellationTokenSource(Constants.Defaults.NetworkScanTimeoutMs);
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cts.Token, cancellationToken);
|
||||
if (await IsPortOpenAsync(ip, tvPort, linkedCts.Token))
|
||||
if (await IsPortOpenAsync(ip, Constants.Ports.TizenDevPort, linkedCts.Token))
|
||||
{
|
||||
var manufacturer = await GetManufacturerFromIp(ip);
|
||||
var device = new NetworkDevice
|
||||
@@ -111,7 +111,7 @@ namespace Jellyfin2Samsung.Services
|
||||
catch { /* Ignore scan failures */ }
|
||||
})));
|
||||
|
||||
Trace.WriteLine($"Scan complete! Found {foundDevices.Count} devices with port {tvPort} open.");
|
||||
Trace.WriteLine($"Scan complete! Found {foundDevices.Count} devices with port {Constants.Ports.TizenDevPort} open.");
|
||||
return foundDevices;
|
||||
}
|
||||
public IEnumerable<IPAddress> GetRelevantLocalIPs(bool virtualScan = false)
|
||||
@@ -130,7 +130,7 @@ namespace Jellyfin2Samsung.Services
|
||||
.ToList();
|
||||
|
||||
var additionalIps = Enumerable.Empty<string>();
|
||||
if (AppSettings.Default.RememberCustomIP && !string.IsNullOrEmpty(AppSettings.Default.UserCustomIP))
|
||||
if (!string.IsNullOrEmpty(AppSettings.Default.UserCustomIP))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -170,10 +170,103 @@ namespace Jellyfin2Samsung.Services
|
||||
}
|
||||
}
|
||||
|
||||
private string GetNetworkPrefix(IPAddress ip)
|
||||
// Returns all local interface IPs with their actual subnet masks.
|
||||
// Falls back to /24 for the user-supplied custom IP since its mask can't be discovered.
|
||||
private List<(IPAddress Address, IPAddress Mask)> GetLocalNetworkInfos(bool virtualScan = false)
|
||||
{
|
||||
var infos = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(ni => ni.OperationalStatus == OperationalStatus.Up)
|
||||
.Where(ni =>
|
||||
virtualScan ||
|
||||
ni.NetworkInterfaceType == NetworkInterfaceType.Ethernet ||
|
||||
ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211)
|
||||
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
|
||||
.Where(ua => ua.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
.Where(ua => !IPAddress.IsLoopback(ua.Address))
|
||||
.Where(ua => ua.IPv4Mask != null && !ua.IPv4Mask.Equals(IPAddress.Any))
|
||||
.Select(ua => (Address: ua.Address, Mask: ua.IPv4Mask))
|
||||
.ToList();
|
||||
|
||||
if (!string.IsNullOrEmpty(AppSettings.Default.UserCustomIP) &&
|
||||
IPAddress.TryParse(AppSettings.Default.UserCustomIP, out var customIp))
|
||||
{
|
||||
// Reuse the mask from a local interface whose network contains the custom IP;
|
||||
// otherwise fall back to /24 so we still scan the right /24 segment.
|
||||
var fallback = IPAddress.Parse("255.255.255.0");
|
||||
var matchingMask = infos
|
||||
.FirstOrDefault(i =>
|
||||
GetNetworkAddress(i.Address, i.Mask).Equals(GetNetworkAddress(customIp, i.Mask)))
|
||||
.Mask ?? fallback;
|
||||
infos.Add((customIp, matchingMask));
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
private static IPAddress GetNetworkAddress(IPAddress ip, IPAddress mask)
|
||||
{
|
||||
var ipBytes = ip.GetAddressBytes();
|
||||
var maskBytes = mask.GetAddressBytes();
|
||||
var result = new byte[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
result[i] = (byte)(ipBytes[i] & maskBytes[i]);
|
||||
return new IPAddress(result);
|
||||
}
|
||||
|
||||
private static IPAddress GetBroadcastAddress(IPAddress ip, IPAddress mask)
|
||||
{
|
||||
var ipBytes = ip.GetAddressBytes();
|
||||
var maskBytes = mask.GetAddressBytes();
|
||||
var result = new byte[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
result[i] = (byte)(ipBytes[i] | (byte)~maskBytes[i]);
|
||||
return new IPAddress(result);
|
||||
}
|
||||
|
||||
// Enumerates usable host addresses for a subnet (excludes network and broadcast addresses).
|
||||
// Caps at 1022 hosts (/22) to keep scans practical; larger subnets are narrowed to the
|
||||
// /24 block that contains the network address.
|
||||
private static IEnumerable<string> GetHostAddresses(IPAddress networkAddress, IPAddress broadcastAddress)
|
||||
{
|
||||
uint netInt = IpToUInt(networkAddress);
|
||||
uint broadInt = IpToUInt(broadcastAddress);
|
||||
uint hostCount = broadInt - netInt - 1;
|
||||
|
||||
if (hostCount > 1022)
|
||||
{
|
||||
// Narrow to /24 to avoid scanning thousands of addresses
|
||||
var bytes = networkAddress.GetAddressBytes();
|
||||
netInt = IpToUInt(new IPAddress(new byte[] { bytes[0], bytes[1], bytes[2], 0 }));
|
||||
broadInt = netInt + 255;
|
||||
}
|
||||
|
||||
for (uint i = netInt + 1; i < broadInt; i++)
|
||||
yield return UIntToIp(i);
|
||||
}
|
||||
|
||||
private static uint IpToUInt(IPAddress ip)
|
||||
{
|
||||
var bytes = ip.GetAddressBytes();
|
||||
return $"{bytes[0]}.{bytes[1]}.{bytes[2]}";
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
|
||||
return BitConverter.ToUInt32(bytes, 0);
|
||||
}
|
||||
|
||||
private static string UIntToIp(uint value)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
|
||||
return $"{bytes[0]}.{bytes[1]}.{bytes[2]}.{bytes[3]}";
|
||||
}
|
||||
|
||||
// Looks up the subnet mask assigned to a local interface IP.
|
||||
private static IPAddress? GetMaskForLocalIp(IPAddress target)
|
||||
{
|
||||
return NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(ni => ni.OperationalStatus == OperationalStatus.Up)
|
||||
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
|
||||
.Where(ua => ua.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
.FirstOrDefault(ua => ua.Address.Equals(target))
|
||||
?.IPv4Mask;
|
||||
}
|
||||
|
||||
public async Task<string?> GetManufacturerFromIp(string ipAddress)
|
||||
@@ -186,7 +279,7 @@ namespace Jellyfin2Samsung.Services
|
||||
|
||||
private static async Task<string?> GetMacAddressFromIp(string ipAddress)
|
||||
{
|
||||
string arpArgs = OperatingSystem.IsWindows() ? $"-a {ipAddress}" : $"-n {ipAddress}";
|
||||
string arpArgs = PlatformService.GetArpArguments(ipAddress);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -206,7 +299,7 @@ namespace Jellyfin2Samsung.Services
|
||||
string output = await process.StandardOutput.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
var match = Regex.Match(output, @"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})");
|
||||
var match = RegexPatterns.Network.MacAddress.Match(output);
|
||||
return match.Success ? match.Value : null;
|
||||
}
|
||||
catch
|
||||
@@ -248,35 +341,54 @@ namespace Jellyfin2Samsung.Services
|
||||
Array.Reverse(parts);
|
||||
return string.Join(".", parts);
|
||||
}
|
||||
public async Task<string?> GetPrimaryOutboundIPAddressAsync()
|
||||
public bool IsDifferentSubnet(string ip1, string ip2)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
if (!IPAddress.TryParse(ip1, out var a) || !IPAddress.TryParse(ip2, out var b))
|
||||
return false;
|
||||
|
||||
// Use the actual mask from the local interface; fall back to /24 if not found
|
||||
var mask = GetMaskForLocalIp(a) ?? IPAddress.Parse("255.255.255.0");
|
||||
|
||||
var aBytes = a.GetAddressBytes();
|
||||
var bBytes = b.GetAddressBytes();
|
||||
var maskBytes = mask.GetAddressBytes();
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
if ((aBytes[i] & maskBytes[i]) != (bBytes[i] & maskBytes[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public Task<IReadOnlyList<NetworkInterfaceOption>> GetNetworkInterfaceOptionsAsync()
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var result = new List<NetworkInterfaceOption>();
|
||||
|
||||
foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
|
||||
{
|
||||
var ipProps = ni.GetIPProperties();
|
||||
if (ni.OperationalStatus != OperationalStatus.Up)
|
||||
continue;
|
||||
|
||||
var gateway = ipProps.GatewayAddresses
|
||||
.FirstOrDefault(g =>
|
||||
g.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(g.Address));
|
||||
|
||||
if (gateway != null)
|
||||
foreach (var ua in ni.GetIPProperties().UnicastAddresses)
|
||||
{
|
||||
var ipv4 = ipProps.UnicastAddresses
|
||||
.FirstOrDefault(ua =>
|
||||
ua.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ua.Address));
|
||||
if (ua.Address.AddressFamily != AddressFamily.InterNetwork)
|
||||
continue;
|
||||
|
||||
if (ipv4 != null)
|
||||
return ipv4.Address.ToString();
|
||||
if (IPAddress.IsLoopback(ua.Address))
|
||||
continue;
|
||||
|
||||
result.Add(new NetworkInterfaceOption
|
||||
{
|
||||
Name = ni.Name,
|
||||
IpAddress = ua.Address.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return (IReadOnlyList<NetworkInterfaceOption>)result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
using Avalonia.Controls;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
@@ -17,13 +16,12 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class SamsungLoginService
|
||||
{
|
||||
private IWebHost _callbackServer;
|
||||
private const string LoopbackHost = "localhost";
|
||||
private const int FixedPort = 4794;
|
||||
private IWebHost? _callbackServer;
|
||||
|
||||
private string _callbackUrl => $"http://{LoopbackHost}:{FixedPort}/signin/callback";
|
||||
private string CallbackUrl =>
|
||||
$"http://{Constants.Samsung.LoopbackHost}:{Constants.Ports.SamsungLoginCallbackPort}{Constants.Samsung.CallbackPath}";
|
||||
|
||||
public Action<SamsungAuth> CallbackReceived;
|
||||
public Action<SamsungAuth>? CallbackReceived;
|
||||
|
||||
public static Task<SamsungAuth> PerformSamsungLoginAsync()
|
||||
{
|
||||
@@ -50,15 +48,15 @@ namespace Jellyfin2Samsung.Services
|
||||
await service.StartCallbackServer();
|
||||
|
||||
string loginUrl =
|
||||
$"https://account.samsung.com/accounts/be1dce529476c1a6d407c4c7578c31bd/signInGate" +
|
||||
$"?locale=&clientId=v285zxnl3h" +
|
||||
$"&redirect_uri={HttpUtility.UrlEncode(service._callbackUrl)}" +
|
||||
$"&state=accountcheckdogeneratedstatetext" +
|
||||
$"&tokenType=TOKEN";
|
||||
$"{Constants.Samsung.SignInGateUrl}" +
|
||||
$"?locale=&clientId={Constants.Samsung.OAuthClientId}" +
|
||||
$"&redirect_uri={HttpUtility.UrlEncode(service.CallbackUrl)}" +
|
||||
$"&state={Constants.Samsung.OAuthState}" +
|
||||
$"&tokenType={Constants.Samsung.TokenType}";
|
||||
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = loginUrl,
|
||||
UseShellExecute = true
|
||||
@@ -84,17 +82,18 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
_callbackServer = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.UseUrls($"http://{LoopbackHost}:{FixedPort}")
|
||||
.UseUrls($"http://{Constants.Samsung.LoopbackHost}:{Constants.Ports.SamsungLoginCallbackPort}")
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Run(async context =>
|
||||
{
|
||||
if (context.Request.Path == "/signin/callback" && context.Request.Method == "POST")
|
||||
if (context.Request.Path == Constants.Samsung.CallbackPath &&
|
||||
context.Request.Method == "POST")
|
||||
{
|
||||
string body = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
||||
|
||||
string state = null;
|
||||
string codeEncoded = null;
|
||||
string? state = null;
|
||||
string? codeEncoded = null;
|
||||
|
||||
var parts = body.Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
@@ -119,7 +118,10 @@ namespace Jellyfin2Samsung.Services
|
||||
|
||||
try
|
||||
{
|
||||
var auth = JsonConvert.DeserializeObject<SamsungAuth>(codeEncoded);
|
||||
var auth = JsonSerializer.Deserialize<SamsungAuth>(
|
||||
codeEncoded,
|
||||
JsonSerializerOptionsProvider.Default);
|
||||
|
||||
if (auth != null)
|
||||
{
|
||||
auth.state = state;
|
||||
@@ -134,7 +136,7 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
await context.Response.WriteAsync(
|
||||
$"[CallbackServer] JSON parse error: {ex}\n\nDecoded JSON:\n{codeEncoded}");
|
||||
$"[CallbackServer] JSON parse error: {ex.Message}\n\nDecoded JSON:\n{codeEncoded}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,8 +153,8 @@ namespace Jellyfin2Samsung.Services
|
||||
|
||||
await _callbackServer.StartAsync();
|
||||
|
||||
System.Diagnostics.Trace.WriteLine(
|
||||
$"[SamsungLoginService] Bound to http://{LoopbackHost}:{FixedPort}");
|
||||
Trace.WriteLine(
|
||||
$"[SamsungLoginService] Bound to http://{Constants.Samsung.LoopbackHost}:{Constants.Ports.SamsungLoginCallbackPort}");
|
||||
}
|
||||
|
||||
public async Task StopCallbackServer()
|
||||
@@ -165,4 +167,4 @@ namespace Jellyfin2Samsung.Services
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
Jellyfin2Samsung-CrossOS/Services/ThemeService.cs
Normal file
37
Jellyfin2Samsung-CrossOS/Services/ThemeService.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Styling;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using System;
|
||||
|
||||
namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class ThemeService : IThemeService
|
||||
{
|
||||
public bool IsDarkMode => AppSettings.Default.DarkMode;
|
||||
|
||||
public event EventHandler<bool>? ThemeChanged;
|
||||
|
||||
public void SetTheme(bool isDarkMode)
|
||||
{
|
||||
if (AppSettings.Default.DarkMode == isDarkMode)
|
||||
return;
|
||||
|
||||
AppSettings.Default.DarkMode = isDarkMode;
|
||||
AppSettings.Default.Save();
|
||||
|
||||
ApplyTheme();
|
||||
ThemeChanged?.Invoke(this, isDarkMode);
|
||||
}
|
||||
|
||||
public void ApplyTheme()
|
||||
{
|
||||
if (Application.Current is null)
|
||||
return;
|
||||
|
||||
Application.Current.RequestedThemeVariant = AppSettings.Default.DarkMode
|
||||
? ThemeVariant.Dark
|
||||
: ThemeVariant.Light;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Helpers.Tizen.Certificate;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Org.BouncyCastle.Asn1;
|
||||
using Org.BouncyCastle.Asn1.Pkcs;
|
||||
@@ -10,6 +12,7 @@ using Org.BouncyCastle.OpenSsl;
|
||||
using Org.BouncyCastle.Pkcs;
|
||||
using Org.BouncyCastle.Security;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -25,7 +28,7 @@ namespace Jellyfin2Samsung.Services
|
||||
private readonly IDialogService _dialogService;
|
||||
|
||||
public TizenCertificateService(
|
||||
HttpClient httpClient,
|
||||
HttpClient httpClient,
|
||||
IDialogService dialogService)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
@@ -90,7 +93,9 @@ namespace Jellyfin2Samsung.Services
|
||||
|
||||
private static byte[] GenerateAuthorCsr(AsymmetricCipherKeyPair keyPair)
|
||||
{
|
||||
var subject = new X509Name("C=, ST=, L=, O=, OU=, CN=Jelly2Sams");
|
||||
var subject = new X509Name(
|
||||
new ArrayList { X509Name.C, X509Name.ST, X509Name.L, X509Name.O, X509Name.OU, X509Name.CN },
|
||||
new ArrayList { "", "", "", "", "", "Jelly2Sams" });
|
||||
var csr = new Pkcs10CertificationRequest("SHA256withRSA", subject, keyPair.Public, null, keyPair.Private);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
@@ -105,7 +110,9 @@ namespace Jellyfin2Samsung.Services
|
||||
|
||||
private static byte[] GenerateDistributorCsr(AsymmetricCipherKeyPair keyPair, string duid, string userEmail)
|
||||
{
|
||||
var subject = new X509Name($"CN=TizenSDK, OU=, O=, L=, ST=, C=, emailAddress={userEmail}");
|
||||
var subject = new X509Name(
|
||||
new ArrayList { X509Name.CN, X509Name.OU, X509Name.O, X509Name.L, X509Name.ST, X509Name.C, X509Name.EmailAddress },
|
||||
new ArrayList { "TizenSDK", "", "", "", "", "", userEmail });
|
||||
var generalNames = new GeneralNames(new[]
|
||||
{
|
||||
new GeneralName(GeneralName.UniformResourceIdentifier, "URN:tizen:packageid="),
|
||||
@@ -144,7 +151,7 @@ namespace Jellyfin2Samsung.Services
|
||||
|
||||
// Simplified Http Post for Author CSR
|
||||
private async Task<byte[]> PostAuthorCsrAsync(byte[] csrData, string accessToken, string userId)
|
||||
{
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, AppSettings.Default.AuthorEndpoint_V3);
|
||||
var content = new MultipartFormDataContent
|
||||
{
|
||||
@@ -196,7 +203,7 @@ namespace Jellyfin2Samsung.Services
|
||||
};
|
||||
|
||||
var v3Request = new HttpRequestMessage(HttpMethod.Post, AppSettings.Default.DistributorsEndpoint_V3);
|
||||
v3Request.Content = v3Content;
|
||||
v3Request.Content = v3Content;
|
||||
var v3Response = await _httpClient.SendAsync(v3Request);
|
||||
|
||||
if (!v3Response.IsSuccessStatusCode)
|
||||
@@ -290,7 +297,7 @@ namespace Jellyfin2Samsung.Services
|
||||
}
|
||||
|
||||
// --- Optional sanity check ---
|
||||
if (OperatingSystem.IsWindows())
|
||||
if (PlatformService.IsWindows)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -318,10 +325,7 @@ namespace Jellyfin2Samsung.Services
|
||||
}
|
||||
private static X509KeyStorageFlags GetX509KeyStorageFlags()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
return X509KeyStorageFlags.EphemeralKeySet;
|
||||
|
||||
return X509KeyStorageFlags.PersistKeySet;
|
||||
return PlatformService.GetX509KeyStorageFlags();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user