Compare commits

...

8 Commits

Author SHA1 Message Date
renovate[bot]
168bf2e54e chore(deps): Update github/codeql-action action to v4.36.2 (#1687)
Some checks are pending
🏗️ Build Apps / 🍎 Build tvOS IPA (Unsigned) (push) Waiting to run
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Waiting to run
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Waiting to run
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Waiting to run
🏗️ Build Apps / 🍎 Build iOS IPA (Phone - Unsigned) (push) Waiting to run
🏗️ Build Apps / 🍎 Build tvOS IPA (push) Waiting to run
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Waiting to run
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Waiting to run
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Waiting to run
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Waiting to run
🌐 Translation Sync / sync-translations (push) Waiting to run
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (i18n:check) (push) Waiting to run
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Waiting to run
🛡️ Trivy Security Scan / 🔎 Filesystem scan (push) Waiting to run
2026-06-09 23:25:26 +02:00
renovate[bot]
6f0230c2ca chore(deps): Update expo/expo-github-action digest to eab7a23 (#1685) 2026-06-09 22:37:14 +02:00
renovate[bot]
d12beee529 chore(deps): Update actions/checkout action to v6.0.3 (#1686) 2026-06-09 21:43:22 +02:00
renovate[bot]
02ffac167b chore(deps): Update dependency xcode to v26.5 (#1609)
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone - Unsigned) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build tvOS IPA (push) Has been cancelled
🏗️ Build Apps / 🍎 Build tvOS IPA (Unsigned) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🌐 Translation Sync / sync-translations (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (i18n:check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🛡️ Trivy Security Scan / 🔎 Filesystem scan (push) Has been cancelled
2026-06-09 15:58:35 +02:00
renovate[bot]
4eb734c99f chore(deps): Update expo monorepo to v1.19.9 (#1606) 2026-06-09 15:05:23 +02:00
Gauvain
b7bae0072f ci(i18n): detect unused & missing translation keys + clean key debt (#1636) 2026-06-09 11:46:42 +02:00
boolemancer
1685571406 fix(downloads): Use mediaSource.Id instead of item.Id in direct download URL (#1666)
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone - Unsigned) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build tvOS IPA (push) Has been cancelled
🏗️ Build Apps / 🍎 Build tvOS IPA (Unsigned) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🌐 Translation Sync / sync-translations (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🛡️ Trivy Security Scan / 🔎 Filesystem scan (push) Has been cancelled
Co-authored-by: lance chant <13349722+lancechant@users.noreply.github.com>
Co-authored-by: Gauvain <contact@uruk.dev>
2026-06-08 14:59:29 +02:00
Gauvain
36ed7539a2 ci(security): add Trivy filesystem scan to code scanning (#1644) 2026-06-08 14:05:23 +02:00
16 changed files with 431 additions and 139 deletions

View File

@@ -33,7 +33,7 @@ jobs:
swap-storage: false swap-storage: false
- name: 📥 Checkout code - name: 📥 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0 fetch-depth: 0
@@ -116,7 +116,7 @@ jobs:
swap-storage: false swap-storage: false
- name: 📥 Checkout code - name: 📥 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0 fetch-depth: 0
@@ -187,7 +187,7 @@ jobs:
steps: steps:
- name: 📥 Checkout code - name: 📥 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0 fetch-depth: 0
@@ -219,10 +219,10 @@ jobs:
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with: with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose # renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.4" xcode-version: "26.5"
- name: 🏗️ Setup EAS - name: 🏗️ Setup EAS
uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main
with: with:
eas-version: latest eas-version: latest
token: ${{ secrets.EXPO_TOKEN }} token: ${{ secrets.EXPO_TOKEN }}
@@ -252,7 +252,7 @@ jobs:
steps: steps:
- name: 📥 Checkout code - name: 📥 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0 fetch-depth: 0
@@ -284,7 +284,7 @@ jobs:
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with: with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose # renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.4" xcode-version: "26.5"
- name: 🚀 Build iOS app - name: 🚀 Build iOS app
env: env:
@@ -312,7 +312,7 @@ jobs:
steps: steps:
- name: 📥 Checkout code - name: 📥 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0 fetch-depth: 0
@@ -344,10 +344,10 @@ jobs:
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with: with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose # renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.4" xcode-version: "26.5"
- name: 🏗️ Setup EAS - name: 🏗️ Setup EAS
uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main
with: with:
eas-version: latest eas-version: latest
token: ${{ secrets.EXPO_TOKEN }} token: ${{ secrets.EXPO_TOKEN }}
@@ -380,7 +380,7 @@ jobs:
steps: steps:
- name: 📥 Checkout code - name: 📥 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0 fetch-depth: 0
@@ -412,7 +412,7 @@ jobs:
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with: with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose # renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.4" xcode-version: "26.5"
- name: 🚀 Build iOS app - name: 🚀 Build iOS app
env: env:

View File

@@ -19,7 +19,7 @@ jobs:
steps: steps:
- name: 📥 Checkout repository - name: 📥 Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
show-progress: false show-progress: false

View File

@@ -24,16 +24,16 @@ jobs:
steps: steps:
- name: 📥 Checkout repository - name: 📥 Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: 🏁 Initialize CodeQL - name: 🏁 Initialize CodeQL
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended,security-and-quality queries: +security-extended,security-and-quality
- name: 🛠️ Autobuild - name: 🛠️ Autobuild
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
- name: 🧪 Perform CodeQL Analysis - name: 🧪 Perform CodeQL Analysis
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2

View File

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: 📥 Checkout Repository - name: 📥 Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@@ -21,7 +21,7 @@ jobs:
contents: read contents: read
steps: steps:
- name: 📥 Checkout repository - name: 📥 Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: 🍞 Setup Bun - name: 🍞 Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0

View File

@@ -51,7 +51,7 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0 fetch-depth: 0
@@ -69,7 +69,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: 🛒 Checkout repository - name: 🛒 Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
submodules: recursive submodules: recursive
@@ -97,10 +97,11 @@ jobs:
- "check" - "check"
- "format" - "format"
- "typecheck" - "typecheck"
- "i18n:check"
steps: steps:
- name: "📥 Checkout PR code" - name: "📥 Checkout PR code"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
submodules: recursive submodules: recursive

View File

@@ -63,7 +63,7 @@ jobs:
steps: steps:
- name: 📥 Checkout code - name: 📥 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -88,7 +88,7 @@ jobs:
bun run submodule-reload bun run submodule-reload
- name: 🏗️ Setup EAS - name: 🏗️ Setup EAS
uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main
with: with:
eas-version: latest eas-version: latest
token: ${{ secrets.EXPO_TOKEN }} token: ${{ secrets.EXPO_TOKEN }}
@@ -182,7 +182,7 @@ jobs:
actions: read # required for `gh run download` to list/fetch this run's artifacts actions: read # required for `gh run download` to list/fetch this run's artifacts
steps: steps:
- name: 📥 Checkout code - name: 📥 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
show-progress: false show-progress: false

60
.github/workflows/trivy-scan.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: 🛡️ Trivy Security Scan
# Filesystem scan (Streamyfin ships no container image): finds vulnerable dependencies,
# leaked secrets and misconfigurations, and reports them to GitHub code scanning.
# Runs post-merge + weekly (not on PRs — dependency-review already gates PRs, and SARIF
# upload needs a write token that fork PRs don't get).
on:
push:
branches: [develop, master]
schedule:
- cron: "50 7 * * 5" # Weekly, Friday 07:50 UTC
workflow_dispatch:
permissions:
contents: read
concurrency:
group: trivy-${{ github.ref }}
cancel-in-progress: true
jobs:
trivy:
name: 🔎 Filesystem scan
runs-on: ubuntu-24.04
permissions:
contents: read
security-events: write # upload SARIF to code scanning
steps:
- name: 📥 Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# Rotate the DB cache weekly (matches the scheduled scan): cache hits within the week
# instead of a fresh immutable entry per run, still refreshing the DB every week.
- name: 🗓️ Compute weekly Trivy cache key
id: trivy-cache-key
run: echo "value=trivy-db-${{ runner.os }}-$(date -u +%G-%V)" >> "$GITHUB_OUTPUT"
- name: 💾 Cache Trivy vulnerability DB
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/trivy
key: ${{ steps.trivy-cache-key.outputs.value }}
restore-keys: trivy-db-${{ runner.os }}-
- name: 🔎 Run Trivy filesystem scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: fs
scan-ref: .
scanners: vuln,secret,misconfig
ignore-unfixed: true
severity: CRITICAL,HIGH
format: sarif
output: trivy-results.sarif
- name: 📤 Upload results to code scanning
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: trivy-results.sarif
category: trivy-fs

View File

@@ -18,7 +18,7 @@ jobs:
steps: steps:
- name: 📥 Checkout repository - name: 📥 Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: "🟢 Setup Node.js" - name: "🟢 Setup Node.js"
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0

View File

@@ -108,7 +108,7 @@
"@types/react": "~19.2.10", "@types/react": "~19.2.10",
"@types/react-test-renderer": "19.1.0", "@types/react-test-renderer": "19.1.0",
"cross-env": "10.1.0", "cross-env": "10.1.0",
"expo-doctor": "1.19.7", "expo-doctor": "1.19.9",
"husky": "9.1.7", "husky": "9.1.7",
"lint-staged": "17.0.5", "lint-staged": "17.0.5",
"react-test-renderer": "19.2.3", "react-test-renderer": "19.2.3",
@@ -969,7 +969,7 @@
"expo-device": ["expo-device@56.0.4", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-ucVcGPkvBrl2QHuy7XcYex2Y6BETvJ6TREutZrwLGUDnlvbpKS8KfQoNZOpvkyo5Nmm9RrasYQ0CrXmBHho2mg=="], "expo-device": ["expo-device@56.0.4", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-ucVcGPkvBrl2QHuy7XcYex2Y6BETvJ6TREutZrwLGUDnlvbpKS8KfQoNZOpvkyo5Nmm9RrasYQ0CrXmBHho2mg=="],
"expo-doctor": ["expo-doctor@1.19.7", "", { "bin": { "expo-doctor": "bin/expo-doctor.js" } }, "sha512-pzn7QtCifRlvGIQz8k7kszeYFaI5Yn81WTHlk/20tmd3jwnXxPjlcdyhFSkuRtO2v4a9gA/6aUWVBOosfffj9w=="], "expo-doctor": ["expo-doctor@1.19.9", "", { "bin": { "expo-doctor": "bin/expo-doctor.js" } }, "sha512-SJW5HxEDQ9f5QdFvrUwfbdJZ4HI0EAAxsrJqrHBFjKBum+uSOcEIZPLRibwNQLTHOwTO1TWNLiMlF9sDUBWeYw=="],
"expo-file-system": ["expo-file-system@56.0.7", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-dcKzo8ShPloM7jgfnMcJStgQebhP8owVjCkNI/aX6NMFV1CYB8bxKGMdnzJ3mXk5nfaiW+F/lSKr2UIJ02WAUA=="], "expo-file-system": ["expo-file-system@56.0.7", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-dcKzo8ShPloM7jgfnMcJStgQebhP8owVjCkNI/aX6NMFV1CYB8bxKGMdnzJ3mXk5nfaiW+F/lSKr2UIJ02WAUA=="],
@@ -1599,7 +1599,7 @@
"react-native-text-ticker": ["react-native-text-ticker@1.15.0", "", {}, "sha512-d/uK+PIOhsYMy1r8h825iq/nADiHsabz3WMbRJSnkpQYn+K9aykUAXRRhu8ZbTAzk4CgnUWajJEFxS5ZDygsdg=="], "react-native-text-ticker": ["react-native-text-ticker@1.15.0", "", {}, "sha512-d/uK+PIOhsYMy1r8h825iq/nADiHsabz3WMbRJSnkpQYn+K9aykUAXRRhu8ZbTAzk4CgnUWajJEFxS5ZDygsdg=="],
"react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#33a3ecd", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-33a3ecd"], "react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#33a3ecd", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-33a3ecd", "sha512-vfkld2jUj7EPkAjIc/Vbx4Q4MtOOLmYtCYCE2dWJsyLnPqgj1f0xVzBxbeVP7dfT+eSh4KIXfdxESXaHgrXIlw=="],
"react-native-udp": ["react-native-udp@4.1.7", "", { "dependencies": { "buffer": "^5.6.0", "events": "^3.1.0" } }, "sha512-NUE3zewu61NCdSsLlj+l0ad6qojcVEZPT4hVG/x6DU9U4iCzwtfZSASh9vm7teAcVzLkdD+cO3411LHshAi/wA=="], "react-native-udp": ["react-native-udp@4.1.7", "", { "dependencies": { "buffer": "^5.6.0", "events": "^3.1.0" } }, "sha512-NUE3zewu61NCdSsLlj+l0ad6qojcVEZPT4hVG/x6DU9U4iCzwtfZSASh9vm7teAcVzLkdD+cO3411LHshAi/wA=="],

View File

@@ -280,7 +280,7 @@ export const TVPasswordEntryModal: React.FC<TVPasswordEntryModalProps> = ({
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<TVSubmitButton <TVSubmitButton
onPress={handleSubmit} onPress={handleSubmit}
label={t("login.login")} label={t("login.login_button")}
loading={isLoading} loading={isLoading}
disabled={!password} disabled={!password}
/> />

View File

@@ -22,7 +22,9 @@
"lint": "biome check --write --unsafe --max-diagnostics 1000", "lint": "biome check --write --unsafe --max-diagnostics 1000",
"format": "biome format --write .", "format": "biome format --write .",
"doctor": "expo-doctor", "doctor": "expo-doctor",
"test": "bun run typecheck && bun run lint && bun run format && bun run doctor", "i18n:check": "bun scripts/check-i18n-keys.mjs",
"i18n:fix-unused": "bun scripts/check-i18n-keys.mjs --fix-unused",
"test": "bun run typecheck && bun run lint && bun run format && bun run i18n:check && bun run doctor",
"postinstall": "patch-package" "postinstall": "patch-package"
}, },
"dependencies": { "dependencies": {
@@ -129,7 +131,7 @@
"@types/react": "~19.2.10", "@types/react": "~19.2.10",
"@types/react-test-renderer": "19.1.0", "@types/react-test-renderer": "19.1.0",
"cross-env": "10.1.0", "cross-env": "10.1.0",
"expo-doctor": "1.19.7", "expo-doctor": "1.19.9",
"husky": "9.1.7", "husky": "9.1.7",
"lint-staged": "17.0.5", "lint-staged": "17.0.5",
"react-test-renderer": "19.2.3", "react-test-renderer": "19.2.3",

273
scripts/check-i18n-keys.mjs Normal file
View File

@@ -0,0 +1,273 @@
#!/usr/bin/env bun
/**
* i18n key checker for Streamyfin.
*
* Detects:
* - MISSING keys: a static `t("a.b.c")` / `i18nKey="a.b.c"` referenced in the code
* that does not exist in the source locale (translations/en.json). These are bugs —
* the app renders the raw key. Always fails CI.
* - UNUSED (dead) keys: a key in the source locale that is referenced nowhere in the
* code, neither statically nor via a detected dynamic prefix (`t(`a.b.${x}`)`).
* These are dead weight that also clutter every locale on Crowdin.
*
* Dynamic usage is handled conservatively:
* - `t(`prefix.${x}`)` -> every key starting with `prefix.` is considered used.
* - `t(`${x}`)` -> fully dynamic, reported for manual review, never used to
* whitelist keys (in Streamyfin these are user-defined section
* titles, not translation keys).
* - Edge cases the static scan cannot see can be allow-listed in the config file.
*
* Usage:
* bun scripts/check-i18n-keys.mjs # report + exit 1 on missing OR unused
* bun scripts/check-i18n-keys.mjs --unused=warn # exit 1 only on missing; unused = warning
* bun scripts/check-i18n-keys.mjs --unused=off # ignore unused entirely
* bun scripts/check-i18n-keys.mjs --json # machine-readable output
* bun scripts/check-i18n-keys.mjs --fix-unused # remove dead keys from en.json (Crowdin syncs the rest)
*/
import {
existsSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from "node:fs";
import { extname, join, relative } from "node:path";
const ROOT = process.cwd();
const args = process.argv.slice(2);
const flag = (name, def) => {
const a = args.find((x) => x === `--${name}` || x.startsWith(`--${name}=`));
if (!a) return def;
const [, v] = a.split("=");
return v === undefined ? true : v;
};
const UNUSED_MODE = String(flag("unused", "error")); // error | warn | off
const JSON_OUT = !!flag("json", false);
const FIX_UNUSED = !!flag("fix-unused", false);
// ---- config ----
const CONFIG_PATH = join(ROOT, "scripts", "i18n-keys.config.json");
const DEFAULT_CONFIG = {
localesDir: "translations",
sourceLocale: "en",
// Scan the whole repo by default so keys referenced outside the obvious dirs
// (e.g. packages/, key constants in utils/atoms) are not wrongly flagged as dead.
srcDirs: ["."],
srcExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
excludeDirs: [
"node_modules",
"ios",
"android",
".expo",
".git",
"dist",
"build",
"translations",
"scripts",
],
// Keys (or glob-ish prefixes ending with .* or *) known to be used dynamically / externally.
ignoreUnused: [],
};
const config = existsSync(CONFIG_PATH)
? { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(CONFIG_PATH, "utf8")) }
: DEFAULT_CONFIG;
// ---- helpers ----
const flatten = (obj, prefix = "", out = {}) => {
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, out);
else out[key] = v;
}
return out;
};
const globMatch = (key, pattern) => {
if (pattern.endsWith(".*"))
return key === pattern.slice(0, -2) || key.startsWith(pattern.slice(0, -1));
if (pattern.endsWith("*")) return key.startsWith(pattern.slice(0, -1));
return key === pattern;
};
const walk = (dir, files = []) => {
let entries;
try {
entries = readdirSync(dir);
} catch {
return files;
}
for (const name of entries) {
const full = join(dir, name);
let st;
try {
st = statSync(full);
} catch {
continue;
}
if (st.isDirectory()) {
if (config.excludeDirs.includes(name)) continue;
walk(full, files);
} else if (config.srcExtensions.includes(extname(name))) {
files.push(full);
}
}
return files;
};
// ---- load source keys ----
const sourcePath = join(ROOT, config.localesDir, `${config.sourceLocale}.json`);
const sourceKeys = Object.keys(
flatten(JSON.parse(readFileSync(sourcePath, "utf8"))),
);
const sourceKeySet = new Set(sourceKeys);
// ---- scan code ----
const STATIC_RE = /\bt\(\s*(['"])((?:\\.|(?!\1).)+?)\1/g; // t("a.b") / t('a.b')
const TPL_STATIC_RE = /\bt\(\s*`([^`$]+)`/g; // t(`a.b`) no interpolation
const TPL_DYN_RE = /\bt\(\s*`([^`$]*)\$\{/g; // t(`a.b.${x}`) -> prefix "a.b."
const I18NKEY_RE = /\bi18nKey\s*=\s*(?:\{\s*)?(['"])((?:\\.|(?!\1).)+?)\1/g; // <Trans i18nKey="a.b">
const KEY_SHAPE = /^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)+$/; // dotted key, e.g. home.x.y
const usedStatic = new Set(); // keys passed to t(...) / i18nKey — used for MISSING detection
const dynamicPrefixes = new Set();
const fullyDynamic = []; // { file, line }
let codeBlob = ""; // all (comment-stripped) source text — searched for delimited key literals
// Strip comments so keys mentioned in comments (e.g. `// t("old.key")`) are not counted as
// usage. Block comments and JSX {/* */} are blanked (preserving newlines for line numbers);
// line comments are only stripped when `//` follows start/whitespace/punctuation, which keeps
// `://` inside string URLs intact.
const stripComments = (src) =>
src
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
.replace(/(^|[\s;{}()[\],=>])\/\/[^\n]*/g, (_m, p) => p);
const files = config.srcDirs.flatMap((d) =>
walk(join(ROOT, d === "." ? "" : d) || ROOT),
);
for (const file of files) {
const text = readFileSync(file, "utf8");
const clean = stripComments(text);
codeBlob += `\n${clean}`;
for (const m of clean.matchAll(STATIC_RE)) usedStatic.add(m[2]);
for (const m of clean.matchAll(TPL_STATIC_RE)) usedStatic.add(m[1]);
for (const m of clean.matchAll(I18NKEY_RE)) usedStatic.add(m[2]);
for (const m of clean.matchAll(TPL_DYN_RE)) {
const prefix = m[1];
if (prefix?.includes(".")) dynamicPrefixes.add(prefix);
else {
const idx = clean.slice(0, m.index).split("\n").length;
fullyDynamic.push({ file: relative(ROOT, file), line: idx });
}
}
}
const prefixList = [...dynamicPrefixes];
// A key counts as used if its EXACT delimited literal ("k" / 'k' / `k`) appears anywhere in
// the code (covers t("k"), <Trans i18nKey>, and keys stored as bare string constants in
// arrays/config then resolved via t(variable)), or it is reached via a dynamic prefix, or
// explicitly allow-listed. Delimited search avoids substring false-matches (e.g. a.b vs a.b_c).
const literalUsed = (key) =>
codeBlob.includes(`"${key}"`) ||
codeBlob.includes(`'${key}'`) ||
codeBlob.includes(`\`${key}\``);
const isUsed = (key) =>
literalUsed(key) ||
prefixList.some((p) => key.startsWith(p)) ||
config.ignoreUnused.some((g) => globMatch(key, g));
// ---- compute ----
const unused = sourceKeys.filter((k) => !isUsed(k)).sort();
// Static references are always validated, even under a dynamic prefix: a dynamic prefix only
// affects the UNUSED calculation, never MISSING.
const missing = [...usedStatic]
.filter((k) => KEY_SHAPE.test(k) && !sourceKeySet.has(k))
.sort();
// Known limitation: only keys seen in a static t("…") / i18nKey="…" / t(`…`) call are
// validated for MISSING. A key stored as a bare string constant and resolved via t(variable)
// counts as USED (via literalUsed → not flagged unused) but its existence in en.json is not
// checked here — static analysis can't resolve which key a runtime variable holds. Streamyfin
// keys are static literals in practice; revisit if dynamic key constants become common.
// ---- optional fix: strip dead keys from the source locale (en.json) ----
const removeKey = (obj, parts) => {
const [head, ...rest] = parts;
if (!(head in obj)) return;
if (rest.length === 0) {
delete obj[head];
return;
}
removeKey(obj[head], rest);
if (
obj[head] &&
typeof obj[head] === "object" &&
Object.keys(obj[head]).length === 0
)
delete obj[head];
};
if (FIX_UNUSED && unused.length) {
// Only edit the SOURCE locale (en.json). Crowdin owns the target locales and removes
// the keys from them automatically on the next sync once they disappear from the source.
const data = JSON.parse(readFileSync(sourcePath, "utf8"));
for (const key of unused) removeKey(data, key.split("."));
writeFileSync(sourcePath, `${JSON.stringify(data, null, 2)}\n`);
console.log(
`🧹 Removed ${unused.length} dead key(s) from ${config.sourceLocale}.json (Crowdin will sync the other locales).`,
);
}
// ---- report ----
if (JSON_OUT) {
console.log(
JSON.stringify(
{
sourceKeys: sourceKeys.length,
missing,
unused,
dynamicPrefixes: prefixList,
fullyDynamic,
},
null,
2,
),
);
} else {
console.log(
`🔑 i18n key check — source: ${relative(ROOT, sourcePath)} (${sourceKeys.length} keys), scanned ${files.length} files`,
);
if (prefixList.length)
console.log(
` dynamic prefixes treated as used: ${prefixList.map((p) => `${p}*`).join(", ")}`,
);
if (fullyDynamic.length)
console.log(
` ⚠️ ${fullyDynamic.length} fully-dynamic t(\`\${…}\`) call(s) (not key-based, manual review): ${fullyDynamic.map((d) => `${d.file}:${d.line}`).join(", ")}`,
);
if (missing.length) {
console.log(
`\n❌ MISSING keys (used in code, absent from ${config.sourceLocale}.json) — ${missing.length}:`,
);
for (const k of missing) console.log(` - ${k}`);
} else console.log("\n✅ No missing keys.");
if (UNUSED_MODE !== "off") {
if (unused.length) {
console.log(
`\n${UNUSED_MODE === "error" ? "❌" : "⚠️ "} UNUSED keys (in ${config.sourceLocale}.json, referenced nowhere) — ${unused.length}:`,
);
for (const k of unused) console.log(` - ${k}`);
console.log(
`\n → remove with: bun scripts/check-i18n-keys.mjs --fix-unused`,
);
console.log(
` → or allow-list a dynamic key in scripts/i18n-keys.config.json ("ignoreUnused").`,
);
} else console.log("\n✅ No unused keys.");
}
}
const fail =
missing.length > 0 || (UNUSED_MODE === "error" && unused.length > 0);
process.exit(fail ? 1 : 0);

View File

@@ -0,0 +1,46 @@
{
"localesDir": "translations",
"sourceLocale": "en",
"srcDirs": [
"app",
"components",
"hooks",
"providers",
"utils",
"modules",
"packages",
"constants"
],
"srcExtensions": [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
"excludeDirs": [
"node_modules",
"ios",
"android",
".expo",
".git",
"dist",
"build",
"translations"
],
"_ignoreUnusedNote": "Keys present in en.json but referenced by no t() call — allow-listed so the unused-key cleanup keeps them. Two kinds: (1) ORPHANS of features that already exist but render hardcoded strings or use other keys — watchlists.* (add/remove, handled by WatchlistSheet via hooks), pin.* (confirm step of the existing PIN setup flow), player.* (in-player subtitle search, swipe-down hint, stop-playback confirm); these should be wired to the live UI or removed in a follow-up i18n pass, not assumed to be future work. (2) GENUINELY PLANNED, tracked by issues: home.settings.other.show_large_home_carousel (#1702 media-bar), home.settings.logs.delete_all_logs (#1703 iOS logs fix), home.suggested_episodes (#1704).",
"ignoreUnused": [
"watchlists.add_to_watchlist",
"watchlists.remove_from_watchlist",
"watchlists.create_one_first",
"watchlists.no_compatible_watchlists",
"pin.confirm_pin",
"pin.pins_dont_match",
"player.search_subtitles",
"player.subtitle_search",
"player.subtitle_download_hint",
"player.subtitle_tracks",
"player.using_jellyfin_server",
"player.swipe_down_settings",
"player.stopPlayback",
"player.stopPlayingTitle",
"player.stopPlayingConfirm",
"home.settings.other.show_large_home_carousel",
"home.settings.logs.delete_all_logs",
"home.suggested_episodes"
]
}

View File

@@ -261,43 +261,6 @@
"None": "None", "None": "None",
"OnlyForced": "Only forced" "OnlyForced": "Only forced"
}, },
"text_color": "Text color",
"background_color": "Background color",
"outline_color": "Outline color",
"outline_thickness": "Outline thickness",
"background_opacity": "Background opacity",
"outline_opacity": "Outline opacity",
"bold_text": "Bold text",
"colors": {
"Black": "Black",
"Gray": "Gray",
"Silver": "Silver",
"White": "White",
"Maroon": "Maroon",
"Red": "Red",
"Fuchsia": "Fuchsia",
"Yellow": "Yellow",
"Olive": "Olive",
"Green": "Green",
"Teal": "Teal",
"Lime": "Lime",
"Purple": "Purple",
"Navy": "Navy",
"Blue": "Blue",
"Aqua": "Aqua"
},
"thickness": {
"None": "None",
"Thin": "Thin",
"Normal": "Normal",
"Thick": "Thick"
},
"subtitle_color": "Subtitle color",
"subtitle_background_color": "Background color",
"subtitle_font": "Subtitle font",
"ksplayer_title": "KSPlayer settings",
"hardware_decode": "Hardware decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.", "opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API key", "opensubtitles_api_key": "API key",
@@ -315,25 +278,6 @@
"bottom": "Bottom" "bottom": "Bottom"
} }
}, },
"vlc_subtitles": {
"title": "VLC subtitle settings",
"hint": "Customize subtitle appearance for the VLC player. Changes take effect on next playback.",
"text_color": "Text color",
"background_color": "Background color",
"background_opacity": "Background opacity",
"outline_color": "Outline color",
"outline_opacity": "Outline opacity",
"outline_thickness": "Outline thickness",
"bold": "Bold text",
"margin": "Bottom margin"
},
"video_player": {
"title": "Video player",
"video_player": "Video player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
},
"other": { "other": {
"other_title": "Other", "other_title": "Other",
"video_orientation": "Video orientation", "video_orientation": "Video orientation",
@@ -351,11 +295,6 @@
"UNKNOWN": "Unknown" "UNKNOWN": "Unknown"
}, },
"safe_area_in_controls": "Safe area in controls", "safe_area_in_controls": "Safe area in controls",
"video_player": "Video player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Show custom menu links", "show_custom_menu_links": "Show custom menu links",
"show_large_home_carousel": "Show large home carousel (beta)", "show_large_home_carousel": "Show large home carousel (beta)",
"hide_libraries": "Hide libraries", "hide_libraries": "Hide libraries",
@@ -367,9 +306,6 @@
"max_auto_play_episode_count": "Max auto-play episode count", "max_auto_play_episode_count": "Max auto-play episode count",
"disabled": "Disabled" "disabled": "Disabled"
}, },
"downloads": {
"downloads_title": "Downloads"
},
"music": { "music": {
"title": "Music", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
@@ -413,23 +349,18 @@
"read_more_about_marlin": "Read more about Marlin.", "read_more_about_marlin": "Read more about Marlin.",
"save_button": "Save", "save_button": "Save",
"toasts": { "toasts": {
"saved": "Saved", "saved": "Saved"
"refreshed": "Settings refreshed from server" }
},
"refresh_from_server": "Refresh settings from server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Disable Streamystats", "disable_streamystats": "Disable Streamystats",
"enable_search": "Use for search", "enable_search": "Use for search",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.", "streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
"read_more_about_streamystats": "Read more about Streamystats.", "read_more_about_streamystats": "Read more about Streamystats.",
"save_button": "Save",
"save": "Save", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home sections",
"enable_movie_recommendations": "Movie recommendations", "enable_movie_recommendations": "Movie recommendations",
"enable_series_recommendations": "Series recommendations", "enable_series_recommendations": "Series recommendations",
"enable_promoted_watchlists": "Promoted watchlists", "enable_promoted_watchlists": "Promoted watchlists",
@@ -445,8 +376,7 @@
"refresh_from_server": "Refresh settings from server" "refresh_from_server": "Refresh settings from server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable watchlist integration", "watchlist_enabler": "Enable watchlist integration"
"watchlist_button": "Toggle watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -457,7 +387,6 @@
"delete_all_downloaded_files": "Delete all downloaded files", "delete_all_downloaded_files": "Delete all downloaded files",
"music_cache_title": "Music cache", "music_cache_title": "Music cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable music cache",
"clear_music_cache": "Clear music cache", "clear_music_cache": "Clear music cache",
"music_cache_size": "{{size}} cached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
@@ -467,8 +396,6 @@
"clear_all_cache": "Clear all cache", "clear_all_cache": "Clear all cache",
"clear_all_cache_confirm": "Clear all cache?", "clear_all_cache_confirm": "Clear all cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.", "clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
@@ -490,15 +417,12 @@
"system": "System" "system": "System"
}, },
"toasts": { "toasts": {
"error_deleting_files": "Error deleting files", "error_deleting_files": "Error deleting files"
"background_downloads_enabled": "Background downloads enabled",
"background_downloads_disabled": "Background downloads disabled"
}, },
"security": { "security": {
"title": "Security", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity timeout", "title": "Inactivity timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
@@ -508,6 +432,10 @@
"4_hours": "4 hours", "4_hours": "4 hours",
"24_hours": "24 hours" "24_hours": "24 hours"
} }
},
"dashboard": {
"title": "Dashboard",
"sessions_title": "Sessions"
} }
}, },
"sessions": { "sessions": {
@@ -518,10 +446,7 @@
"downloads_title": "Downloads", "downloads_title": "Downloads",
"series": "Series", "series": "Series",
"movies": "Movies", "movies": "Movies",
"queue": "Queue",
"other_media": "Other media", "other_media": "Other media",
"queue_hint": "Queue and downloads will be lost on app restart",
"no_items_in_queue": "No items in queue",
"no_downloaded_items": "No downloaded items", "no_downloaded_items": "No downloaded items",
"delete_all_movies_button": "Delete all movies", "delete_all_movies_button": "Delete all movies",
"delete_all_series_button": "Delete all series", "delete_all_series_button": "Delete all series",
@@ -546,13 +471,8 @@
"failed_to_delete_all_series": "Failed to delete all series", "failed_to_delete_all_series": "Failed to delete all series",
"deleted_media_successfully": "Deleted other media successfully!", "deleted_media_successfully": "Deleted other media successfully!",
"failed_to_delete_media": "Failed to delete other media", "failed_to_delete_media": "Failed to delete other media",
"download_deleted": "Download deleted",
"download_cancelled": "Download cancelled", "download_cancelled": "Download cancelled",
"could_not_delete_download": "Could not delete download", "could_not_delete_download": "Could not delete download",
"download_paused": "Download paused",
"could_not_pause_download": "Could not pause download",
"download_resumed": "Download resumed",
"could_not_resume_download": "Could not resume download",
"download_completed": "Download completed", "download_completed": "Download completed",
"download_failed": "Download failed", "download_failed": "Download failed",
"download_failed_for_item": "Download failed for {{item}} - {{error}}", "download_failed_for_item": "Download failed for {{item}} - {{error}}",
@@ -562,10 +482,7 @@
"item_already_downloading": "{{item}} is already downloading", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "All downloads deleted successfully", "all_files_deleted": "All downloads deleted successfully",
"files_deleted_by_type": "{{count}} {{type}} deleted", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully",
"failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Go to Downloads",
"file_deleted": "{{item}} deleted" "file_deleted": "{{item}} deleted"
} }
} }
@@ -583,16 +500,17 @@
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying...",
"login": "Login", "login": "Login",
"refresh": "Refresh" "episodes": "Episodes",
"movies": "Movies",
"loading": "Loading…",
"seeAll": "See all"
}, },
"search": { "search": {
"search": "Search...", "search": "Search...",
@@ -691,10 +609,6 @@
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from server: {{message}}", "message_from_server": "Message from server: {{message}}",
"next_episode": "Next episode", "next_episode": "Next episode",
"refresh_tracks": "Refresh tracks",
"audio_tracks": "Audio tracks:",
"playback_state": "Playback state:",
"index": "Index:",
"continue_watching": "Continue watching", "continue_watching": "Continue watching",
"go_back": "Go back", "go_back": "Go back",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
@@ -723,7 +637,8 @@
"stopPlayback": "Stop playback", "stopPlayback": "Stop playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?", "stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?", "stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded" "downloaded": "Downloaded",
"missing_parameters": "Missing playback parameters"
}, },
"chapters": { "chapters": {
"title": "Chapters", "title": "Chapters",
@@ -761,7 +676,6 @@
"show_more": "Show more", "show_more": "Show more",
"show_less": "Show less", "show_less": "Show less",
"left": "left", "left": "left",
"more_info": "More info",
"director": "Director", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical details", "technical_details": "Technical details",
@@ -784,7 +698,8 @@
"resume_playback": "Resume playback", "resume_playback": "Resume playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?", "resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from start", "play_from_start": "Play from start",
"continue_from": "Continue from {{time}}" "continue_from": "Continue from {{time}}",
"no_data_available": "No data available"
}, },
"live_tv": { "live_tv": {
"next": "Next", "next": "Next",
@@ -888,13 +803,9 @@
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "Songs" "tracks": "Songs"
}, },
"filters": {
"all": "All"
},
"recently_added": "Recently added", "recently_added": "Recently added",
"recently_played": "Recently played", "recently_played": "Recently played",
"frequently_played": "Frequently played", "frequently_played": "Frequently played",
"explore": "Explore",
"top_tracks": "Top songs", "top_tracks": "Top songs",
"play": "Play", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -1028,7 +939,6 @@
"pairing": { "pairing": {
"pair_with_phone": "Pair with phone", "pair_with_phone": "Pair with phone",
"pair_with_phone_title": "Log in on TV", "pair_with_phone_title": "Log in on TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...", "logging_in": "Logging in...",

View File

@@ -50,7 +50,7 @@ export const getDownloadUrl = async ({
if (maxBitrate.key === "Max" && !streamDetails?.mediaSource?.TranscodingUrl) { if (maxBitrate.key === "Max" && !streamDetails?.mediaSource?.TranscodingUrl) {
console.log("Downloading item directly"); console.log("Downloading item directly");
return { return {
url: `${api.basePath}/Items/${item.Id}/Download?api_key=${api.accessToken}`, url: `${api.basePath}/Items/${mediaSource.Id}/Download?api_key=${api.accessToken}`,
mediaSource: streamDetails?.mediaSource ?? null, mediaSource: streamDetails?.mediaSource ?? null,
}; };
} }