mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-12 08:50:25 +01:00
Compare commits
18 Commits
renovate/r
...
refactor/j
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf2bab57bb | ||
|
|
f97852ae98 | ||
|
|
96116e0451 | ||
|
|
938918fa06 | ||
|
|
a4b6f456f2 | ||
|
|
0a2dadffd2 | ||
|
|
6818ea380f | ||
|
|
7cf0a13317 | ||
|
|
168bf2e54e | ||
|
|
6f0230c2ca | ||
|
|
d12beee529 | ||
|
|
02ffac167b | ||
|
|
4eb734c99f | ||
|
|
b7bae0072f | ||
|
|
1685571406 | ||
|
|
36ed7539a2 | ||
|
|
8f82ac481a | ||
|
|
a242ff69fd |
29
.gitattributes
vendored
29
.gitattributes
vendored
@@ -1 +1,28 @@
|
|||||||
.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text
|
# Normalise line endings to LF for everyone. Files are stored as LF in git and
|
||||||
|
# checked out as LF on every OS, so Windows clones stop producing CRLF churn
|
||||||
|
# (no more "LF will be replaced by CRLF" warnings) regardless of core.autocrlf.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Windows-only scripts must stay CRLF
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# Binary assets — never touched / never normalised
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.webp binary
|
||||||
|
*.ico binary
|
||||||
|
*.icns binary
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.mov binary
|
||||||
|
*.pdf binary
|
||||||
|
*.keystore binary
|
||||||
|
*.jks binary
|
||||||
|
*.p12 binary
|
||||||
|
|||||||
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -42,7 +42,7 @@ and provides seamless media streaming with offline capabilities and Chromecast s
|
|||||||
|
|
||||||
## Coding Standards
|
## Coding Standards
|
||||||
|
|
||||||
- Use TypeScript for ALL files (no .js files)
|
- Use TypeScript for ALL files (no .js files). Tooling-required exceptions: `babel.config.js`, `metro.config.js`, `react-native.config.js`, `tailwind.config.js` (their loaders cannot parse TypeScript)
|
||||||
- Use descriptive English names for variables, functions, and components
|
- Use descriptive English names for variables, functions, and components
|
||||||
- Prefer functional React components with hooks
|
- Prefer functional React components with hooks
|
||||||
- Use Jotai atoms for global state management
|
- Use Jotai atoms for global state management
|
||||||
|
|||||||
54
.github/renovate.json
vendored
54
.github/renovate.json
vendored
@@ -44,22 +44,42 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lockFileMaintenance": {
|
"vulnerabilityAlerts": {
|
||||||
"vulnerabilityAlerts": {
|
"enabled": true,
|
||||||
"enabled": true,
|
"addLabels": ["security", "vulnerability"],
|
||||||
"addLabels": ["security", "vulnerability"],
|
"assigneesFromCodeOwners": true,
|
||||||
"assigneesFromCodeOwners": true,
|
"commitMessageSuffix": " [SECURITY]"
|
||||||
"commitMessageSuffix": " [SECURITY]"
|
},
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "Expo SDK coherence: expo, react, react-native and Expo-managed modules are pinned by the Expo SDK and must move together (via `expo install --fix`), so do not raise individual update PRs — group them and require manual approval from the Dependency Dashboard",
|
||||||
|
"matchPackageNames": [
|
||||||
|
"expo",
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"react-native",
|
||||||
|
"react-native-web",
|
||||||
|
"expo-*",
|
||||||
|
"@expo/*"
|
||||||
|
],
|
||||||
|
"groupName": "Expo SDK",
|
||||||
|
"dependencyDashboardApproval": true
|
||||||
},
|
},
|
||||||
"packageRules": [
|
{
|
||||||
{
|
"description": "Group minor and patch GitHub Action updates into a single PR",
|
||||||
"description": "Group minor and patch GitHub Action updates into a single PR",
|
"matchManagers": ["github-actions"],
|
||||||
"matchManagers": ["github-actions"],
|
"groupName": "CI dependencies",
|
||||||
"groupName": "CI dependencies",
|
"groupSlug": "ci-deps",
|
||||||
"groupSlug": "ci-deps",
|
"matchUpdateTypes": ["minor", "patch", "digest", "pin"],
|
||||||
"matchUpdateTypes": ["minor", "patch", "digest", "pin"],
|
"automerge": true
|
||||||
"automerge": true
|
},
|
||||||
}
|
{
|
||||||
]
|
"description": "androidx and other Google-hosted Maven packages resolve from Google's Maven repository (not Maven Central)",
|
||||||
}
|
"matchDatasources": ["maven"],
|
||||||
|
"registryUrls": [
|
||||||
|
"https://dl.google.com/dl/android/maven2/",
|
||||||
|
"https://repo.maven.apache.org/maven2/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
39
.github/workflows/build-apps.yml
vendored
39
.github/workflows/build-apps.yml
vendored
@@ -11,6 +11,15 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [develop, master]
|
branches: [develop, master]
|
||||||
|
|
||||||
|
# Exposed to `expo prebuild` (app.config.js → extra.build) so Settings can show the
|
||||||
|
# branch + commit + Actions run a CI build was made from. EAS cloud builds use
|
||||||
|
# EAS_BUILD_* instead. The run number maps a sideloaded build back to its Actions
|
||||||
|
# run (artifacts + logs) without needing Expo access.
|
||||||
|
env:
|
||||||
|
EXPO_PUBLIC_GIT_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
|
EXPO_PUBLIC_GIT_COMMIT: ${{ github.sha }}
|
||||||
|
EXPO_PUBLIC_GIT_RUN_NUMBER: ${{ github.run_number }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-android-phone:
|
build-android-phone:
|
||||||
if: (!contains(github.event.head_commit.message, '[skip ci]'))
|
if: (!contains(github.event.head_commit.message, '[skip ci]'))
|
||||||
@@ -33,7 +42,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 +125,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 +196,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 +228,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 }}
|
||||||
@@ -231,7 +240,9 @@ jobs:
|
|||||||
- name: 🚀 Build iOS app
|
- name: 🚀 Build iOS app
|
||||||
env:
|
env:
|
||||||
EXPO_TV: 0
|
EXPO_TV: 0
|
||||||
run: eas build -p ios --local --non-interactive
|
# `ci` profile (extends production, autoIncrement off): keeps CI builds out of
|
||||||
|
# the production version tier and stops them inflating the store build counter.
|
||||||
|
run: eas build -p ios --local --non-interactive --profile ci
|
||||||
|
|
||||||
- name: 📅 Set date tag
|
- name: 📅 Set date tag
|
||||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||||
@@ -252,7 +263,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 +295,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 +323,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 +355,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 }}
|
||||||
@@ -356,7 +367,7 @@ jobs:
|
|||||||
- name: 🚀 Build iOS app
|
- name: 🚀 Build iOS app
|
||||||
env:
|
env:
|
||||||
EXPO_TV: 1
|
EXPO_TV: 1
|
||||||
run: eas build -p ios --local --non-interactive
|
run: eas build -p ios --local --non-interactive --profile ci_tv
|
||||||
|
|
||||||
- name: 📅 Set date tag
|
- name: 📅 Set date tag
|
||||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||||
@@ -380,7 +391,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 +423,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:
|
||||||
|
|||||||
2
.github/workflows/check-lockfile.yml
vendored
2
.github/workflows/check-lockfile.yml
vendored
@@ -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
|
||||||
|
|||||||
8
.github/workflows/ci-codeql.yml
vendored
8
.github/workflows/ci-codeql.yml
vendored
@@ -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
|
||||||
|
|||||||
2
.github/workflows/conflict.yml
vendored
2
.github/workflows/conflict.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: 🚩 Apply merge conflict label
|
- name: 🚩 Apply merge conflict label
|
||||||
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
|
||||||
with:
|
with:
|
||||||
dirtyLabel: '⚔️ merge-conflict'
|
dirtyLabel: '⚔️ merge-conflict'
|
||||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||||
|
|||||||
102
.github/workflows/crowdin.yml
vendored
102
.github/workflows/crowdin.yml
vendored
@@ -1,51 +1,51 @@
|
|||||||
name: 🌐 Translation Sync
|
name: 🌐 Translation Sync
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [develop]
|
branches: [develop]
|
||||||
paths:
|
paths:
|
||||||
- "translations/**"
|
- "translations/**"
|
||||||
- "crowdin.yml"
|
- "crowdin.yml"
|
||||||
- "i18n.ts"
|
- "i18n.ts"
|
||||||
- ".github/workflows/crowdin.yml"
|
- ".github/workflows/crowdin.yml"
|
||||||
# Run weekly to pull new translations
|
# Run weekly to pull new translations
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 2 * * 1" # Every Monday at 2 AM UTC
|
- cron: "0 2 * * 1" # Every Monday at 2 AM UTC
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync-translations:
|
sync-translations:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
- name: 🌐 Sync Translations with Crowdin
|
- name: 🌐 Sync Translations with Crowdin
|
||||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||||
with:
|
with:
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: true
|
upload_translations: true
|
||||||
download_translations: true
|
download_translations: true
|
||||||
localization_branch_name: I10n_crowdin_translations
|
localization_branch_name: I10n_crowdin_translations
|
||||||
create_pull_request: true
|
create_pull_request: true
|
||||||
pull_request_title: "feat: New Crowdin Translations"
|
pull_request_title: "feat: New Crowdin Translations"
|
||||||
pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)"
|
pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)"
|
||||||
pull_request_base_branch_name: "develop"
|
pull_request_base_branch_name: "develop"
|
||||||
pull_request_labels: "🌐 translation"
|
pull_request_labels: "🌐 translation"
|
||||||
# Quality control options
|
# Quality control options
|
||||||
skip_untranslated_strings: false
|
skip_untranslated_strings: false
|
||||||
skip_untranslated_files: false
|
skip_untranslated_files: false
|
||||||
export_only_approved: false
|
export_only_approved: false
|
||||||
# Commit customization
|
# Commit customization
|
||||||
commit_message: "feat(i18n): update translations from Crowdin"
|
commit_message: "feat(i18n): update translations from Crowdin"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/detect-duplicate.yml
vendored
4
.github/workflows/detect-duplicate.yml
vendored
@@ -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
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
bun-version: latest
|
bun-version: latest
|
||||||
|
|
||||||
- name: 🔍 Detect duplicate issues
|
- name: 🔍 Detect duplicate issues
|
||||||
run: bun scripts/detect-duplicate-issue.mjs
|
run: bun scripts/detect-duplicate-issue.ts
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
|||||||
7
.github/workflows/linting.yml
vendored
7
.github/workflows/linting.yml
vendored
@@ -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
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -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
60
.github/workflows/trivy-scan.yml
vendored
Normal 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
|
||||||
2
.github/workflows/update-issue-form.yml
vendored
2
.github/workflows/update-issue-form.yml
vendored
@@ -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
|
||||||
|
|||||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,6 +1,5 @@
|
|||||||
# Dependencies and Package Managers
|
# Dependencies and Package Managers
|
||||||
node_modules/
|
node_modules/
|
||||||
bun.lock
|
|
||||||
bun.lockb
|
bun.lockb
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
@@ -13,18 +12,12 @@ web-build/
|
|||||||
# Platform-specific Build Directories
|
# Platform-specific Build Directories
|
||||||
/ios
|
/ios
|
||||||
/android
|
/android
|
||||||
/iostv
|
|
||||||
/iosmobile
|
|
||||||
/androidmobile
|
|
||||||
/androidtv
|
|
||||||
|
|
||||||
# Gradle caches (top-level + per-module native projects)
|
# Gradle caches (top-level + per-module native projects)
|
||||||
**/.gradle/
|
**/.gradle/
|
||||||
|
|
||||||
# Module-specific Builds
|
# Native module build outputs (any module)
|
||||||
modules/mpv-player/android/build
|
modules/*/android/build/
|
||||||
modules/player/android
|
|
||||||
modules/hls-downloader/android/build
|
|
||||||
|
|
||||||
# Generated Applications
|
# Generated Applications
|
||||||
Streamyfin.app
|
Streamyfin.app
|
||||||
@@ -69,10 +62,6 @@ certs/
|
|||||||
|
|
||||||
# Version and Backup Files
|
# Version and Backup Files
|
||||||
/version-backup-*
|
/version-backup-*
|
||||||
/modules/sf-player/android/build
|
|
||||||
/modules/music-controls/android/build
|
|
||||||
modules/background-downloader/android/build/*
|
|
||||||
/modules/mpv-player/android/build
|
|
||||||
|
|
||||||
# ios:unsigned-build Artifacts
|
# ios:unsigned-build Artifacts
|
||||||
build/
|
build/
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ import { apiAtom } from "@/providers/JellyfinProvider";
|
|||||||
|
|
||||||
## Coding Standards
|
## Coding Standards
|
||||||
|
|
||||||
- Use TypeScript for all files (no .js)
|
- Use TypeScript for all files (no .js). Tooling-required exceptions: `babel.config.js`, `metro.config.js`, `react-native.config.js`, `tailwind.config.js` (their loaders cannot parse TypeScript)
|
||||||
- Use functional React components with hooks
|
- Use functional React components with hooks
|
||||||
- Use Jotai atoms for global state, React Query for server state
|
- Use Jotai atoms for global state, React Query for server state
|
||||||
- Follow BiomeJS formatting rules (2-space indent, semicolons, LF line endings)
|
- Follow BiomeJS formatting rules (2-space indent, semicolons, LF line endings)
|
||||||
|
|||||||
@@ -1,3 +1,47 @@
|
|||||||
|
const { execFileSync } = require("node:child_process");
|
||||||
|
|
||||||
|
// Build metadata, injected into `extra.build` and read at runtime via
|
||||||
|
// expo-constants (see utils/version.ts). Sources in priority order:
|
||||||
|
// EAS cloud build → GitHub Actions → explicit EXPO_PUBLIC_* → local git → null.
|
||||||
|
const git = (args) => {
|
||||||
|
try {
|
||||||
|
return execFileSync("git", args, { stdio: ["ignore", "pipe", "ignore"] })
|
||||||
|
.toString()
|
||||||
|
.trim();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildMeta = {
|
||||||
|
commit:
|
||||||
|
(
|
||||||
|
process.env.EAS_BUILD_GIT_COMMIT_HASH ||
|
||||||
|
process.env.GITHUB_SHA ||
|
||||||
|
process.env.EXPO_PUBLIC_GIT_COMMIT ||
|
||||||
|
git(["rev-parse", "HEAD"]) ||
|
||||||
|
""
|
||||||
|
).slice(0, 7) || null,
|
||||||
|
branch:
|
||||||
|
process.env.EAS_BUILD_GIT_BRANCH ||
|
||||||
|
process.env.GITHUB_HEAD_REF ||
|
||||||
|
process.env.GITHUB_REF_NAME ||
|
||||||
|
process.env.EXPO_PUBLIC_GIT_BRANCH ||
|
||||||
|
git(["rev-parse", "--abbrev-ref", "HEAD"]) ||
|
||||||
|
null,
|
||||||
|
profile:
|
||||||
|
process.env.EAS_BUILD_PROFILE ||
|
||||||
|
process.env.EXPO_PUBLIC_BUILD_PROFILE ||
|
||||||
|
null,
|
||||||
|
// GitHub Actions run number (#2098) — lets anyone map a sideloaded CI build back
|
||||||
|
// to its Actions run (artifacts + logs) without Expo access. Null outside CI.
|
||||||
|
runNumber:
|
||||||
|
process.env.GITHUB_RUN_NUMBER ||
|
||||||
|
process.env.EXPO_PUBLIC_GIT_RUN_NUMBER ||
|
||||||
|
null,
|
||||||
|
builtAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = ({ config }) => {
|
module.exports = ({ config }) => {
|
||||||
if (process.env.EXPO_TV !== "1") {
|
if (process.env.EXPO_TV !== "1") {
|
||||||
config.plugins.push("expo-background-task");
|
config.plugins.push("expo-background-task");
|
||||||
@@ -22,6 +66,8 @@ module.exports = ({ config }) => {
|
|||||||
androidConfig.googleServicesFile = process.env.GOOGLE_SERVICES_JSON;
|
androidConfig.googleServicesFile = process.env.GOOGLE_SERVICES_JSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.extra = { ...config.extra, build: buildMeta };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(Object.keys(androidConfig).length > 0 && { android: androidConfig }),
|
...(Object.keys(androidConfig).length > 0 && { android: androidConfig }),
|
||||||
...config,
|
...config,
|
||||||
|
|||||||
@@ -161,9 +161,7 @@ export default function FavoritesSeeAllScreen() {
|
|||||||
/>
|
/>
|
||||||
{!itemType ? (
|
{!itemType ? (
|
||||||
<View className='flex-1 items-center justify-center px-6'>
|
<View className='flex-1 items-center justify-center px-6'>
|
||||||
<Text className='text-neutral-500'>
|
<Text className='text-neutral-500'>{t("favorites.noData")}</Text>
|
||||||
{t("favorites.noData", { defaultValue: "No items found." })}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : isLoading ? (
|
) : isLoading ? (
|
||||||
<View className='justify-center items-center h-full'>
|
<View className='justify-center items-center h-full'>
|
||||||
@@ -194,7 +192,7 @@ export default function FavoritesSeeAllScreen() {
|
|||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View className='flex flex-col items-center justify-center h-full py-12'>
|
<View className='flex flex-col items-center justify-center h-full py-12'>
|
||||||
<Text className='font-bold text-xl text-neutral-500'>
|
<Text className='font-bold text-xl text-neutral-500'>
|
||||||
{t("home.no_items", { defaultValue: "No items" })}
|
{t("home.no_items")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,12 +137,12 @@ export default function DownloadsPage() {
|
|||||||
deleteFileByType("Episode")
|
deleteFileByType("Episode")
|
||||||
.then(() =>
|
.then(() =>
|
||||||
toast.success(
|
toast.success(
|
||||||
t("home.downloads.toasts.deleted_all_tvseries_successfully"),
|
t("home.downloads.toasts.deleted_all_series_successfully"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
writeToLog("ERROR", reason);
|
writeToLog("ERROR", reason);
|
||||||
toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries"));
|
toast.error(t("home.downloads.toasts.failed_to_delete_all_series"));
|
||||||
});
|
});
|
||||||
const deleteOtherMedia = () =>
|
const deleteOtherMedia = () =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
@@ -207,7 +207,7 @@ export default function DownloadsPage() {
|
|||||||
<View className='mb-4'>
|
<View className='mb-4'>
|
||||||
<View className='flex flex-row items-center justify-between mb-2 px-4'>
|
<View className='flex flex-row items-center justify-between mb-2 px-4'>
|
||||||
<Text className='text-lg font-bold'>
|
<Text className='text-lg font-bold'>
|
||||||
{t("home.downloads.tvseries")}
|
{t("home.downloads.series")}
|
||||||
</Text>
|
</Text>
|
||||||
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
|
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
|
||||||
<Text className='text-xs font-bold'>
|
<Text className='text-xs font-bold'>
|
||||||
@@ -288,7 +288,7 @@ export default function DownloadsPage() {
|
|||||||
{t("home.downloads.delete_all_movies_button")}
|
{t("home.downloads.delete_all_movies_button")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color='purple' onPress={deleteShows}>
|
<Button color='purple' onPress={deleteShows}>
|
||||||
{t("home.downloads.delete_all_tvseries_button")}
|
{t("home.downloads.delete_all_series_button")}
|
||||||
</Button>
|
</Button>
|
||||||
{otherMedia.length > 0 && (
|
{otherMedia.length > 0 && (
|
||||||
<Button color='purple' onPress={deleteOtherMedia}>
|
<Button color='purple' onPress={deleteOtherMedia}>
|
||||||
|
|||||||
@@ -179,18 +179,15 @@ export default function SettingsTV() {
|
|||||||
// Handle clearing all cache in the entire app
|
// Handle clearing all cache in the entire app
|
||||||
const handleClearCache = async () => {
|
const handleClearCache = async () => {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t("home.settings.storage.clear_all_cache_confirm", "Clear All Cache?"),
|
t("home.settings.storage.clear_all_cache_confirm"),
|
||||||
t(
|
t("home.settings.storage.clear_all_cache_confirm_desc"),
|
||||||
"home.settings.storage.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.",
|
|
||||||
),
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: t("common.cancel", "Cancel"),
|
text: t("common.cancel"),
|
||||||
style: "cancel",
|
style: "cancel",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: t("common.ok", "OK"),
|
text: t("common.ok"),
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
try {
|
try {
|
||||||
// 1. Clear React Query Cache (memory & MMKV)
|
// 1. Clear React Query Cache (memory & MMKV)
|
||||||
@@ -243,11 +240,8 @@ export default function SettingsTV() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to clear cache:", error);
|
console.error("Failed to clear cache:", error);
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t("home.settings.toasts.error_deleting_files", "Error"),
|
t("home.settings.toasts.error_deleting_files"),
|
||||||
t(
|
t("home.settings.storage.clear_all_cache_error_desc"),
|
||||||
"home.settings.storage.clear_all_cache_error_desc",
|
|
||||||
"An error occurred while clearing the cache.",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
6
bun.lock
6
bun.lock
@@ -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=="],
|
||||||
|
|
||||||
|
|||||||
@@ -69,17 +69,23 @@ export const SaveAccountModal: React.FC<SaveAccountModalProps> = ({
|
|||||||
[isAndroid],
|
[isAndroid],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isPresentedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
bottomSheetModalRef.current?.present();
|
bottomSheetModalRef.current?.present();
|
||||||
} else {
|
} else if (isPresentedRef.current) {
|
||||||
bottomSheetModalRef.current?.dismiss();
|
bottomSheetModalRef.current?.dismiss();
|
||||||
|
isPresentedRef.current = false;
|
||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
const handleSheetChanges = useCallback(
|
const handleSheetChanges = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
if (index === -1) {
|
if (index >= 0) {
|
||||||
|
isPresentedRef.current = true;
|
||||||
|
} else if (index === -1 && isPresentedRef.current) {
|
||||||
|
isPresentedRef.current = false;
|
||||||
resetState();
|
resetState();
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const TrackSheet: React.FC<Props> = ({
|
|||||||
<Text numberOfLines={1}>
|
<Text numberOfLines={1}>
|
||||||
{selected === -1 && streamType === "Subtitle"
|
{selected === -1 && streamType === "Subtitle"
|
||||||
? t("common.none")
|
? t("common.none")
|
||||||
: selectedSteam?.DisplayTitle || t("common.select", "Select")}
|
: selectedSteam?.DisplayTitle || t("common.select")}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ScrollView, View } from "react-native";
|
import { Platform, ScrollView, TextInput, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { TVDiscover } from "@/components/jellyseerr/discover/TVDiscover";
|
import { TVDiscover } from "@/components/jellyseerr/discover/TVDiscover";
|
||||||
@@ -231,26 +231,48 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
|
|||||||
paddingTop: insets.top + TOP_PADDING,
|
paddingTop: insets.top + TOP_PADDING,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Native tvOS search field (SwiftUI `.searchable`, our `tv-search`
|
{/* Search bar: native tvOS SwiftUI `.searchable` on Apple TV, standard
|
||||||
module). It renders the native search bar + grid keyboard and
|
TextInput fallback on Android TV (the native module is Apple-only). */}
|
||||||
forwards typed text into the existing query pipeline via setSearch;
|
{Platform.OS === "ios" ? (
|
||||||
our own results grid renders below. */}
|
<View
|
||||||
{/* No horizontal margin here: the native tvOS search bar centers itself
|
style={{
|
||||||
and renders a trailing "Hold to Dictate in <Language>" hint. Extra
|
marginBottom: 24,
|
||||||
margins squeeze the bar's width and clip that trailing hint, so let
|
height: SEARCH_AREA_HEIGHT,
|
||||||
the native view span the full width and own its own insets. */}
|
}}
|
||||||
<View
|
>
|
||||||
style={{
|
{/* No horizontal margin here: the native tvOS search bar centers
|
||||||
marginBottom: 24,
|
itself and renders a trailing "Hold to Dictate" hint. */}
|
||||||
height: SEARCH_AREA_HEIGHT,
|
<TvSearchView
|
||||||
}}
|
style={{ width: "100%", height: "100%" }}
|
||||||
>
|
placeholder={t("search.search")}
|
||||||
<TvSearchView
|
onChangeText={(e) => setSearch(e.nativeEvent.text)}
|
||||||
style={{ width: "100%", height: "100%" }}
|
/>
|
||||||
placeholder={t("search.search")}
|
</View>
|
||||||
onChangeText={(e) => setSearch(e.nativeEvent.text)}
|
) : (
|
||||||
/>
|
<View
|
||||||
</View>
|
style={{
|
||||||
|
marginHorizontal: HORIZONTAL_PADDING,
|
||||||
|
marginBottom: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
style={{
|
||||||
|
height: 56,
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: "#262626",
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
fontSize: 28,
|
||||||
|
color: "#fff",
|
||||||
|
}}
|
||||||
|
placeholder={t("search.search")}
|
||||||
|
placeholderTextColor='rgba(255,255,255,0.4)'
|
||||||
|
onChangeText={setSearch}
|
||||||
|
defaultValue=''
|
||||||
|
autoFocus={false}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as Application from "expo-application";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View, type ViewProps } from "react-native";
|
import { View, type ViewProps } from "react-native";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { getVersionInfo } from "@/utils/version";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
@@ -13,10 +13,9 @@ export const UserInfo: React.FC<Props> = ({ ...props }) => {
|
|||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const version =
|
// Graduated build identifier — see utils/version.ts:
|
||||||
Application?.nativeApplicationVersion ||
|
// dev → "0.54.1 · branch · commit", develop/CI → "0.54.1 · commit · #run", production → "0.54.1".
|
||||||
Application?.nativeBuildVersion ||
|
const { display: version } = getVersionInfo();
|
||||||
"N/A";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
|
|||||||
@@ -3,9 +3,13 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
export default {
|
const MediaTypes = {
|
||||||
Audio: "Audio",
|
Audio: "Audio",
|
||||||
Video: "Video",
|
Video: "Video",
|
||||||
Photo: "Photo",
|
Photo: "Photo",
|
||||||
Book: "Book",
|
Book: "Book",
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
|
export type MediaType = (typeof MediaTypes)[keyof typeof MediaTypes];
|
||||||
|
|
||||||
|
export default MediaTypes;
|
||||||
8
eas.json
8
eas.json
@@ -97,6 +97,14 @@
|
|||||||
"credentialsSource": "local",
|
"credentialsSource": "local",
|
||||||
"config": "ios-production.yml"
|
"config": "ios-production.yml"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ci": {
|
||||||
|
"extends": "production",
|
||||||
|
"autoIncrement": false
|
||||||
|
},
|
||||||
|
"ci_tv": {
|
||||||
|
"extends": "production_tv",
|
||||||
|
"autoIncrement": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"submit": {
|
"submit": {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application>
|
<application>
|
||||||
<receiver
|
<receiver android:name=".TvRecommendationsReceiver" android:exported="true">
|
||||||
android:name=".TvRecommendationsReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />
|
<action android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.tv.action.PREVIEW_PROGRAM_BROWSABLE_DISABLED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ import androidx.tvprovider.media.tv.PreviewProgram
|
|||||||
import androidx.tvprovider.media.tv.TvContractCompat
|
import androidx.tvprovider.media.tv.TvContractCompat
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
internal object TvRecommendationsPublisher {
|
internal object TvRecommendationsPublisher {
|
||||||
private const val TAG = "TvRecommendations"
|
private const val TAG = "TvRecommendations"
|
||||||
private const val PREFS_NAME = "StreamyfinTvRecommendations"
|
private const val PREFS_NAME = "StreamyfinTvRecommendations"
|
||||||
private const val KEY_PAYLOAD = "payload"
|
private const val KEY_PAYLOAD = "payload"
|
||||||
private const val KEY_CHANNEL_ID = "channelId"
|
private const val KEY_CHANNEL_ID_PREFIX = "channelId_"
|
||||||
private const val KEY_PROGRAM_IDS = "programIds"
|
private const val KEY_PROGRAM_IDS = "programIds"
|
||||||
private const val DEFAULT_CHANNEL_NAME = "Continue and Next Up"
|
private const val DEFAULT_CHANNEL_NAME = "Continue and Next Up"
|
||||||
|
|
||||||
@@ -61,31 +62,61 @@ internal object TvRecommendationsPublisher {
|
|||||||
|
|
||||||
fun clear(context: Context): Boolean {
|
fun clear(context: Context): Boolean {
|
||||||
val prefs = preferences(context)
|
val prefs = preferences(context)
|
||||||
val channelId = prefs.getLong(KEY_CHANNEL_ID, -1L)
|
|
||||||
val programIds = prefs.getString(KEY_PROGRAM_IDS, null)?.let(::JSONObject)
|
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
if (programIds != null) {
|
// KEY_PROGRAM_IDS is now { channelId: "{ providerId: programId }" }
|
||||||
|
val allProgramIds = prefs.getString(KEY_PROGRAM_IDS, null)?.let(::JSONObject)
|
||||||
|
if (allProgramIds != null) {
|
||||||
var deletedPrograms = 0
|
var deletedPrograms = 0
|
||||||
val keys = programIds.keys()
|
val channelKeys = allProgramIds.keys()
|
||||||
while (keys.hasNext()) {
|
while (channelKeys.hasNext()) {
|
||||||
val key = keys.next()
|
val channelIdStr = channelKeys.next()
|
||||||
val programId = programIds.optLong(key, -1L)
|
val programIdsJson = allProgramIds.optString(channelIdStr)
|
||||||
if (programId > 0L) {
|
if (programIdsJson.isBlank()) continue
|
||||||
contentResolver.delete(
|
|
||||||
TvContractCompat.buildPreviewProgramUri(programId),
|
try {
|
||||||
null,
|
val programIds = JSONObject(programIdsJson)
|
||||||
null
|
val keys = programIds.keys()
|
||||||
)
|
while (keys.hasNext()) {
|
||||||
deletedPrograms += 1
|
val providerId = keys.next()
|
||||||
|
val programId = programIds.optLong(providerId, -1L)
|
||||||
|
if (programId > 0L) {
|
||||||
|
deletePreviewProgram(contentResolver, programId)
|
||||||
|
deletedPrograms += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "clear(): failed to parse programIds for channel $channelIdStr", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify the channel
|
||||||
|
val channelId = channelIdStr.toLongOrNull() ?: -1L
|
||||||
|
if (channelId > 0L) {
|
||||||
|
try {
|
||||||
|
contentResolver.notifyChange(TvContractCompat.buildChannelUri(channelId), null)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "clear(): lost provider permission, cannot notify channel $channelId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove per-channel pref
|
||||||
|
prefs.edit().remove("programIds_$channelIdStr").apply()
|
||||||
}
|
}
|
||||||
Log.d(TAG, "clear(): deleted $deletedPrograms preview program(s)")
|
Log.d(TAG, "clear(): deleted $deletedPrograms preview program(s)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channelId > 0L) {
|
// Also handle legacy format (flat { providerId: programId }) for migration
|
||||||
contentResolver.notifyChange(TvContractCompat.buildChannelUri(channelId), null)
|
val legacyProgramIds = prefs.getString("legacy_" + KEY_PROGRAM_IDS, null)?.let(::JSONObject)
|
||||||
Log.d(TAG, "clear(): notified channel $channelId")
|
if (legacyProgramIds != null) {
|
||||||
|
val keys = legacyProgramIds.keys()
|
||||||
|
while (keys.hasNext()) {
|
||||||
|
val key = keys.next()
|
||||||
|
val programId = legacyProgramIds.optLong(key, -1L)
|
||||||
|
if (programId > 0L) {
|
||||||
|
deletePreviewProgram(contentResolver, programId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefs.edit().remove("legacy_" + KEY_PROGRAM_IDS).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
@@ -96,128 +127,274 @@ internal object TvRecommendationsPublisher {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single preview program from the TvProvider.
|
||||||
|
* Called by clear(), synchronize() (stale programs), and TvRecommendationsReceiver (user removed).
|
||||||
|
*/
|
||||||
|
fun deletePreviewProgram(context: Context, programId: Long) {
|
||||||
|
try {
|
||||||
|
context.contentResolver.delete(
|
||||||
|
TvContractCompat.buildPreviewProgramUri(programId),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
Log.d(TAG, "deletePreviewProgram(): deleted programId=$programId")
|
||||||
|
|
||||||
|
// Also remove from stored programIds prefs
|
||||||
|
removeProgramFromPrefs(context, programId)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "deletePreviewProgram(): lost provider permission for programId=$programId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deletePreviewProgram(contentResolver: android.content.ContentResolver, programId: Long) {
|
||||||
|
try {
|
||||||
|
contentResolver.delete(
|
||||||
|
TvContractCompat.buildPreviewProgramUri(programId),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "deletePreviewProgram(): lost provider permission for programId=$programId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeProgramFromPrefs(context: Context, programId: Long) {
|
||||||
|
val prefs = preferences(context)
|
||||||
|
val programIdsJson = prefs.getString(KEY_PROGRAM_IDS, null) ?: return
|
||||||
|
try {
|
||||||
|
val channelMap = JSONObject(programIdsJson)
|
||||||
|
val channelKeys = channelMap.keys()
|
||||||
|
while (channelKeys.hasNext()) {
|
||||||
|
val channelId = channelKeys.next()
|
||||||
|
val inner = channelMap.optJSONObject(channelId) ?: continue
|
||||||
|
val providerKeys = inner.keys()
|
||||||
|
while (providerKeys.hasNext()) {
|
||||||
|
val providerId = providerKeys.next()
|
||||||
|
if (inner.optLong(providerId, -1L) == programId) {
|
||||||
|
inner.remove(providerId)
|
||||||
|
if (inner.length() == 0) {
|
||||||
|
channelMap.remove(channelId)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefs.edit().putString(KEY_PROGRAM_IDS, channelMap.toString()).apply()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "removeProgramFromPrefs(): failed to update prefs for programId=$programId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun synchronize(context: Context, payload: JSONObject): Boolean {
|
private fun synchronize(context: Context, payload: JSONObject): Boolean {
|
||||||
val sections = payload.optJSONArray("sections") ?: JSONArray()
|
val sections = payload.optJSONArray("sections") ?: JSONArray()
|
||||||
val firstSection = if (sections.length() > 0) sections.optJSONObject(0) else null
|
if (sections.length() == 0) {
|
||||||
val sectionTitle = firstSection?.optString("title")?.takeIf { it.isNotBlank() } ?: DEFAULT_CHANNEL_NAME
|
Log.w(TAG, "synchronize(): no sections in payload")
|
||||||
val items = firstSection?.optJSONArray("items") ?: JSONArray()
|
|
||||||
|
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"synchronize(): using section \"$sectionTitle\" with ${items.length()} item(s)"
|
|
||||||
)
|
|
||||||
|
|
||||||
val channelId = getOrCreateChannel(context, sectionTitle)
|
|
||||||
if (channelId <= 0L) {
|
|
||||||
Log.w(TAG, "synchronize(): failed to get or create preview channel")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "synchronize(): publishing into channelId=$channelId")
|
val prefs = preferences(context)
|
||||||
|
val allNextProgramIds = JSONObject()
|
||||||
|
var totalActive = 0
|
||||||
|
var totalDeleted = 0
|
||||||
|
|
||||||
val previousProgramIds = preferences(context)
|
for (sectionIndex in 0 until sections.length()) {
|
||||||
.getString(KEY_PROGRAM_IDS, null)
|
val section = sections.optJSONObject(sectionIndex) ?: continue
|
||||||
?.let(::JSONObject)
|
val sectionTitle = section.optString("title")?.takeIf { it.isNotBlank() } ?: DEFAULT_CHANNEL_NAME
|
||||||
?: JSONObject()
|
val items = section.optJSONArray("items") ?: JSONArray()
|
||||||
val nextProgramIds = JSONObject()
|
|
||||||
val activeProviderIds = mutableSetOf<String>()
|
|
||||||
|
|
||||||
for (index in 0 until items.length()) {
|
Log.d(
|
||||||
val item = items.optJSONObject(index) ?: continue
|
TAG,
|
||||||
val providerId = item.optString("id")
|
"synchronize(): section \"$sectionTitle\" ($sectionIndex/${sections.length()}) with ${items.length()} item(s)"
|
||||||
if (providerId.isBlank()) continue
|
|
||||||
|
|
||||||
val programId = upsertPreviewProgram(
|
|
||||||
context = context,
|
|
||||||
channelId = channelId,
|
|
||||||
item = item,
|
|
||||||
previousProgramId = previousProgramIds.optLong(providerId, -1L),
|
|
||||||
weight = index
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (programId > 0L) {
|
val channelId = getOrCreateChannel(context, sectionTitle)
|
||||||
activeProviderIds += providerId
|
if (channelId <= 0L) {
|
||||||
nextProgramIds.put(providerId, programId)
|
Log.w(TAG, "synchronize(): failed to get or create channel for \"$sectionTitle\"")
|
||||||
Log.d(TAG, "synchronize(): upserted program for item=$providerId programId=$programId")
|
continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var deletedPrograms = 0
|
// Per Android docs: check channel.isBrowsable() and request if needed.
|
||||||
val previousKeys = previousProgramIds.keys()
|
if (!isChannelBrowsable(context, channelId)) {
|
||||||
while (previousKeys.hasNext()) {
|
Log.d(TAG, "synchronize(): channel $channelId not browsable, requesting browsable")
|
||||||
val providerId = previousKeys.next()
|
TvContractCompat.requestChannelBrowsable(context, channelId)
|
||||||
if (activeProviderIds.contains(providerId)) continue
|
}
|
||||||
|
|
||||||
val programId = previousProgramIds.optLong(providerId, -1L)
|
val prefKey = "programIds_$channelId"
|
||||||
if (programId > 0L) {
|
val previousProgramIds = prefs.getString(prefKey, null)
|
||||||
context.contentResolver.delete(
|
?.let(::JSONObject)
|
||||||
TvContractCompat.buildPreviewProgramUri(programId),
|
?: JSONObject()
|
||||||
null,
|
val nextProgramIds = JSONObject()
|
||||||
null
|
val activeProviderIds = mutableSetOf<String>()
|
||||||
|
|
||||||
|
for (index in 0 until items.length()) {
|
||||||
|
val item = items.optJSONObject(index) ?: continue
|
||||||
|
val providerId = item.optString("id")
|
||||||
|
if (providerId.isBlank()) continue
|
||||||
|
|
||||||
|
val programId = upsertPreviewProgram(
|
||||||
|
context = context,
|
||||||
|
channelId = channelId,
|
||||||
|
item = item,
|
||||||
|
previousProgramId = previousProgramIds.optLong(providerId, -1L),
|
||||||
|
weight = index
|
||||||
)
|
)
|
||||||
deletedPrograms += 1
|
|
||||||
Log.d(TAG, "synchronize(): deleted stale programId=$programId for item=$providerId")
|
if (programId > 0L) {
|
||||||
|
activeProviderIds += providerId
|
||||||
|
nextProgramIds.put(providerId, programId)
|
||||||
|
Log.d(TAG, "synchronize(): upserted program for item=$providerId programId=$programId")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var deletedPrograms = 0
|
||||||
|
val previousKeys = previousProgramIds.keys()
|
||||||
|
while (previousKeys.hasNext()) {
|
||||||
|
val providerId = previousKeys.next()
|
||||||
|
if (activeProviderIds.contains(providerId)) continue
|
||||||
|
|
||||||
|
val programId = previousProgramIds.optLong(providerId, -1L)
|
||||||
|
if (programId > 0L) {
|
||||||
|
deletePreviewProgram(context, programId)
|
||||||
|
deletedPrograms += 1
|
||||||
|
Log.d(TAG, "synchronize(): deleted stale programId=$programId for item=$providerId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allNextProgramIds.put(channelId.toString(), nextProgramIds.toString())
|
||||||
|
prefs.edit().putString(prefKey, nextProgramIds.toString()).apply()
|
||||||
|
totalActive += activeProviderIds.size
|
||||||
|
totalDeleted += deletedPrograms
|
||||||
|
|
||||||
|
logProviderState(context, channelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences(context)
|
// Store all channel program IDs for clear() to use
|
||||||
.edit()
|
prefs.edit().putString(KEY_PROGRAM_IDS, allNextProgramIds.toString()).apply()
|
||||||
.putLong(KEY_CHANNEL_ID, channelId)
|
|
||||||
.putString(KEY_PROGRAM_IDS, nextProgramIds.toString())
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
logProviderState(context, channelId)
|
|
||||||
|
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"synchronize(): completed with ${activeProviderIds.size} active program(s), deleted $deletedPrograms stale program(s)"
|
"synchronize(): completed across ${sections.length()} section(s), $totalActive active program(s), deleted $totalDeleted stale program(s)"
|
||||||
)
|
)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query provider to check if a channel is browsable.
|
||||||
|
* Per Android docs: "check channel.isBrowsable() before updating programs."
|
||||||
|
*/
|
||||||
|
private fun isChannelBrowsable(context: Context, channelId: Long): Boolean {
|
||||||
|
return try {
|
||||||
|
context.contentResolver.query(
|
||||||
|
TvContractCompat.buildChannelUri(channelId),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val browsableIndex = cursor.getColumnIndex(TvContractCompat.Channels.COLUMN_BROWSABLE)
|
||||||
|
if (browsableIndex >= 0) cursor.getInt(browsableIndex) == 1 else true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} ?: false
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "isChannelBrowsable(): lost provider permission for channelId=$channelId", e)
|
||||||
|
true // Assume browsable if we can't check, to avoid blocking updates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query provider to verify a channel actually exists.
|
||||||
|
* Prevents the channel-delete-recreate bug: if update() returns 0 rows
|
||||||
|
* we must first check whether the channel was deleted by the system
|
||||||
|
* or if the update simply failed for another reason.
|
||||||
|
*/
|
||||||
|
private fun channelExistsInProvider(context: Context, channelId: Long): Boolean {
|
||||||
|
return try {
|
||||||
|
context.contentResolver.query(
|
||||||
|
TvContractCompat.buildChannelUri(channelId),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
cursor.moveToFirst()
|
||||||
|
} ?: false
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "channelExistsInProvider(): lost provider permission for channelId=$channelId", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getOrCreateChannel(context: Context, displayName: String): Long {
|
private fun getOrCreateChannel(context: Context, displayName: String): Long {
|
||||||
val prefs = preferences(context)
|
val prefs = preferences(context)
|
||||||
val existingChannelId = prefs.getLong(KEY_CHANNEL_ID, -1L)
|
val channelKey = getChannelKey(displayName)
|
||||||
|
val existingChannelId = prefs.getLong(channelKey, -1L)
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
if (existingChannelId > 0L) {
|
if (existingChannelId > 0L) {
|
||||||
val updated = Channel.Builder()
|
// Query provider first to verify channel actually exists (prevents recreate bug)
|
||||||
.setType(TvContractCompat.Channels.TYPE_PREVIEW)
|
val exists = channelExistsInProvider(context, existingChannelId)
|
||||||
.setDisplayName(displayName)
|
|
||||||
.setAppLinkIntentUri(buildIntentUri(context, "streamyfin://"))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val updatedRows = contentResolver.update(
|
if (exists) {
|
||||||
TvContractCompat.buildChannelUri(existingChannelId),
|
// Channel exists — update it in place, never recreate
|
||||||
updated.toContentValues(),
|
val updated = Channel.Builder()
|
||||||
null,
|
.setType(TvContractCompat.Channels.TYPE_PREVIEW)
|
||||||
null
|
.setDisplayName(displayName)
|
||||||
)
|
.setAppLinkIntentUri(buildIntentUri(context, "streamyfin://"))
|
||||||
|
.build()
|
||||||
|
|
||||||
if (updatedRows > 0) {
|
try {
|
||||||
TvContractCompat.requestChannelBrowsable(context, existingChannelId)
|
val updatedRows = contentResolver.update(
|
||||||
storeChannelLogo(context, existingChannelId)
|
TvContractCompat.buildChannelUri(existingChannelId),
|
||||||
Log.d(TAG, "getOrCreateChannel(): updated existing channelId=$existingChannelId and requested browsable")
|
updated.toContentValues(),
|
||||||
return existingChannelId
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
if (updatedRows > 0) {
|
||||||
|
TvContractCompat.requestChannelBrowsable(context, existingChannelId)
|
||||||
|
storeChannelLogo(context, existingChannelId)
|
||||||
|
Log.d(TAG, "getOrCreateChannel(): updated existing channelId=$existingChannelId and requested browsable")
|
||||||
|
return existingChannelId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update returned 0 rows but channel exists — log and return existing ID, don't recreate
|
||||||
|
Log.e(TAG, "getOrCreateChannel(): update returned 0 for existing channelId=$existingChannelId but channel exists — not recreating")
|
||||||
|
return existingChannelId
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "getOrCreateChannel(): lost provider permission updating channelId=$existingChannelId", e)
|
||||||
|
return existingChannelId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.w(TAG, "getOrCreateChannel(): stored channelId=$existingChannelId was stale, recreating")
|
// Channel truly doesn't exist in provider — recreate
|
||||||
prefs.edit().remove(KEY_CHANNEL_ID).apply()
|
Log.w(TAG, "getOrCreateChannel(): channelId=$existingChannelId not in provider, recreating")
|
||||||
|
prefs.edit().remove(channelKey).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new channel
|
||||||
val channel = Channel.Builder()
|
val channel = Channel.Builder()
|
||||||
.setType(TvContractCompat.Channels.TYPE_PREVIEW)
|
.setType(TvContractCompat.Channels.TYPE_PREVIEW)
|
||||||
.setDisplayName(displayName)
|
.setDisplayName(displayName)
|
||||||
.setAppLinkIntentUri(buildIntentUri(context, "streamyfin://"))
|
.setAppLinkIntentUri(buildIntentUri(context, "streamyfin://"))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val channelUri = contentResolver.insert(
|
val channelUri = try {
|
||||||
TvContractCompat.Channels.CONTENT_URI,
|
contentResolver.insert(
|
||||||
channel.toContentValues()
|
TvContractCompat.Channels.CONTENT_URI,
|
||||||
) ?: return -1L
|
channel.toContentValues()
|
||||||
|
)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.e(TAG, "getOrCreateChannel(): lost provider permission, cannot create channel", e)
|
||||||
|
null
|
||||||
|
} ?: return -1L
|
||||||
|
|
||||||
val channelId = ContentUris.parseId(channelUri)
|
val channelId = ContentUris.parseId(channelUri)
|
||||||
|
prefs.edit().putLong(channelKey, channelId).apply()
|
||||||
TvContractCompat.requestChannelBrowsable(context, channelId)
|
TvContractCompat.requestChannelBrowsable(context, channelId)
|
||||||
storeChannelLogo(context, channelId)
|
storeChannelLogo(context, channelId)
|
||||||
Log.d(TAG, "getOrCreateChannel(): created new channelId=$channelId displayName=\"$displayName\"")
|
Log.d(TAG, "getOrCreateChannel(): created new channelId=$channelId displayName=\"$displayName\"")
|
||||||
@@ -225,6 +402,10 @@ internal object TvRecommendationsPublisher {
|
|||||||
return channelId
|
return channelId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getChannelKey(displayName: String): String {
|
||||||
|
return KEY_CHANNEL_ID_PREFIX + displayName.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
private fun upsertPreviewProgram(
|
private fun upsertPreviewProgram(
|
||||||
context: Context,
|
context: Context,
|
||||||
channelId: Long,
|
channelId: Long,
|
||||||
@@ -249,42 +430,67 @@ internal object TvRecommendationsPublisher {
|
|||||||
builder.setDescription(it)
|
builder.setDescription(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per Android docs: use unique URIs for all images to avoid stale cache
|
||||||
imageUrl.takeIf { it.isNotBlank() }?.let {
|
imageUrl.takeIf { it.isNotBlank() }?.let {
|
||||||
val imageUri = Uri.parse(it)
|
val uniqueImageUrl = appendCacheBuster(it)
|
||||||
|
val imageUri = Uri.parse(uniqueImageUrl)
|
||||||
builder.setPosterArtUri(imageUri)
|
builder.setPosterArtUri(imageUri)
|
||||||
builder.setThumbnailUri(imageUri)
|
builder.setThumbnailUri(imageUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val contentValues = builder.build().toContentValues()
|
val contentValues = builder.build().toContentValues()
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
if (previousProgramId > 0L) {
|
if (previousProgramId > 0L) {
|
||||||
val updatedRows = contentResolver.update(
|
try {
|
||||||
TvContractCompat.buildPreviewProgramUri(previousProgramId),
|
val updatedRows = contentResolver.update(
|
||||||
contentValues,
|
TvContractCompat.buildPreviewProgramUri(previousProgramId),
|
||||||
null,
|
contentValues,
|
||||||
null
|
null,
|
||||||
)
|
null
|
||||||
|
)
|
||||||
|
|
||||||
if (updatedRows > 0) {
|
if (updatedRows > 0) {
|
||||||
Log.d(TAG, "upsertPreviewProgram(): updated existing programId=$previousProgramId")
|
Log.d(TAG, "upsertPreviewProgram(): updated existing programId=$previousProgramId")
|
||||||
return previousProgramId
|
return previousProgramId
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.w(TAG, "upsertPreviewProgram(): existing programId=$previousProgramId was stale, inserting new row")
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "upsertPreviewProgram(): lost provider permission for programId=$previousProgramId", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.w(TAG, "upsertPreviewProgram(): existing programId=$previousProgramId was stale, inserting new row")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val insertedUri = contentResolver.insert(
|
val insertedUri = try {
|
||||||
TvContractCompat.PreviewPrograms.CONTENT_URI,
|
contentResolver.insert(
|
||||||
contentValues
|
TvContractCompat.PreviewPrograms.CONTENT_URI,
|
||||||
) ?: return -1L
|
contentValues
|
||||||
|
)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "upsertPreviewProgram(): lost provider permission, cannot insert program", e)
|
||||||
|
null
|
||||||
|
} ?: return -1L
|
||||||
|
|
||||||
val programId = ContentUris.parseId(insertedUri)
|
val programId = ContentUris.parseId(insertedUri)
|
||||||
Log.d(TAG, "upsertPreviewProgram(): inserted new programId=$programId")
|
Log.d(TAG, "upsertPreviewProgram(): inserted new programId=$programId")
|
||||||
return programId
|
return programId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a stable cache key derived from the image URL.
|
||||||
|
* The Jellyfin image URLs already include a `tag=` query param (etag)
|
||||||
|
* that changes whenever the image content changes, so a deterministic
|
||||||
|
* hash of the URL is sufficient — the param only changes when the URL
|
||||||
|
* (and therefore the image) actually changes, avoiding unnecessary
|
||||||
|
* re-downloads on every sync.
|
||||||
|
*/
|
||||||
|
private fun appendCacheBuster(imageUrl: String): String {
|
||||||
|
val digest = MessageDigest.getInstance("MD5").digest(imageUrl.toByteArray())
|
||||||
|
val hash = digest.joinToString("") { "%02x".format(it) }.substring(0, 16)
|
||||||
|
val separator = if (imageUrl.contains("?")) "&" else "?"
|
||||||
|
return "$imageUrl${separator}_v=$hash"
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildIntentUri(context: Context, deepLink: String): Uri {
|
private fun buildIntentUri(context: Context, deepLink: String): Uri {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
data = Uri.parse(deepLink)
|
data = Uri.parse(deepLink)
|
||||||
@@ -306,13 +512,17 @@ internal object TvRecommendationsPublisher {
|
|||||||
|
|
||||||
private fun storeChannelLogo(context: Context, channelId: Long) {
|
private fun storeChannelLogo(context: Context, channelId: Long) {
|
||||||
val bitmap = applicationIconBitmap(context) ?: return
|
val bitmap = applicationIconBitmap(context) ?: return
|
||||||
val outputStream = context.contentResolver.openOutputStream(
|
try {
|
||||||
TvContractCompat.buildChannelLogoUri(channelId)
|
val outputStream = context.contentResolver.openOutputStream(
|
||||||
) ?: return
|
TvContractCompat.buildChannelLogoUri(channelId)
|
||||||
|
) ?: return
|
||||||
|
|
||||||
outputStream.use { stream ->
|
outputStream.use { stream ->
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||||
stream.flush()
|
stream.flush()
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "storeChannelLogo(): lost provider permission for channelId=$channelId", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,9 +551,14 @@ internal object TvRecommendationsPublisher {
|
|||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getChannelId(context: Context, displayName: String = DEFAULT_CHANNEL_NAME): Long {
|
||||||
|
return preferences(context).getLong(getChannelKey(displayName), -1L)
|
||||||
|
}
|
||||||
|
|
||||||
private fun preferences(context: Context): SharedPreferences {
|
private fun preferences(context: Context): SharedPreferences {
|
||||||
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logProviderState(context: Context, channelId: Long) {
|
private fun logProviderState(context: Context, channelId: Long) {
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
@@ -372,8 +587,10 @@ internal object TvRecommendationsPublisher {
|
|||||||
Log.w(TAG, "logProviderState(): channelId=$channelId exists=false")
|
Log.w(TAG, "logProviderState(): channelId=$channelId exists=false")
|
||||||
}
|
}
|
||||||
} ?: Log.w(TAG, "logProviderState(): channel query returned null for channelId=$channelId")
|
} ?: Log.w(TAG, "logProviderState(): channel query returned null for channelId=$channelId")
|
||||||
} catch (error: Exception) {
|
} catch (error: SecurityException) {
|
||||||
Log.w(TAG, "logProviderState(): failed to query channelId=$channelId", error)
|
Log.w(TAG, "logProviderState(): lost provider permission for channelId=$channelId", error)
|
||||||
}
|
} catch (error: Exception) {
|
||||||
|
Log.w(TAG, "logProviderState(): failed to query channelId=$channelId", error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,24 @@ package expo.modules.tvrecommendations
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.ContentUris
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.tvprovider.media.tv.TvContractCompat
|
import androidx.tvprovider.media.tv.TvContractCompat
|
||||||
|
|
||||||
class TvRecommendationsReceiver : BroadcastReceiver() {
|
class TvRecommendationsReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.action != TvContractCompat.ACTION_INITIALIZE_PROGRAMS) {
|
when (intent.action) {
|
||||||
return
|
TvContractCompat.ACTION_INITIALIZE_PROGRAMS -> {
|
||||||
|
Log.d("TvRecommendations", "Handling INITIALIZE_PROGRAMS broadcast")
|
||||||
|
TvRecommendationsPublisher.refreshFromCache(context)
|
||||||
|
}
|
||||||
|
"android.media.tv.action.PREVIEW_PROGRAM_BROWSABLE_DISABLED" -> {
|
||||||
|
val programId = intent.data?.let { ContentUris.parseId(it) } ?: -1L
|
||||||
|
if (programId > 0L) {
|
||||||
|
Log.d("TvRecommendations", "Handling PREVIEW_PROGRAM_BROWSABLE_DISABLED for programId=$programId")
|
||||||
|
TvRecommendationsPublisher.deletePreviewProgram(context, programId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TvRecommendations", "Handling INITIALIZE_PROGRAMS broadcast")
|
|
||||||
TvRecommendationsPublisher.refreshFromCache(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { requireNativeView } from "expo";
|
import { requireNativeView } from "expo";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { View } from "react-native";
|
import type { View } from "react-native";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
import type { TvSearchViewProps } from "./TvSearchView.types";
|
import type { TvSearchViewProps } from "./TvSearchView.types";
|
||||||
|
|
||||||
|
// The native TvSearchModule is Apple-only (tvOS SwiftUI `.searchable`).
|
||||||
|
// On Android the component is never rendered, but we must avoid calling
|
||||||
|
// `requireNativeView` at module-scope because it would crash on import.
|
||||||
const NativeView: React.ComponentType<
|
const NativeView: React.ComponentType<
|
||||||
TvSearchViewProps & React.RefAttributes<View>
|
TvSearchViewProps & React.RefAttributes<View>
|
||||||
> = requireNativeView("TvSearchModule");
|
> =
|
||||||
|
Platform.OS === "ios"
|
||||||
|
? requireNativeView("TvSearchModule")
|
||||||
|
: ((() => null) as any);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forwards its ref to the underlying native view so it can be used as a
|
* Forwards its ref to the underlying native view so it can be used as a
|
||||||
|
|||||||
@@ -17,12 +17,14 @@
|
|||||||
"ios:unsigned-build": "cross-env EXPO_TV=0 bun scripts/ios/build-ios.ts --production",
|
"ios:unsigned-build": "cross-env EXPO_TV=0 bun scripts/ios/build-ios.ts --production",
|
||||||
"ios:unsigned-build:tv": "cross-env EXPO_TV=1 bun scripts/ios/build-ios.ts --production",
|
"ios:unsigned-build:tv": "cross-env EXPO_TV=1 bun scripts/ios/build-ios.ts --production",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"typecheck": "node scripts/typecheck.js",
|
"typecheck": "bun scripts/typecheck.ts",
|
||||||
"check": "biome check . --max-diagnostics 1000",
|
"check": "biome check . --max-diagnostics 1000",
|
||||||
"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.ts",
|
||||||
|
"i18n:fix-unused": "bun scripts/check-i18n-keys.ts --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",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
} from "@/utils/secureCredentials";
|
} from "@/utils/secureCredentials";
|
||||||
import { store } from "@/utils/store";
|
import { store } from "@/utils/store";
|
||||||
import { clearTVDiscoverySafely } from "@/utils/tvDiscovery/sync";
|
import { clearTVDiscoverySafely } from "@/utils/tvDiscovery/sync";
|
||||||
|
import { APP_VERSION } from "@/utils/version";
|
||||||
|
|
||||||
interface Server {
|
interface Server {
|
||||||
address: string;
|
address: string;
|
||||||
@@ -53,7 +54,7 @@ const initialApi = (() => {
|
|||||||
const id = getOrSetDeviceId();
|
const id = getOrSetDeviceId();
|
||||||
const deviceName = getDeviceNameSync();
|
const deviceName = getDeviceNameSync();
|
||||||
const jellyfinInstance = new Jellyfin({
|
const jellyfinInstance = new Jellyfin({
|
||||||
clientInfo: { name: "Streamyfin", version: "0.54.1" },
|
clientInfo: { name: "Streamyfin", version: APP_VERSION },
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
name: deviceName,
|
name: deviceName,
|
||||||
id,
|
id,
|
||||||
@@ -135,7 +136,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
const id = getOrSetDeviceId();
|
const id = getOrSetDeviceId();
|
||||||
const deviceName = getDeviceNameSync();
|
const deviceName = getDeviceNameSync();
|
||||||
return new Jellyfin({
|
return new Jellyfin({
|
||||||
clientInfo: { name: "Streamyfin", version: "0.54.1" },
|
clientInfo: { name: "Streamyfin", version: APP_VERSION },
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
name: deviceName,
|
name: deviceName,
|
||||||
id,
|
id,
|
||||||
@@ -169,7 +170,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
return {
|
return {
|
||||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||||
Platform.OS === "android" ? "Android" : "iOS"
|
Platform.OS === "android" ? "Android" : "iOS"
|
||||||
}, DeviceId="${deviceId}", Version="0.54.1"`,
|
}, DeviceId="${deviceId}", Version="${APP_VERSION}"`,
|
||||||
};
|
};
|
||||||
}, [deviceId]);
|
}, [deviceId]);
|
||||||
|
|
||||||
|
|||||||
288
scripts/check-i18n-keys.ts
Normal file
288
scripts/check-i18n-keys.ts
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
#!/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.ts # report + exit 1 on missing OR unused
|
||||||
|
* bun scripts/check-i18n-keys.ts --unused=warn # exit 1 only on missing; unused = warning
|
||||||
|
* bun scripts/check-i18n-keys.ts --unused=off # ignore unused entirely
|
||||||
|
* bun scripts/check-i18n-keys.ts --json # machine-readable output
|
||||||
|
* bun scripts/check-i18n-keys.ts --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";
|
||||||
|
|
||||||
|
type LocaleTree = { [key: string]: LocaleTree | string };
|
||||||
|
|
||||||
|
interface I18nConfig {
|
||||||
|
localesDir: string;
|
||||||
|
sourceLocale: string;
|
||||||
|
srcDirs: string[];
|
||||||
|
srcExtensions: string[];
|
||||||
|
excludeDirs: string[];
|
||||||
|
ignoreUnused: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROOT = process.cwd();
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const flag = (name: string, def: string | boolean): string | boolean => {
|
||||||
|
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: I18nConfig = {
|
||||||
|
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: I18nConfig = existsSync(CONFIG_PATH)
|
||||||
|
? {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
...(JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<I18nConfig>),
|
||||||
|
}
|
||||||
|
: DEFAULT_CONFIG;
|
||||||
|
|
||||||
|
// ---- helpers ----
|
||||||
|
const flatten = (
|
||||||
|
obj: LocaleTree,
|
||||||
|
prefix = "",
|
||||||
|
out: Record<string, string> = {},
|
||||||
|
): Record<string, string> => {
|
||||||
|
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 as string;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const globMatch = (key: string, pattern: string): boolean => {
|
||||||
|
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: string, files: string[] = []): string[] => {
|
||||||
|
let entries: string[];
|
||||||
|
try {
|
||||||
|
entries = readdirSync(dir);
|
||||||
|
} catch {
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
for (const name of entries) {
|
||||||
|
const full = join(dir, name);
|
||||||
|
let st: ReturnType<typeof statSync>;
|
||||||
|
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")) as LocaleTree),
|
||||||
|
);
|
||||||
|
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<string>(); // keys passed to t(...) / i18nKey — used for MISSING detection
|
||||||
|
const dynamicPrefixes = new Set<string>();
|
||||||
|
const fullyDynamic: Array<{ file: string; line: number }> = [];
|
||||||
|
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: string): string =>
|
||||||
|
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: string): boolean =>
|
||||||
|
codeBlob.includes(`"${key}"`) ||
|
||||||
|
codeBlob.includes(`'${key}'`) ||
|
||||||
|
codeBlob.includes(`\`${key}\``);
|
||||||
|
const isUsed = (key: string): boolean =>
|
||||||
|
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: LocaleTree, parts: string[]): void => {
|
||||||
|
const [head, ...rest] = parts;
|
||||||
|
if (!(head in obj)) return;
|
||||||
|
if (rest.length === 0) {
|
||||||
|
delete obj[head];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const child = obj[head];
|
||||||
|
if (!child || typeof child !== "object") return;
|
||||||
|
removeKey(child, rest);
|
||||||
|
if (Object.keys(child).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")) as LocaleTree;
|
||||||
|
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.ts --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);
|
||||||
@@ -21,8 +21,14 @@
|
|||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
interface Issue {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
body: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse a numeric env var, falling back to `def` only when unset/empty/NaN so an explicit 0 is honoured.
|
// Parse a numeric env var, falling back to `def` only when unset/empty/NaN so an explicit 0 is honoured.
|
||||||
const numEnv = (name, def) => {
|
const numEnv = (name: string, def: number): number => {
|
||||||
const raw = process.env[name];
|
const raw = process.env[name];
|
||||||
if (raw === undefined || raw === "") return def;
|
if (raw === undefined || raw === "") return def;
|
||||||
const n = Number(raw);
|
const n = Number(raw);
|
||||||
@@ -51,9 +57,9 @@ const STOP = new Set(
|
|||||||
).split(/\s+/),
|
).split(/\s+/),
|
||||||
);
|
);
|
||||||
|
|
||||||
const stem = (w) => w.replace(/(ing|ed|es|s)$/, "");
|
const stem = (w: string): string => w.replace(/(ing|ed|es|s)$/, "");
|
||||||
|
|
||||||
const tokens = (s) =>
|
const tokens = (s: string | null): string[] =>
|
||||||
(s || "")
|
(s || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/```[\s\S]*?```/g, " ") // drop code blocks
|
.replace(/```[\s\S]*?```/g, " ") // drop code blocks
|
||||||
@@ -65,7 +71,7 @@ const tokens = (s) =>
|
|||||||
.map(stem)
|
.map(stem)
|
||||||
.filter((w) => w.length > 2);
|
.filter((w) => w.length > 2);
|
||||||
|
|
||||||
const jaccard = (a, b) => {
|
const jaccard = (a: string[], b: string[]): number => {
|
||||||
const A = new Set(a);
|
const A = new Set(a);
|
||||||
const B = new Set(b);
|
const B = new Set(b);
|
||||||
if (!A.size || !B.size) return 0;
|
if (!A.size || !B.size) return 0;
|
||||||
@@ -76,14 +82,14 @@ const jaccard = (a, b) => {
|
|||||||
|
|
||||||
const newTitle = tokens(TITLE);
|
const newTitle = tokens(TITLE);
|
||||||
const newBody = tokens(BODY);
|
const newBody = tokens(BODY);
|
||||||
const score = (o) =>
|
const score = (o: Issue): number =>
|
||||||
0.6 * jaccard(newTitle, tokens(o.title)) +
|
0.6 * jaccard(newTitle, tokens(o.title)) +
|
||||||
0.4 * jaccard(newBody, tokens(o.body));
|
0.4 * jaccard(newBody, tokens(o.body));
|
||||||
|
|
||||||
// fetch open issues (excluding PRs and the new issue itself)
|
// fetch open issues (excluding PRs and the new issue itself)
|
||||||
let issues;
|
let issues: Issue[];
|
||||||
if (process.env.DUP_FIXTURE) {
|
if (process.env.DUP_FIXTURE) {
|
||||||
issues = JSON.parse(readFileSync(process.env.DUP_FIXTURE, "utf8"));
|
issues = JSON.parse(readFileSync(process.env.DUP_FIXTURE, "utf8")) as Issue[];
|
||||||
} else {
|
} else {
|
||||||
const raw = execFileSync(
|
const raw = execFileSync(
|
||||||
"gh",
|
"gh",
|
||||||
@@ -105,7 +111,7 @@ if (process.env.DUP_FIXTURE) {
|
|||||||
issues = raw
|
issues = raw
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((l) => JSON.parse(l));
|
.map((l) => JSON.parse(l) as Issue);
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches = issues
|
const matches = issues
|
||||||
@@ -123,7 +129,7 @@ if (!matches.length) {
|
|||||||
// Neutralise other issues' titles before echoing them back: break @mentions and
|
// Neutralise other issues' titles before echoing them back: break @mentions and
|
||||||
// strip markdown/HTML control chars so a maliciously-named issue can't ping people
|
// strip markdown/HTML control chars so a maliciously-named issue can't ping people
|
||||||
// or inject formatting into our comment. GitHub linkifies "#123" on its own.
|
// or inject formatting into our comment. GitHub linkifies "#123" on its own.
|
||||||
const safeTitle = (t) =>
|
const safeTitle = (t: string): string =>
|
||||||
(t || "")
|
(t || "")
|
||||||
.replace(/@/g, "@")
|
.replace(/@/g, "@")
|
||||||
.replace(/[`<>|*_~[\]]/g, " ")
|
.replace(/[`<>|*_~[\]]/g, " ")
|
||||||
46
scripts/i18n-keys.config.json
Normal file
46
scripts/i18n-keys.config.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const _fs = require("node:fs");
|
|
||||||
const path = require("node:path");
|
|
||||||
const process = require("node:process");
|
|
||||||
const { execSync } = require("node:child_process");
|
|
||||||
|
|
||||||
const root = process.cwd();
|
|
||||||
// const tvosPath = path.join(root, 'iostv');
|
|
||||||
// const iosPath = path.join(root, 'iosmobile');
|
|
||||||
// const androidPath = path.join(root, 'androidmobile');
|
|
||||||
// const androidTVPath = path.join(root, 'androidtv');
|
|
||||||
// const device = process.argv[2];
|
|
||||||
// const platform = process.argv[2];
|
|
||||||
const isTV = process.env.EXPO_TV || false;
|
|
||||||
|
|
||||||
const paths = new Map([
|
|
||||||
["tvos", path.join(root, "iostv")],
|
|
||||||
["ios", path.join(root, "iosmobile")],
|
|
||||||
["android", path.join(root, "androidmobile")],
|
|
||||||
["androidtv", path.join(root, "androidtv")],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// const platformPath = paths.get(platform);
|
|
||||||
|
|
||||||
if (isTV) {
|
|
||||||
stdout = execSync(
|
|
||||||
`mkdir -p ${paths.get("tvos")}; ln -nsf ${paths.get("tvos")} ios`,
|
|
||||||
);
|
|
||||||
console.log(stdout.toString());
|
|
||||||
stdout = execSync(
|
|
||||||
`mkdir -p ${paths.get("androidtv")}; ln -nsf ${paths.get(
|
|
||||||
"androidtv",
|
|
||||||
)} android`,
|
|
||||||
);
|
|
||||||
console.log(stdout.toString());
|
|
||||||
} else {
|
|
||||||
stdout = execSync(
|
|
||||||
`mkdir -p ${paths.get("ios")}; ln -nsf ${paths.get("ios")} ios`,
|
|
||||||
);
|
|
||||||
console.log(stdout.toString());
|
|
||||||
stdout = execSync(
|
|
||||||
`mkdir -p ${paths.get("android")}; ln -nsf ${paths.get("android")} android`,
|
|
||||||
);
|
|
||||||
console.log(stdout.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// target = "";
|
|
||||||
// switch (platform) {
|
|
||||||
// case "tvos":
|
|
||||||
// target = "ios";
|
|
||||||
// break;
|
|
||||||
// case "ios":
|
|
||||||
// target = "ios";
|
|
||||||
// break;
|
|
||||||
// case "android":
|
|
||||||
// target = "android";
|
|
||||||
// break;
|
|
||||||
// case "androidtv":
|
|
||||||
// target = "android";
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
const { execFileSync } = require("node:child_process");
|
import { execFileSync } from "node:child_process";
|
||||||
const process = require("node:process");
|
import { createRequire } from "node:module";
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
// Enhanced ANSI color codes and styles
|
// Enhanced ANSI color codes and styles
|
||||||
const colors = {
|
const colors = {
|
||||||
@@ -32,7 +35,7 @@ const centeredTitle = " ".repeat(titlePadding) + title;
|
|||||||
|
|
||||||
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
||||||
|
|
||||||
function log(message, color = "") {
|
function log(message: string, color = "") {
|
||||||
if (useColor && color) {
|
if (useColor && color) {
|
||||||
console.log(`${color}${message}${colors.reset}`);
|
console.log(`${color}${message}${colors.reset}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -40,7 +43,7 @@ function log(message, color = "") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatError(errorLine) {
|
function formatError(errorLine: string): string {
|
||||||
if (!useColor) return errorLine;
|
if (!useColor) return errorLine;
|
||||||
|
|
||||||
// Color file paths in cyan
|
// Color file paths in cyan
|
||||||
@@ -70,12 +73,15 @@ function formatError(errorLine) {
|
|||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseErrorsAndCreateSummary(errorOutput) {
|
function parseErrorsAndCreateSummary(errorOutput: string): {
|
||||||
|
formattedErrors: string[];
|
||||||
|
errorsByFile: Map<string, number>;
|
||||||
|
} {
|
||||||
const lines = errorOutput.split("\n").filter((line) => line.trim());
|
const lines = errorOutput.split("\n").filter((line) => line.trim());
|
||||||
const errorsByFile = new Map();
|
const errorsByFile = new Map<string, number>();
|
||||||
const formattedErrors = [];
|
const formattedErrors: string[] = [];
|
||||||
|
|
||||||
let currentError = [];
|
let currentError: string[] = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmedLine = line.trim();
|
const trimmedLine = line.trim();
|
||||||
@@ -96,7 +102,7 @@ function parseErrorsAndCreateSummary(errorOutput) {
|
|||||||
if (!errorsByFile.has(filePath)) {
|
if (!errorsByFile.has(filePath)) {
|
||||||
errorsByFile.set(filePath, 0);
|
errorsByFile.set(filePath, 0);
|
||||||
}
|
}
|
||||||
errorsByFile.set(filePath, errorsByFile.get(filePath) + 1);
|
errorsByFile.set(filePath, (errorsByFile.get(filePath) ?? 0) + 1);
|
||||||
|
|
||||||
// Start new error
|
// Start new error
|
||||||
currentError.push(formatError(line));
|
currentError.push(formatError(line));
|
||||||
@@ -119,7 +125,7 @@ function parseErrorsAndCreateSummary(errorOutput) {
|
|||||||
return { formattedErrors, errorsByFile };
|
return { formattedErrors, errorsByFile };
|
||||||
}
|
}
|
||||||
|
|
||||||
function createErrorSummaryTable(errorsByFile) {
|
function createErrorSummaryTable(errorsByFile: Map<string, number>): string {
|
||||||
if (errorsByFile.size === 0) return "";
|
if (errorsByFile.size === 0) return "";
|
||||||
|
|
||||||
const sortedFiles = Array.from(errorsByFile.entries()).sort(
|
const sortedFiles = Array.from(errorsByFile.entries()).sort(
|
||||||
@@ -136,12 +142,12 @@ function createErrorSummaryTable(errorsByFile) {
|
|||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runTypeCheck() {
|
function runTypeCheck(): { ok: boolean } {
|
||||||
const extraArgs = process.argv.slice(2);
|
const extraArgs = process.argv.slice(2);
|
||||||
|
|
||||||
// Prefer local TypeScript binary when available
|
// Prefer local TypeScript binary when available
|
||||||
const runnerArgs = ["-p", "tsconfig.json", "--noEmit", ...extraArgs];
|
const runnerArgs = ["-p", "tsconfig.json", "--noEmit", ...extraArgs];
|
||||||
let execArgs = null;
|
let execArgs: { cmd: string; args: string[] };
|
||||||
try {
|
try {
|
||||||
const tscBin = require.resolve("typescript/bin/tsc");
|
const tscBin = require.resolve("typescript/bin/tsc");
|
||||||
execArgs = { cmd: process.execPath, args: [tscBin, ...runnerArgs] };
|
execArgs = { cmd: process.execPath, args: [tscBin, ...runnerArgs] };
|
||||||
@@ -174,7 +180,8 @@ function runTypeCheck() {
|
|||||||
);
|
);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorOutput = (error && (error.stderr || error.stdout)) || "";
|
const execError = error as { stderr?: string; stdout?: string };
|
||||||
|
const errorOutput = execError.stderr || execError.stdout || "";
|
||||||
|
|
||||||
// Filter out jellyseerr utils errors - this is a third-party git submodule
|
// Filter out jellyseerr utils errors - this is a third-party git submodule
|
||||||
// that generates a large volume of known type errors
|
// that generates a large volume of known type errors
|
||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "التنزيلات",
|
"downloads_title": "التنزيلات",
|
||||||
"tvseries": "مسلسلات",
|
"series": "مسلسلات",
|
||||||
"movies": "أفلام",
|
"movies": "أفلام",
|
||||||
"queue": "قائمة الانتظار",
|
"queue": "قائمة الانتظار",
|
||||||
"other_media": "وسائط أخرى",
|
"other_media": "وسائط أخرى",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "لا توجد عناصر في قائمة الانتظار",
|
"no_items_in_queue": "لا توجد عناصر في قائمة الانتظار",
|
||||||
"no_downloaded_items": "لا توجد عناصر تم تنزيلها",
|
"no_downloaded_items": "لا توجد عناصر تم تنزيلها",
|
||||||
"delete_all_movies_button": "حذف جميع الأفلام",
|
"delete_all_movies_button": "حذف جميع الأفلام",
|
||||||
"delete_all_tvseries_button": "حذف جميع المسلسلات",
|
"delete_all_series_button": "حذف جميع المسلسلات",
|
||||||
"delete_all_button": "حذف الكل",
|
"delete_all_button": "حذف الكل",
|
||||||
"delete_all_other_media_button": "حذف الوسائط الأخرى",
|
"delete_all_other_media_button": "حذف الوسائط الأخرى",
|
||||||
"active_download": "التنزيل الجاري",
|
"active_download": "التنزيل الجاري",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "غير مسموح لك بتنزيل الملفات.",
|
"you_are_not_allowed_to_download_files": "غير مسموح لك بتنزيل الملفات.",
|
||||||
"deleted_all_movies_successfully": "تم حذف جميع الأفلام بنجاح!",
|
"deleted_all_movies_successfully": "تم حذف جميع الأفلام بنجاح!",
|
||||||
"failed_to_delete_all_movies": "فشل حذف جميع الأفلام",
|
"failed_to_delete_all_movies": "فشل حذف جميع الأفلام",
|
||||||
"deleted_all_tvseries_successfully": "تم حذف جميع المسلسلات بنجاح!",
|
"deleted_all_series_successfully": "تم حذف جميع المسلسلات بنجاح!",
|
||||||
"failed_to_delete_all_tvseries": "فشل حذف جميع المسلسلات",
|
"failed_to_delete_all_series": "فشل حذف جميع المسلسلات",
|
||||||
"deleted_media_successfully": "تم حذف الوسائط الأخرى بنجاح!",
|
"deleted_media_successfully": "تم حذف الوسائط الأخرى بنجاح!",
|
||||||
"failed_to_delete_media": "فشل حذف الوسائط الأخرى",
|
"failed_to_delete_media": "فشل حذف الوسائط الأخرى",
|
||||||
"download_deleted": "تم حذف التنزيل",
|
"download_deleted": "تم حذف التنزيل",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Descàrregues",
|
"downloads_title": "Descàrregues",
|
||||||
"tvseries": "Sèries",
|
"series": "Sèries",
|
||||||
"movies": "Pel·lícules",
|
"movies": "Pel·lícules",
|
||||||
"queue": "Cua",
|
"queue": "Cua",
|
||||||
"other_media": "Other media",
|
"other_media": "Other media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "No hi ha elements a la cua",
|
"no_items_in_queue": "No hi ha elements a la cua",
|
||||||
"no_downloaded_items": "No hi ha elements descarregats",
|
"no_downloaded_items": "No hi ha elements descarregats",
|
||||||
"delete_all_movies_button": "Suprimeix totes les pel·lícules",
|
"delete_all_movies_button": "Suprimeix totes les pel·lícules",
|
||||||
"delete_all_tvseries_button": "Suprimeix totes les sèries",
|
"delete_all_series_button": "Suprimeix totes les sèries",
|
||||||
"delete_all_button": "Suprimeix-ho tot",
|
"delete_all_button": "Suprimeix-ho tot",
|
||||||
"delete_all_other_media_button": "Delete other media",
|
"delete_all_other_media_button": "Delete other media",
|
||||||
"active_download": "Descàrrega activa",
|
"active_download": "Descàrrega activa",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "No teniu permís per descarregar fitxers.",
|
"you_are_not_allowed_to_download_files": "No teniu permís per descarregar fitxers.",
|
||||||
"deleted_all_movies_successfully": "S'han suprimit totes les pel·lícules correctament!",
|
"deleted_all_movies_successfully": "S'han suprimit totes les pel·lícules correctament!",
|
||||||
"failed_to_delete_all_movies": "No s'han pogut suprimir totes les pel·lícules",
|
"failed_to_delete_all_movies": "No s'han pogut suprimir totes les pel·lícules",
|
||||||
"deleted_all_tvseries_successfully": "S'han suprimit totes les sèries correctament!",
|
"deleted_all_series_successfully": "S'han suprimit totes les sèries correctament!",
|
||||||
"failed_to_delete_all_tvseries": "No s'han pogut suprimir totes les sèries",
|
"failed_to_delete_all_series": "No s'han pogut suprimir totes les sèries",
|
||||||
"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_deleted": "Download Deleted",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Stahování",
|
"downloads_title": "Stahování",
|
||||||
"tvseries": "Televizní série",
|
"series": "Televizní série",
|
||||||
"movies": "Filmy",
|
"movies": "Filmy",
|
||||||
"queue": "Fronta",
|
"queue": "Fronta",
|
||||||
"other_media": "Ostatní média",
|
"other_media": "Ostatní média",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Žádné položky ve frontě",
|
"no_items_in_queue": "Žádné položky ve frontě",
|
||||||
"no_downloaded_items": "Žádné stažené položky",
|
"no_downloaded_items": "Žádné stažené položky",
|
||||||
"delete_all_movies_button": "Odstranit všechny filmy",
|
"delete_all_movies_button": "Odstranit všechny filmy",
|
||||||
"delete_all_tvseries_button": "Odstranit všechny TV-série",
|
"delete_all_series_button": "Odstranit všechny TV-série",
|
||||||
"delete_all_button": "Smazat vše",
|
"delete_all_button": "Smazat vše",
|
||||||
"delete_all_other_media_button": "Odstranit ostatní média",
|
"delete_all_other_media_button": "Odstranit ostatní média",
|
||||||
"active_download": "Aktivní stahování",
|
"active_download": "Aktivní stahování",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Nemáte oprávnění stahovat soubory.",
|
"you_are_not_allowed_to_download_files": "Nemáte oprávnění stahovat soubory.",
|
||||||
"deleted_all_movies_successfully": "Všechny filmy byly úspěšně smazány!",
|
"deleted_all_movies_successfully": "Všechny filmy byly úspěšně smazány!",
|
||||||
"failed_to_delete_all_movies": "Nepodařilo se odstranit všechny filmy",
|
"failed_to_delete_all_movies": "Nepodařilo se odstranit všechny filmy",
|
||||||
"deleted_all_tvseries_successfully": "Všechny série televizorů byly úspěšně smazány!",
|
"deleted_all_series_successfully": "Všechny série televizorů byly úspěšně smazány!",
|
||||||
"failed_to_delete_all_tvseries": "Nepodařilo se odstranit všechny TV-série",
|
"failed_to_delete_all_series": "Nepodařilo se odstranit všechny TV-série",
|
||||||
"deleted_media_successfully": "Ostatní média úspěšně smazána!",
|
"deleted_media_successfully": "Ostatní média úspěšně smazána!",
|
||||||
"failed_to_delete_media": "Nepodařilo se odstranit ostatní média",
|
"failed_to_delete_media": "Nepodařilo se odstranit ostatní média",
|
||||||
"download_deleted": "Stahování smazáno",
|
"download_deleted": "Stahování smazáno",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"tvseries": "TV-serier",
|
"series": "TV-serier",
|
||||||
"movies": "Film",
|
"movies": "Film",
|
||||||
"queue": "Kø",
|
"queue": "Kø",
|
||||||
"other_media": "Andre medier",
|
"other_media": "Andre medier",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Ingen elementer i køen",
|
"no_items_in_queue": "Ingen elementer i køen",
|
||||||
"no_downloaded_items": "Ingen downloadede elementer",
|
"no_downloaded_items": "Ingen downloadede elementer",
|
||||||
"delete_all_movies_button": "Slet alle film",
|
"delete_all_movies_button": "Slet alle film",
|
||||||
"delete_all_tvseries_button": "Slet alle TV-serier",
|
"delete_all_series_button": "Slet alle TV-serier",
|
||||||
"delete_all_button": "Slet alle",
|
"delete_all_button": "Slet alle",
|
||||||
"delete_all_other_media_button": "Slet andre medier",
|
"delete_all_other_media_button": "Slet andre medier",
|
||||||
"active_download": "Aktiv download",
|
"active_download": "Aktiv download",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Du har ikke tilladelse til at downloade filer.",
|
"you_are_not_allowed_to_download_files": "Du har ikke tilladelse til at downloade filer.",
|
||||||
"deleted_all_movies_successfully": "Alle film er slettet med succes!",
|
"deleted_all_movies_successfully": "Alle film er slettet med succes!",
|
||||||
"failed_to_delete_all_movies": "Kunne ikke slette alle film",
|
"failed_to_delete_all_movies": "Kunne ikke slette alle film",
|
||||||
"deleted_all_tvseries_successfully": "Alle TV-serier er slettet med succes!",
|
"deleted_all_series_successfully": "Alle TV-serier er slettet med succes!",
|
||||||
"failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-serier",
|
"failed_to_delete_all_series": "Kunne ikke slette alle TV-serier",
|
||||||
"deleted_media_successfully": "Slettede andre medier med succes!",
|
"deleted_media_successfully": "Slettede andre medier med succes!",
|
||||||
"failed_to_delete_media": "Kunne ikke slette andre medier",
|
"failed_to_delete_media": "Kunne ikke slette andre medier",
|
||||||
"download_deleted": "Download Slettet",
|
"download_deleted": "Download Slettet",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"tvseries": "Serien",
|
"series": "Serien",
|
||||||
"movies": "Filme",
|
"movies": "Filme",
|
||||||
"queue": "Warteschlange",
|
"queue": "Warteschlange",
|
||||||
"other_media": "Andere Medien",
|
"other_media": "Andere Medien",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Keine Elemente in der Warteschlange",
|
"no_items_in_queue": "Keine Elemente in der Warteschlange",
|
||||||
"no_downloaded_items": "Keine heruntergeladenen Elemente",
|
"no_downloaded_items": "Keine heruntergeladenen Elemente",
|
||||||
"delete_all_movies_button": "Alle Filme löschen",
|
"delete_all_movies_button": "Alle Filme löschen",
|
||||||
"delete_all_tvseries_button": "Alle Serien löschen",
|
"delete_all_series_button": "Alle Serien löschen",
|
||||||
"delete_all_button": "Alles löschen",
|
"delete_all_button": "Alles löschen",
|
||||||
"delete_all_other_media_button": "Alle anderen Medien löschen",
|
"delete_all_other_media_button": "Alle anderen Medien löschen",
|
||||||
"active_download": "Aktiver Download",
|
"active_download": "Aktiver Download",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen",
|
"you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen",
|
||||||
"deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!",
|
"deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!",
|
||||||
"failed_to_delete_all_movies": "Fehler beim Löschen aller Filme",
|
"failed_to_delete_all_movies": "Fehler beim Löschen aller Filme",
|
||||||
"deleted_all_tvseries_successfully": "Alle Serien erfolgreich gelöscht!",
|
"deleted_all_series_successfully": "Alle Serien erfolgreich gelöscht!",
|
||||||
"failed_to_delete_all_tvseries": "Fehler beim Löschen aller Serien",
|
"failed_to_delete_all_series": "Fehler beim Löschen aller Serien",
|
||||||
"deleted_media_successfully": "Andere Medien erfolgreich gelöscht!",
|
"deleted_media_successfully": "Andere Medien erfolgreich gelöscht!",
|
||||||
"failed_to_delete_media": "Fehler beim Löschen anderer Medien",
|
"failed_to_delete_media": "Fehler beim Löschen anderer Medien",
|
||||||
"download_deleted": "Download gelöscht",
|
"download_deleted": "Download gelöscht",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Λήψεις",
|
"downloads_title": "Λήψεις",
|
||||||
"tvseries": "Τηλεόραση-Σειρά",
|
"series": "Τηλεόραση-Σειρά",
|
||||||
"movies": "Ταινίες",
|
"movies": "Ταινίες",
|
||||||
"queue": "Ουρά",
|
"queue": "Ουρά",
|
||||||
"other_media": "Άλλα μέσα",
|
"other_media": "Άλλα μέσα",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Δεν υπάρχουν αντικείμενα στην ουρά",
|
"no_items_in_queue": "Δεν υπάρχουν αντικείμενα στην ουρά",
|
||||||
"no_downloaded_items": "Δεν Έχουν Ληφθεί Αντικείμενα",
|
"no_downloaded_items": "Δεν Έχουν Ληφθεί Αντικείμενα",
|
||||||
"delete_all_movies_button": "Διαγραφή Όλων Των Ταινιών",
|
"delete_all_movies_button": "Διαγραφή Όλων Των Ταινιών",
|
||||||
"delete_all_tvseries_button": "Διαγραφή Όλων Των Τηλεοπτικών Σειρών",
|
"delete_all_series_button": "Διαγραφή Όλων Των Τηλεοπτικών Σειρών",
|
||||||
"delete_all_button": "Διαγραφή Όλων",
|
"delete_all_button": "Διαγραφή Όλων",
|
||||||
"delete_all_other_media_button": "Διαγραφή άλλων μέσων",
|
"delete_all_other_media_button": "Διαγραφή άλλων μέσων",
|
||||||
"active_download": "Ενεργή Λήψη",
|
"active_download": "Ενεργή Λήψη",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Δεν επιτρέπεται να κατεβάσετε αρχεία.",
|
"you_are_not_allowed_to_download_files": "Δεν επιτρέπεται να κατεβάσετε αρχεία.",
|
||||||
"deleted_all_movies_successfully": "Διαγράφηκε Όλες Οι Ταινίες Επιτυχία!",
|
"deleted_all_movies_successfully": "Διαγράφηκε Όλες Οι Ταινίες Επιτυχία!",
|
||||||
"failed_to_delete_all_movies": "Αποτυχία διαγραφής όλων των ταινιών",
|
"failed_to_delete_all_movies": "Αποτυχία διαγραφής όλων των ταινιών",
|
||||||
"deleted_all_tvseries_successfully": "Διαγράφηκε Όλη Η Τηλεόραση-Σειρά Επιτυχία!",
|
"deleted_all_series_successfully": "Διαγράφηκε Όλη Η Τηλεόραση-Σειρά Επιτυχία!",
|
||||||
"failed_to_delete_all_tvseries": "Αποτυχία διαγραφής Όλων των TV-Series",
|
"failed_to_delete_all_series": "Αποτυχία διαγραφής Όλων των TV-Series",
|
||||||
"deleted_media_successfully": "Διαγράφηκε άλλο μέσο επιτυχώς!",
|
"deleted_media_successfully": "Διαγράφηκε άλλο μέσο επιτυχώς!",
|
||||||
"failed_to_delete_media": "Αποτυχία διαγραφής άλλων πολυμέσων",
|
"failed_to_delete_media": "Αποτυχία διαγραφής άλλων πολυμέσων",
|
||||||
"download_deleted": "Η Λήψη Διαγράφηκε",
|
"download_deleted": "Η Λήψη Διαγράφηκε",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -229,14 +229,14 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Elŝutoj",
|
"downloads_title": "Elŝutoj",
|
||||||
"tvseries": "Televidaj serioj",
|
"series": "Televidaj serioj",
|
||||||
"movies": "Filmoj",
|
"movies": "Filmoj",
|
||||||
"queue": "Vico",
|
"queue": "Vico",
|
||||||
"queue_hint": "Vico kaj elŝutoj perdiĝos ĉe aplikaĵa rekomenco",
|
"queue_hint": "Vico kaj elŝutoj perdiĝos ĉe aplikaĵa rekomenco",
|
||||||
"no_items_in_queue": "Neniuj eroj en vico",
|
"no_items_in_queue": "Neniuj eroj en vico",
|
||||||
"no_downloaded_items": "Neniuj elŝutitaj eroj",
|
"no_downloaded_items": "Neniuj elŝutitaj eroj",
|
||||||
"delete_all_movies_button": "Forigi ĉiujn Filmojn",
|
"delete_all_movies_button": "Forigi ĉiujn Filmojn",
|
||||||
"delete_all_tvseries_button": "Forigi ĉiujn Televidajn Seriojn",
|
"delete_all_series_button": "Forigi ĉiujn Televidajn Seriojn",
|
||||||
"delete_all_button": "Forigi ĉion",
|
"delete_all_button": "Forigi ĉion",
|
||||||
"active_download": "Aktiva elŝuto",
|
"active_download": "Aktiva elŝuto",
|
||||||
"no_active_downloads": "Neniuj aktivaj elŝutoj",
|
"no_active_downloads": "Neniuj aktivaj elŝutoj",
|
||||||
@@ -253,8 +253,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Vi ne rajtas elŝuti dosierojn.",
|
"you_are_not_allowed_to_download_files": "Vi ne rajtas elŝuti dosierojn.",
|
||||||
"deleted_all_movies_successfully": "Sukcese forigis ĉiujn filmojn!",
|
"deleted_all_movies_successfully": "Sukcese forigis ĉiujn filmojn!",
|
||||||
"failed_to_delete_all_movies": "Malsukcesis forigi ĉiujn filmojn",
|
"failed_to_delete_all_movies": "Malsukcesis forigi ĉiujn filmojn",
|
||||||
"deleted_all_tvseries_successfully": "Sukcese forigis ĉiujn Televidajn Seriojn!",
|
"deleted_all_series_successfully": "Sukcese forigis ĉiujn Televidajn Seriojn!",
|
||||||
"failed_to_delete_all_tvseries": "Malsukcesis forigi ĉiujn Televidajn Seriojn",
|
"failed_to_delete_all_series": "Malsukcesis forigi ĉiujn Televidajn Seriojn",
|
||||||
"download_cancelled": "Elŝuto nuligita",
|
"download_cancelled": "Elŝuto nuligita",
|
||||||
"could_not_cancel_download": "Ne povis nuligi elŝuton",
|
"could_not_cancel_download": "Ne povis nuligi elŝuton",
|
||||||
"download_completed": "Elŝuto finita",
|
"download_completed": "Elŝuto finita",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Descargas",
|
"downloads_title": "Descargas",
|
||||||
"tvseries": "Series",
|
"series": "Series",
|
||||||
"movies": "Películas",
|
"movies": "Películas",
|
||||||
"queue": "Cola",
|
"queue": "Cola",
|
||||||
"other_media": "Otros medios",
|
"other_media": "Otros medios",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "No hay ítems en la cola",
|
"no_items_in_queue": "No hay ítems en la cola",
|
||||||
"no_downloaded_items": "No hay ítems descargados",
|
"no_downloaded_items": "No hay ítems descargados",
|
||||||
"delete_all_movies_button": "Eliminar todas las películas",
|
"delete_all_movies_button": "Eliminar todas las películas",
|
||||||
"delete_all_tvseries_button": "Eliminar todas las series",
|
"delete_all_series_button": "Eliminar todas las series",
|
||||||
"delete_all_button": "Eliminar todo",
|
"delete_all_button": "Eliminar todo",
|
||||||
"delete_all_other_media_button": "Eliminar otros medios",
|
"delete_all_other_media_button": "Eliminar otros medios",
|
||||||
"active_download": "Descarga activa",
|
"active_download": "Descarga activa",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "No tienes permiso para descargar archivos.",
|
"you_are_not_allowed_to_download_files": "No tienes permiso para descargar archivos.",
|
||||||
"deleted_all_movies_successfully": "¡Todas las películas eliminadas con éxito!",
|
"deleted_all_movies_successfully": "¡Todas las películas eliminadas con éxito!",
|
||||||
"failed_to_delete_all_movies": "Error al eliminar todas las películas",
|
"failed_to_delete_all_movies": "Error al eliminar todas las películas",
|
||||||
"deleted_all_tvseries_successfully": "¡Todas las series eliminadas con éxito!",
|
"deleted_all_series_successfully": "¡Todas las series eliminadas con éxito!",
|
||||||
"failed_to_delete_all_tvseries": "Error al eliminar todas las series",
|
"failed_to_delete_all_series": "Error al eliminar todas las series",
|
||||||
"deleted_media_successfully": "¡Otros medios eliminados con éxito!",
|
"deleted_media_successfully": "¡Otros medios eliminados con éxito!",
|
||||||
"failed_to_delete_media": "Error al eliminar otros medios",
|
"failed_to_delete_media": "Error al eliminar otros medios",
|
||||||
"download_deleted": "Descarga eliminada",
|
"download_deleted": "Descarga eliminada",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Lataukset",
|
"downloads_title": "Lataukset",
|
||||||
"tvseries": "TV-sarjat",
|
"series": "TV-sarjat",
|
||||||
"movies": "Elokuvat",
|
"movies": "Elokuvat",
|
||||||
"queue": "Jonot",
|
"queue": "Jonot",
|
||||||
"other_media": "Muu media",
|
"other_media": "Muu media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Ei kohteita jonossa",
|
"no_items_in_queue": "Ei kohteita jonossa",
|
||||||
"no_downloaded_items": "Ei ladattuja kohteita",
|
"no_downloaded_items": "Ei ladattuja kohteita",
|
||||||
"delete_all_movies_button": "Poista kaikki elokuvat",
|
"delete_all_movies_button": "Poista kaikki elokuvat",
|
||||||
"delete_all_tvseries_button": "Poista kaikki TV-sarjat",
|
"delete_all_series_button": "Poista kaikki TV-sarjat",
|
||||||
"delete_all_button": "Poista kaikki",
|
"delete_all_button": "Poista kaikki",
|
||||||
"delete_all_other_media_button": "Poista muu media",
|
"delete_all_other_media_button": "Poista muu media",
|
||||||
"active_download": "Aktiivinen lataus",
|
"active_download": "Aktiivinen lataus",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Sinulla ei ole lupaa ladata tiedostoja.",
|
"you_are_not_allowed_to_download_files": "Sinulla ei ole lupaa ladata tiedostoja.",
|
||||||
"deleted_all_movies_successfully": "Kaikki elokuvat poistettu onnistuneesti!",
|
"deleted_all_movies_successfully": "Kaikki elokuvat poistettu onnistuneesti!",
|
||||||
"failed_to_delete_all_movies": "Kaikkien elokuvien poistaminen epäonnistui",
|
"failed_to_delete_all_movies": "Kaikkien elokuvien poistaminen epäonnistui",
|
||||||
"deleted_all_tvseries_successfully": "Kaikki TV-sarjat poistettu onnistuneesti!",
|
"deleted_all_series_successfully": "Kaikki TV-sarjat poistettu onnistuneesti!",
|
||||||
"failed_to_delete_all_tvseries": "Kaikkien TV-sarjojen poistaminen epäonnistui",
|
"failed_to_delete_all_series": "Kaikkien TV-sarjojen poistaminen epäonnistui",
|
||||||
"deleted_media_successfully": "Muu media poistettu onnistuneesti!",
|
"deleted_media_successfully": "Muu media poistettu onnistuneesti!",
|
||||||
"failed_to_delete_media": "Muiden medioiden poistaminen epäonnistui",
|
"failed_to_delete_media": "Muiden medioiden poistaminen epäonnistui",
|
||||||
"download_deleted": "Lataus Poistettu",
|
"download_deleted": "Lataus Poistettu",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Téléchargements",
|
"downloads_title": "Téléchargements",
|
||||||
"tvseries": "Séries",
|
"series": "Séries",
|
||||||
"movies": "Films",
|
"movies": "Films",
|
||||||
"queue": "File d'attente",
|
"queue": "File d'attente",
|
||||||
"other_media": "Autres médias",
|
"other_media": "Autres médias",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Aucun téléchargement de média dans la file d'attente",
|
"no_items_in_queue": "Aucun téléchargement de média dans la file d'attente",
|
||||||
"no_downloaded_items": "Aucun média téléchargé",
|
"no_downloaded_items": "Aucun média téléchargé",
|
||||||
"delete_all_movies_button": "Supprimer tous les films",
|
"delete_all_movies_button": "Supprimer tous les films",
|
||||||
"delete_all_tvseries_button": "Supprimer toutes les séries",
|
"delete_all_series_button": "Supprimer toutes les séries",
|
||||||
"delete_all_button": "Supprimer tous les médias",
|
"delete_all_button": "Supprimer tous les médias",
|
||||||
"delete_all_other_media_button": "Supprimer un autre média",
|
"delete_all_other_media_button": "Supprimer un autre média",
|
||||||
"active_download": "Téléchargement actif",
|
"active_download": "Téléchargement actif",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Vous n'êtes pas autorisé à télécharger des fichiers.",
|
"you_are_not_allowed_to_download_files": "Vous n'êtes pas autorisé à télécharger des fichiers.",
|
||||||
"deleted_all_movies_successfully": "Tous les films ont été supprimés avec succès !",
|
"deleted_all_movies_successfully": "Tous les films ont été supprimés avec succès !",
|
||||||
"failed_to_delete_all_movies": "Échec de la suppression de tous les films",
|
"failed_to_delete_all_movies": "Échec de la suppression de tous les films",
|
||||||
"deleted_all_tvseries_successfully": "Toutes les séries ont été supprimées avec succès !",
|
"deleted_all_series_successfully": "Toutes les séries ont été supprimées avec succès !",
|
||||||
"failed_to_delete_all_tvseries": "Échec de la suppression de toutes les séries",
|
"failed_to_delete_all_series": "Échec de la suppression de toutes les séries",
|
||||||
"deleted_media_successfully": "Les autres médias ont été supprimés avec succès !",
|
"deleted_media_successfully": "Les autres médias ont été supprimés avec succès !",
|
||||||
"failed_to_delete_media": "Échec de la suppression d'un autre média",
|
"failed_to_delete_media": "Échec de la suppression d'un autre média",
|
||||||
"download_deleted": "Téléchargement supprimé",
|
"download_deleted": "Téléchargement supprimé",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "הורדות",
|
"downloads_title": "הורדות",
|
||||||
"tvseries": "סדרות",
|
"series": "סדרות",
|
||||||
"movies": "סרטים",
|
"movies": "סרטים",
|
||||||
"queue": "תוֹר",
|
"queue": "תוֹר",
|
||||||
"other_media": "תוכן אחר",
|
"other_media": "תוכן אחר",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "אין פרטים בתור",
|
"no_items_in_queue": "אין פרטים בתור",
|
||||||
"no_downloaded_items": "אין פריטים שהורדו",
|
"no_downloaded_items": "אין פריטים שהורדו",
|
||||||
"delete_all_movies_button": "מחק את כל הסרטים",
|
"delete_all_movies_button": "מחק את כל הסרטים",
|
||||||
"delete_all_tvseries_button": "מחק את כל הסדרות",
|
"delete_all_series_button": "מחק את כל הסדרות",
|
||||||
"delete_all_button": "מחק הכל",
|
"delete_all_button": "מחק הכל",
|
||||||
"delete_all_other_media_button": "מחק שאר תוכן",
|
"delete_all_other_media_button": "מחק שאר תוכן",
|
||||||
"active_download": "הורדה פעילה",
|
"active_download": "הורדה פעילה",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "אתה לא מורשה להוריד קבצים.",
|
"you_are_not_allowed_to_download_files": "אתה לא מורשה להוריד קבצים.",
|
||||||
"deleted_all_movies_successfully": "כל הסרטים נמחקו בהצלחה!",
|
"deleted_all_movies_successfully": "כל הסרטים נמחקו בהצלחה!",
|
||||||
"failed_to_delete_all_movies": "נכשל במחיקת כל הסרטים",
|
"failed_to_delete_all_movies": "נכשל במחיקת כל הסרטים",
|
||||||
"deleted_all_tvseries_successfully": "כל הסדרות נמחקו בהצלחה!",
|
"deleted_all_series_successfully": "כל הסדרות נמחקו בהצלחה!",
|
||||||
"failed_to_delete_all_tvseries": "נכשל במחיקת כל הסדרות",
|
"failed_to_delete_all_series": "נכשל במחיקת כל הסדרות",
|
||||||
"deleted_media_successfully": "כל שאר התוכן נמחק בהצלחה!",
|
"deleted_media_successfully": "כל שאר התוכן נמחק בהצלחה!",
|
||||||
"failed_to_delete_media": "נכשל במחיקת שאר התוכן",
|
"failed_to_delete_media": "נכשל במחיקת שאר התוכן",
|
||||||
"download_deleted": "ההורדה נמחקה",
|
"download_deleted": "ההורדה נמחקה",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Letöltések",
|
"downloads_title": "Letöltések",
|
||||||
"tvseries": "Sorozatok",
|
"series": "Sorozatok",
|
||||||
"movies": "Filmek",
|
"movies": "Filmek",
|
||||||
"queue": "Sor",
|
"queue": "Sor",
|
||||||
"other_media": "Other media",
|
"other_media": "Other media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Nincs Elem a Sorban",
|
"no_items_in_queue": "Nincs Elem a Sorban",
|
||||||
"no_downloaded_items": "Nincsenek Letöltött Elemek",
|
"no_downloaded_items": "Nincsenek Letöltött Elemek",
|
||||||
"delete_all_movies_button": "Összes Film Törlése",
|
"delete_all_movies_button": "Összes Film Törlése",
|
||||||
"delete_all_tvseries_button": "Összes Sorozat Törlése",
|
"delete_all_series_button": "Összes Sorozat Törlése",
|
||||||
"delete_all_button": "Összes Törlése",
|
"delete_all_button": "Összes Törlése",
|
||||||
"delete_all_other_media_button": "Delete other media",
|
"delete_all_other_media_button": "Delete other media",
|
||||||
"active_download": "Aktív Letöltés",
|
"active_download": "Aktív Letöltés",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Nem engedélyezett a fájlok letöltése.",
|
"you_are_not_allowed_to_download_files": "Nem engedélyezett a fájlok letöltése.",
|
||||||
"deleted_all_movies_successfully": "Az Összes Film Sikeresen Törölve!",
|
"deleted_all_movies_successfully": "Az Összes Film Sikeresen Törölve!",
|
||||||
"failed_to_delete_all_movies": "Nem Sikerült Törölni Az Összes Filmet",
|
"failed_to_delete_all_movies": "Nem Sikerült Törölni Az Összes Filmet",
|
||||||
"deleted_all_tvseries_successfully": "Az Összes Sorozat Sikeresen Törölve!",
|
"deleted_all_series_successfully": "Az Összes Sorozat Sikeresen Törölve!",
|
||||||
"failed_to_delete_all_tvseries": "Nem Sikerült Törölni Az Összes Sorozatot",
|
"failed_to_delete_all_series": "Nem Sikerült Törölni Az Összes Sorozatot",
|
||||||
"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": "Letöltés Törölve",
|
"download_deleted": "Letöltés Törölve",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Scaricati",
|
"downloads_title": "Scaricati",
|
||||||
"tvseries": "Serie TV",
|
"series": "Serie TV",
|
||||||
"movies": "Film",
|
"movies": "Film",
|
||||||
"queue": "Coda",
|
"queue": "Coda",
|
||||||
"other_media": "Altri supporti",
|
"other_media": "Altri supporti",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Nessun elemento in coda",
|
"no_items_in_queue": "Nessun elemento in coda",
|
||||||
"no_downloaded_items": "Nessun elemento scaricato",
|
"no_downloaded_items": "Nessun elemento scaricato",
|
||||||
"delete_all_movies_button": "Cancella tutti i film",
|
"delete_all_movies_button": "Cancella tutti i film",
|
||||||
"delete_all_tvseries_button": "Cancella tutte le serie TV",
|
"delete_all_series_button": "Cancella tutte le serie TV",
|
||||||
"delete_all_button": "Cancella tutti",
|
"delete_all_button": "Cancella tutti",
|
||||||
"delete_all_other_media_button": "Elimina altri supporti",
|
"delete_all_other_media_button": "Elimina altri supporti",
|
||||||
"active_download": "Scaricamento in corso",
|
"active_download": "Scaricamento in corso",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Non è consentito scaricare file.",
|
"you_are_not_allowed_to_download_files": "Non è consentito scaricare file.",
|
||||||
"deleted_all_movies_successfully": "Cancellati tutti i film con successo!",
|
"deleted_all_movies_successfully": "Cancellati tutti i film con successo!",
|
||||||
"failed_to_delete_all_movies": "Impossibile eliminare tutti i film",
|
"failed_to_delete_all_movies": "Impossibile eliminare tutti i film",
|
||||||
"deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!",
|
"deleted_all_series_successfully": "Eliminate tutte le serie TV con successo!",
|
||||||
"failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV",
|
"failed_to_delete_all_series": "Impossibile eliminare tutte le serie TV",
|
||||||
"deleted_media_successfully": "Eliminato altri supporti con successo!",
|
"deleted_media_successfully": "Eliminato altri supporti con successo!",
|
||||||
"failed_to_delete_media": "Impossibile eliminare altri media",
|
"failed_to_delete_media": "Impossibile eliminare altri media",
|
||||||
"download_deleted": "Download Eliminato",
|
"download_deleted": "Download Eliminato",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "ダウンロード",
|
"downloads_title": "ダウンロード",
|
||||||
"tvseries": "TVシリーズ",
|
"series": "TVシリーズ",
|
||||||
"movies": "映画",
|
"movies": "映画",
|
||||||
"queue": "キュー",
|
"queue": "キュー",
|
||||||
"other_media": "その他のメディア",
|
"other_media": "その他のメディア",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "キューにアイテムがありません",
|
"no_items_in_queue": "キューにアイテムがありません",
|
||||||
"no_downloaded_items": "ダウンロードしたアイテムはありません",
|
"no_downloaded_items": "ダウンロードしたアイテムはありません",
|
||||||
"delete_all_movies_button": "すべての映画を削除",
|
"delete_all_movies_button": "すべての映画を削除",
|
||||||
"delete_all_tvseries_button": "すべてのシリーズを削除",
|
"delete_all_series_button": "すべてのシリーズを削除",
|
||||||
"delete_all_button": "すべて削除",
|
"delete_all_button": "すべて削除",
|
||||||
"delete_all_other_media_button": "他のメディアを削除する",
|
"delete_all_other_media_button": "他のメディアを削除する",
|
||||||
"active_download": "アクティブなダウンロード",
|
"active_download": "アクティブなダウンロード",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "ファイルをダウンロードする権限がありません。",
|
"you_are_not_allowed_to_download_files": "ファイルをダウンロードする権限がありません。",
|
||||||
"deleted_all_movies_successfully": "すべての映画を正常に削除しました!",
|
"deleted_all_movies_successfully": "すべての映画を正常に削除しました!",
|
||||||
"failed_to_delete_all_movies": "すべての映画を削除できませんでした",
|
"failed_to_delete_all_movies": "すべての映画を削除できませんでした",
|
||||||
"deleted_all_tvseries_successfully": "すべてのシリーズを正常に削除しました!",
|
"deleted_all_series_successfully": "すべてのシリーズを正常に削除しました!",
|
||||||
"failed_to_delete_all_tvseries": "すべてのシリーズを削除できませんでした",
|
"failed_to_delete_all_series": "すべてのシリーズを削除できませんでした",
|
||||||
"deleted_media_successfully": "他のメディアを削除しました!",
|
"deleted_media_successfully": "他のメディアを削除しました!",
|
||||||
"failed_to_delete_media": "他のメディアの削除に失敗しました",
|
"failed_to_delete_media": "他のメディアの削除に失敗しました",
|
||||||
"download_deleted": "ダウンロードが削除されました",
|
"download_deleted": "ダウンロードが削除されました",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"tvseries": "TV-Series",
|
"series": "TV-Series",
|
||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
"other_media": "Other media",
|
"other_media": "Other media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "No Items in Queue",
|
"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_tvseries_button": "Delete All TV-Series",
|
"delete_all_series_button": "Delete All TV-Series",
|
||||||
"delete_all_button": "Delete All",
|
"delete_all_button": "Delete All",
|
||||||
"delete_all_other_media_button": "Delete other media",
|
"delete_all_other_media_button": "Delete other media",
|
||||||
"active_download": "Active Download",
|
"active_download": "Active Download",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
||||||
"deleted_all_movies_successfully": "Deleted All Movies Successfully!",
|
"deleted_all_movies_successfully": "Deleted All Movies Successfully!",
|
||||||
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
||||||
"deleted_all_tvseries_successfully": "Deleted All TV-Series Successfully!",
|
"deleted_all_series_successfully": "Deleted All TV-Series Successfully!",
|
||||||
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
|
"failed_to_delete_all_series": "Failed to Delete All TV-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_deleted": "Download Deleted",
|
||||||
|
|||||||
@@ -229,14 +229,14 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Nedlastinger",
|
"downloads_title": "Nedlastinger",
|
||||||
"tvseries": "TV-serier",
|
"series": "TV-serier",
|
||||||
"movies": "Filmer",
|
"movies": "Filmer",
|
||||||
"queue": "Kø",
|
"queue": "Kø",
|
||||||
"queue_hint": "Kø og nedlastinger vil gå tapt ved omstart av appen",
|
"queue_hint": "Kø og nedlastinger vil gå tapt ved omstart av appen",
|
||||||
"no_items_in_queue": "Ingen elementer i køen",
|
"no_items_in_queue": "Ingen elementer i køen",
|
||||||
"no_downloaded_items": "Ingen nedlastede elementer",
|
"no_downloaded_items": "Ingen nedlastede elementer",
|
||||||
"delete_all_movies_button": "Slett alle filmer",
|
"delete_all_movies_button": "Slett alle filmer",
|
||||||
"delete_all_tvseries_button": "Slett alle TV-serier",
|
"delete_all_series_button": "Slett alle TV-serier",
|
||||||
"delete_all_button": "Slett alt",
|
"delete_all_button": "Slett alt",
|
||||||
"active_download": "Aktiv nedlasting",
|
"active_download": "Aktiv nedlasting",
|
||||||
"no_active_downloads": "Ingen aktive nedlastinger",
|
"no_active_downloads": "Ingen aktive nedlastinger",
|
||||||
@@ -253,8 +253,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Du har ikke tillatelse til å laste ned filer.",
|
"you_are_not_allowed_to_download_files": "Du har ikke tillatelse til å laste ned filer.",
|
||||||
"deleted_all_movies_successfully": "Alle filmer ble slettet!",
|
"deleted_all_movies_successfully": "Alle filmer ble slettet!",
|
||||||
"failed_to_delete_all_movies": "Kunne ikke slette alle filmer",
|
"failed_to_delete_all_movies": "Kunne ikke slette alle filmer",
|
||||||
"deleted_all_tvseries_successfully": "Alle TV-serier ble slettet!",
|
"deleted_all_series_successfully": "Alle TV-serier ble slettet!",
|
||||||
"failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-serier",
|
"failed_to_delete_all_series": "Kunne ikke slette alle TV-serier",
|
||||||
"download_cancelled": "Nedlasting avbrutt",
|
"download_cancelled": "Nedlasting avbrutt",
|
||||||
"could_not_cancel_download": "Kunne ikke avbryte nedlastingen",
|
"could_not_cancel_download": "Kunne ikke avbryte nedlastingen",
|
||||||
"download_completed": "Nedlasting fullført",
|
"download_completed": "Nedlasting fullført",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"tvseries": "Series",
|
"series": "Series",
|
||||||
"movies": "Films",
|
"movies": "Films",
|
||||||
"queue": "Wachtrij",
|
"queue": "Wachtrij",
|
||||||
"other_media": "Andere media",
|
"other_media": "Andere media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Geen items in wachtrij",
|
"no_items_in_queue": "Geen items in wachtrij",
|
||||||
"no_downloaded_items": "Geen gedownloade items",
|
"no_downloaded_items": "Geen gedownloade items",
|
||||||
"delete_all_movies_button": "Verwijder alle films",
|
"delete_all_movies_button": "Verwijder alle films",
|
||||||
"delete_all_tvseries_button": "Verwijder alle Series",
|
"delete_all_series_button": "Verwijder alle Series",
|
||||||
"delete_all_button": "Verwijder alles",
|
"delete_all_button": "Verwijder alles",
|
||||||
"delete_all_other_media_button": "Andere media verwijderen",
|
"delete_all_other_media_button": "Andere media verwijderen",
|
||||||
"active_download": "Actieve download",
|
"active_download": "Actieve download",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.",
|
"you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.",
|
||||||
"deleted_all_movies_successfully": "Alle films succesvol verwijderd!",
|
"deleted_all_movies_successfully": "Alle films succesvol verwijderd!",
|
||||||
"failed_to_delete_all_movies": "Alle films zijn niet verwijderd",
|
"failed_to_delete_all_movies": "Alle films zijn niet verwijderd",
|
||||||
"deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!",
|
"deleted_all_series_successfully": "Alle series succesvol verwijderd!",
|
||||||
"failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd",
|
"failed_to_delete_all_series": "Alle series zijn niet verwijderd",
|
||||||
"deleted_media_successfully": "Andere media succesvol verwijderd!",
|
"deleted_media_successfully": "Andere media succesvol verwijderd!",
|
||||||
"failed_to_delete_media": "Verwijderen van andere media mislukt",
|
"failed_to_delete_media": "Verwijderen van andere media mislukt",
|
||||||
"download_deleted": "Download verwijderd",
|
"download_deleted": "Download verwijderd",
|
||||||
|
|||||||
@@ -229,14 +229,14 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Nedlastingar",
|
"downloads_title": "Nedlastingar",
|
||||||
"tvseries": "TV-seriar",
|
"series": "TV-seriar",
|
||||||
"movies": "Filmar",
|
"movies": "Filmar",
|
||||||
"queue": "Kø",
|
"queue": "Kø",
|
||||||
"queue_hint": "Kø og nedlastingar vil gå tapt ved omstart av appen",
|
"queue_hint": "Kø og nedlastingar vil gå tapt ved omstart av appen",
|
||||||
"no_items_in_queue": "Ingen element i køen",
|
"no_items_in_queue": "Ingen element i køen",
|
||||||
"no_downloaded_items": "Ingen nedlasta element",
|
"no_downloaded_items": "Ingen nedlasta element",
|
||||||
"delete_all_movies_button": "Slett alle filmar",
|
"delete_all_movies_button": "Slett alle filmar",
|
||||||
"delete_all_tvseries_button": "Slett alle TV-seriar",
|
"delete_all_series_button": "Slett alle TV-seriar",
|
||||||
"delete_all_button": "Slett alt",
|
"delete_all_button": "Slett alt",
|
||||||
"active_download": "Aktiv nedlasting",
|
"active_download": "Aktiv nedlasting",
|
||||||
"no_active_downloads": "Ingen aktive nedlastingar",
|
"no_active_downloads": "Ingen aktive nedlastingar",
|
||||||
@@ -253,8 +253,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Du har ikkje løyve til å lasta ned filer.",
|
"you_are_not_allowed_to_download_files": "Du har ikkje løyve til å lasta ned filer.",
|
||||||
"deleted_all_movies_successfully": "Alle filmar vart sletta!",
|
"deleted_all_movies_successfully": "Alle filmar vart sletta!",
|
||||||
"failed_to_delete_all_movies": "Kunne ikkje sletta alle filmar",
|
"failed_to_delete_all_movies": "Kunne ikkje sletta alle filmar",
|
||||||
"deleted_all_tvseries_successfully": "Alle TV-seriar vart sletta!",
|
"deleted_all_series_successfully": "Alle TV-seriar vart sletta!",
|
||||||
"failed_to_delete_all_tvseries": "Kunne ikkje sletta alle TV-seriar",
|
"failed_to_delete_all_series": "Kunne ikkje sletta alle TV-seriar",
|
||||||
"download_cancelled": "Nedlasting avbroten",
|
"download_cancelled": "Nedlasting avbroten",
|
||||||
"could_not_cancel_download": "Kunne ikkje avbryta nedlastinga",
|
"could_not_cancel_download": "Kunne ikkje avbryta nedlastinga",
|
||||||
"download_completed": "Nedlasting fullført",
|
"download_completed": "Nedlasting fullført",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Nedlastinger",
|
"downloads_title": "Nedlastinger",
|
||||||
"tvseries": "TV-Serier",
|
"series": "TV-Serier",
|
||||||
"movies": "Filmer",
|
"movies": "Filmer",
|
||||||
"queue": "Kø",
|
"queue": "Kø",
|
||||||
"other_media": "Andre medier",
|
"other_media": "Andre medier",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Ingen elementer i køen",
|
"no_items_in_queue": "Ingen elementer i køen",
|
||||||
"no_downloaded_items": "Ingen nedlastede elementer",
|
"no_downloaded_items": "Ingen nedlastede elementer",
|
||||||
"delete_all_movies_button": "Slett alle filmer",
|
"delete_all_movies_button": "Slett alle filmer",
|
||||||
"delete_all_tvseries_button": "Slett alle TV-Serier",
|
"delete_all_series_button": "Slett alle TV-Serier",
|
||||||
"delete_all_button": "Slett alle",
|
"delete_all_button": "Slett alle",
|
||||||
"delete_all_other_media_button": "Slett andre media",
|
"delete_all_other_media_button": "Slett andre media",
|
||||||
"active_download": "Aktiv nedlasting",
|
"active_download": "Aktiv nedlasting",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Du har ikke lov til å laste ned filer.",
|
"you_are_not_allowed_to_download_files": "Du har ikke lov til å laste ned filer.",
|
||||||
"deleted_all_movies_successfully": "Slettet alle filmer vellykket!",
|
"deleted_all_movies_successfully": "Slettet alle filmer vellykket!",
|
||||||
"failed_to_delete_all_movies": "Kunne ikke slette alle filmer",
|
"failed_to_delete_all_movies": "Kunne ikke slette alle filmer",
|
||||||
"deleted_all_tvseries_successfully": "Alle TV-Serier ble slettet!",
|
"deleted_all_series_successfully": "Alle TV-Serier ble slettet!",
|
||||||
"failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-Serier",
|
"failed_to_delete_all_series": "Kunne ikke slette alle TV-Serier",
|
||||||
"deleted_media_successfully": "Slettet andre media vellykket!",
|
"deleted_media_successfully": "Slettet andre media vellykket!",
|
||||||
"failed_to_delete_media": "Kunne ikke slette andre medier",
|
"failed_to_delete_media": "Kunne ikke slette andre medier",
|
||||||
"download_deleted": "Nedlasting slettet",
|
"download_deleted": "Nedlasting slettet",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Pobrane",
|
"downloads_title": "Pobrane",
|
||||||
"tvseries": "Seriale",
|
"series": "Seriale",
|
||||||
"movies": "Filmy",
|
"movies": "Filmy",
|
||||||
"queue": "Kolejka",
|
"queue": "Kolejka",
|
||||||
"other_media": "Inne media",
|
"other_media": "Inne media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Brak elementów w kolejce",
|
"no_items_in_queue": "Brak elementów w kolejce",
|
||||||
"no_downloaded_items": "Brak pobranych elementów",
|
"no_downloaded_items": "Brak pobranych elementów",
|
||||||
"delete_all_movies_button": "Usuń wszystkie filmy",
|
"delete_all_movies_button": "Usuń wszystkie filmy",
|
||||||
"delete_all_tvseries_button": "Usuń wszystkie seriale",
|
"delete_all_series_button": "Usuń wszystkie seriale",
|
||||||
"delete_all_button": "Usuń wszystko",
|
"delete_all_button": "Usuń wszystko",
|
||||||
"delete_all_other_media_button": "Usuń inne media",
|
"delete_all_other_media_button": "Usuń inne media",
|
||||||
"active_download": "Aktywne pobieranie",
|
"active_download": "Aktywne pobieranie",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Nie masz uprawnień do pobierania plików.",
|
"you_are_not_allowed_to_download_files": "Nie masz uprawnień do pobierania plików.",
|
||||||
"deleted_all_movies_successfully": "Wszystkie filmy zostały pomyślnie usunięte!",
|
"deleted_all_movies_successfully": "Wszystkie filmy zostały pomyślnie usunięte!",
|
||||||
"failed_to_delete_all_movies": "Nie udało się usunąć wszystkich filmów",
|
"failed_to_delete_all_movies": "Nie udało się usunąć wszystkich filmów",
|
||||||
"deleted_all_tvseries_successfully": "Wszystkie seriale zostały pomyślnie usunięte!",
|
"deleted_all_series_successfully": "Wszystkie seriale zostały pomyślnie usunięte!",
|
||||||
"failed_to_delete_all_tvseries": "Nie udało się usunąć wszystkich seriali",
|
"failed_to_delete_all_series": "Nie udało się usunąć wszystkich seriali",
|
||||||
"deleted_media_successfully": "Pomyślnie usunięto inne media!",
|
"deleted_media_successfully": "Pomyślnie usunięto inne media!",
|
||||||
"failed_to_delete_media": "Nie udało się usunąć innych mediów",
|
"failed_to_delete_media": "Nie udało się usunąć innych mediów",
|
||||||
"download_deleted": "Pobieranie usunięte",
|
"download_deleted": "Pobieranie usunięte",
|
||||||
|
|||||||
@@ -227,14 +227,14 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"tvseries": "TV/Séries",
|
"series": "TV/Séries",
|
||||||
"movies": "Filmes",
|
"movies": "Filmes",
|
||||||
"queue": "Fila",
|
"queue": "Fila",
|
||||||
"queue_hint": "A fila e os downloads serão perdidos ao reiniciar o aplicativo",
|
"queue_hint": "A fila e os downloads serão perdidos ao reiniciar o aplicativo",
|
||||||
"no_items_in_queue": "Nenhum item na fila",
|
"no_items_in_queue": "Nenhum item na fila",
|
||||||
"no_downloaded_items": "Nenhum item baixado",
|
"no_downloaded_items": "Nenhum item baixado",
|
||||||
"delete_all_movies_button": "Remover todos os filmes",
|
"delete_all_movies_button": "Remover todos os filmes",
|
||||||
"delete_all_tvseries_button": "Remover todos as TV/Séries",
|
"delete_all_series_button": "Remover todos as TV/Séries",
|
||||||
"delete_all_button": "Remover tudo",
|
"delete_all_button": "Remover tudo",
|
||||||
"active_download": "Downloads ativos",
|
"active_download": "Downloads ativos",
|
||||||
"no_active_downloads": "Nenhum download ativo",
|
"no_active_downloads": "Nenhum download ativo",
|
||||||
@@ -251,8 +251,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Você não tem permissão para baixar arquivos.",
|
"you_are_not_allowed_to_download_files": "Você não tem permissão para baixar arquivos.",
|
||||||
"deleted_all_movies_successfully": "Todos os filmes foram removidos com sucesso!",
|
"deleted_all_movies_successfully": "Todos os filmes foram removidos com sucesso!",
|
||||||
"failed_to_delete_all_movies": "Falha ao remover todos os filmes",
|
"failed_to_delete_all_movies": "Falha ao remover todos os filmes",
|
||||||
"deleted_all_tvseries_successfully": "Todos as TV/Séries foram removidos com sucesso!",
|
"deleted_all_series_successfully": "Todos as TV/Séries foram removidos com sucesso!",
|
||||||
"failed_to_delete_all_tvseries": "Falha ao remover todos as TV/Séries",
|
"failed_to_delete_all_series": "Falha ao remover todos as TV/Séries",
|
||||||
"download_cancelled": "Download cancelado",
|
"download_cancelled": "Download cancelado",
|
||||||
"could_not_cancel_download": "Não foi possível cancelar o download",
|
"could_not_cancel_download": "Não foi possível cancelar o download",
|
||||||
"download_completed": "Download completo",
|
"download_completed": "Download completo",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"tvseries": "TV-Séries",
|
"series": "TV-Séries",
|
||||||
"movies": "Filmes",
|
"movies": "Filmes",
|
||||||
"queue": "Fila",
|
"queue": "Fila",
|
||||||
"other_media": "Outras mídias",
|
"other_media": "Outras mídias",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Nenhum item na fila",
|
"no_items_in_queue": "Nenhum item na fila",
|
||||||
"no_downloaded_items": "Nenhum item baixado",
|
"no_downloaded_items": "Nenhum item baixado",
|
||||||
"delete_all_movies_button": "Excluir todos os filmes",
|
"delete_all_movies_button": "Excluir todos os filmes",
|
||||||
"delete_all_tvseries_button": "Excluir todas as séries",
|
"delete_all_series_button": "Excluir todas as séries",
|
||||||
"delete_all_button": "Excluir todos os",
|
"delete_all_button": "Excluir todos os",
|
||||||
"delete_all_other_media_button": "Excluir outras mídias",
|
"delete_all_other_media_button": "Excluir outras mídias",
|
||||||
"active_download": "Download ativo",
|
"active_download": "Download ativo",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Você não tem permissão para baixar arquivos.",
|
"you_are_not_allowed_to_download_files": "Você não tem permissão para baixar arquivos.",
|
||||||
"deleted_all_movies_successfully": "Todos os filmes excluídos com sucesso!",
|
"deleted_all_movies_successfully": "Todos os filmes excluídos com sucesso!",
|
||||||
"failed_to_delete_all_movies": "Falha ao excluir todos os filmes",
|
"failed_to_delete_all_movies": "Falha ao excluir todos os filmes",
|
||||||
"deleted_all_tvseries_successfully": "Todas as TV-Series excluídas com sucesso!",
|
"deleted_all_series_successfully": "Todas as TV-Series excluídas com sucesso!",
|
||||||
"failed_to_delete_all_tvseries": "Falha ao excluir todas as séries",
|
"failed_to_delete_all_series": "Falha ao excluir todas as séries",
|
||||||
"deleted_media_successfully": "Outras mídias excluídas com sucesso!",
|
"deleted_media_successfully": "Outras mídias excluídas com sucesso!",
|
||||||
"failed_to_delete_media": "Falha ao excluir outras mídias",
|
"failed_to_delete_media": "Falha ao excluir outras mídias",
|
||||||
"download_deleted": "Download Excluído",
|
"download_deleted": "Download Excluído",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Descărcări",
|
"downloads_title": "Descărcări",
|
||||||
"tvseries": "Seriale",
|
"series": "Seriale",
|
||||||
"movies": "Filme",
|
"movies": "Filme",
|
||||||
"queue": "Coadă",
|
"queue": "Coadă",
|
||||||
"other_media": "Alte suporturi",
|
"other_media": "Alte suporturi",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Niciun articol în coadă",
|
"no_items_in_queue": "Niciun articol în coadă",
|
||||||
"no_downloaded_items": "Niciun element descărcat",
|
"no_downloaded_items": "Niciun element descărcat",
|
||||||
"delete_all_movies_button": "Șterge toate filmele",
|
"delete_all_movies_button": "Șterge toate filmele",
|
||||||
"delete_all_tvseries_button": "Șterge toate serialele",
|
"delete_all_series_button": "Șterge toate serialele",
|
||||||
"delete_all_button": "Șterge tot",
|
"delete_all_button": "Șterge tot",
|
||||||
"delete_all_other_media_button": "Șterge alte fișiere media",
|
"delete_all_other_media_button": "Șterge alte fișiere media",
|
||||||
"active_download": "Descărcare activă",
|
"active_download": "Descărcare activă",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Nu aveți voie să descărcați fișiere.",
|
"you_are_not_allowed_to_download_files": "Nu aveți voie să descărcați fișiere.",
|
||||||
"deleted_all_movies_successfully": "Toate filmele au fost șterse cu succes!",
|
"deleted_all_movies_successfully": "Toate filmele au fost șterse cu succes!",
|
||||||
"failed_to_delete_all_movies": "Nu s-au putut șterge toate filmele",
|
"failed_to_delete_all_movies": "Nu s-au putut șterge toate filmele",
|
||||||
"deleted_all_tvseries_successfully": "Toate serialele au fost șterse cu succes!",
|
"deleted_all_series_successfully": "Toate serialele au fost șterse cu succes!",
|
||||||
"failed_to_delete_all_tvseries": "Nu s-au putut șterge toate serialele",
|
"failed_to_delete_all_series": "Nu s-au putut șterge toate serialele",
|
||||||
"deleted_media_successfully": "Alte fișiere șterse cu succes!",
|
"deleted_media_successfully": "Alte fișiere șterse cu succes!",
|
||||||
"failed_to_delete_media": "Ștergerea altor fișiere media a eșuat",
|
"failed_to_delete_media": "Ștergerea altor fișiere media a eșuat",
|
||||||
"download_deleted": "Descărcare ştearsă",
|
"download_deleted": "Descărcare ştearsă",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Загрузки",
|
"downloads_title": "Загрузки",
|
||||||
"tvseries": "Сериалы",
|
"series": "Сериалы",
|
||||||
"movies": "Фильмы",
|
"movies": "Фильмы",
|
||||||
"queue": "Очередь",
|
"queue": "Очередь",
|
||||||
"other_media": "Прочие файлы",
|
"other_media": "Прочие файлы",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Нет элементов в очереди",
|
"no_items_in_queue": "Нет элементов в очереди",
|
||||||
"no_downloaded_items": "Нет загруженных файлов",
|
"no_downloaded_items": "Нет загруженных файлов",
|
||||||
"delete_all_movies_button": "Удалить все фильмы",
|
"delete_all_movies_button": "Удалить все фильмы",
|
||||||
"delete_all_tvseries_button": "Удалить все сериалы",
|
"delete_all_series_button": "Удалить все сериалы",
|
||||||
"delete_all_button": "Удалить все",
|
"delete_all_button": "Удалить все",
|
||||||
"delete_all_other_media_button": "Удалить прочие файлы",
|
"delete_all_other_media_button": "Удалить прочие файлы",
|
||||||
"active_download": "Загружается",
|
"active_download": "Загружается",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Нет разрешения на скачивание файлов.",
|
"you_are_not_allowed_to_download_files": "Нет разрешения на скачивание файлов.",
|
||||||
"deleted_all_movies_successfully": "Все фильмы были успешно удалены!",
|
"deleted_all_movies_successfully": "Все фильмы были успешно удалены!",
|
||||||
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
|
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
|
||||||
"deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!",
|
"deleted_all_series_successfully": "Все сериалы были успешно удалены!",
|
||||||
"failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов",
|
"failed_to_delete_all_series": "Возникла ошибка при удалении всех сериалов",
|
||||||
"deleted_media_successfully": "Остальные медиафайлы успешно удалены!",
|
"deleted_media_successfully": "Остальные медиафайлы успешно удалены!",
|
||||||
"failed_to_delete_media": "Не удалось удалить остальные медиафайлы",
|
"failed_to_delete_media": "Не удалось удалить остальные медиафайлы",
|
||||||
"download_deleted": "Загруженный контент удалён",
|
"download_deleted": "Загруженный контент удалён",
|
||||||
|
|||||||
@@ -229,14 +229,14 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Shkarkimet",
|
"downloads_title": "Shkarkimet",
|
||||||
"tvseries": "Seriale TV",
|
"series": "Seriale TV",
|
||||||
"movies": "Filma",
|
"movies": "Filma",
|
||||||
"queue": "Rradhë",
|
"queue": "Rradhë",
|
||||||
"queue_hint": "Rradhat dhe shkarkimet do të humbasin pas genstartit të aplikacionit",
|
"queue_hint": "Rradhat dhe shkarkimet do të humbasin pas genstartit të aplikacionit",
|
||||||
"no_items_in_queue": "Nuk ka elemente në rradhë",
|
"no_items_in_queue": "Nuk ka elemente në rradhë",
|
||||||
"no_downloaded_items": "Nuk ka shkarkime",
|
"no_downloaded_items": "Nuk ka shkarkime",
|
||||||
"delete_all_movies_button": "Fshijë të gjithë filmat",
|
"delete_all_movies_button": "Fshijë të gjithë filmat",
|
||||||
"delete_all_tvseries_button": "Fshijë të gjitha serialet TV",
|
"delete_all_series_button": "Fshijë të gjitha serialet TV",
|
||||||
"delete_all_button": "Fshijë të gjitha",
|
"delete_all_button": "Fshijë të gjitha",
|
||||||
"active_download": "Shkarkim aktiv",
|
"active_download": "Shkarkim aktiv",
|
||||||
"no_active_downloads": "Nuk ka shkarkime aktive",
|
"no_active_downloads": "Nuk ka shkarkime aktive",
|
||||||
@@ -253,8 +253,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Nuk keni të drejtë të shkarkoni skedarë.",
|
"you_are_not_allowed_to_download_files": "Nuk keni të drejtë të shkarkoni skedarë.",
|
||||||
"deleted_all_movies_successfully": "Të gjithë filmat u fshinë me sukses!",
|
"deleted_all_movies_successfully": "Të gjithë filmat u fshinë me sukses!",
|
||||||
"failed_to_delete_all_movies": "Dështojë fshirja e të gjithë filmave",
|
"failed_to_delete_all_movies": "Dështojë fshirja e të gjithë filmave",
|
||||||
"deleted_all_tvseries_successfully": "Të gjitha serialet TV u fshinë me sukses!",
|
"deleted_all_series_successfully": "Të gjitha serialet TV u fshinë me sukses!",
|
||||||
"failed_to_delete_all_tvseries": "Dështojë fshirja e të gjitha serialeve TV",
|
"failed_to_delete_all_series": "Dështojë fshirja e të gjitha serialeve TV",
|
||||||
"download_cancelled": "Shkarkimi u anulua",
|
"download_cancelled": "Shkarkimi u anulua",
|
||||||
"could_not_cancel_download": "Nuk mundet të anulohet shkarkimi",
|
"could_not_cancel_download": "Nuk mundet të anulohet shkarkimi",
|
||||||
"download_completed": "Shkarkimi u përfundua",
|
"download_completed": "Shkarkimi u përfundua",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Nedladdningar",
|
"downloads_title": "Nedladdningar",
|
||||||
"tvseries": "TV-Serier",
|
"series": "TV-Serier",
|
||||||
"movies": "Filmer",
|
"movies": "Filmer",
|
||||||
"queue": "Kö",
|
"queue": "Kö",
|
||||||
"other_media": "Annan media",
|
"other_media": "Annan media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Inga objekt i Kön",
|
"no_items_in_queue": "Inga objekt i Kön",
|
||||||
"no_downloaded_items": "Inga Nedladdade Objekt",
|
"no_downloaded_items": "Inga Nedladdade Objekt",
|
||||||
"delete_all_movies_button": "Ta Bort Alla Filmer",
|
"delete_all_movies_button": "Ta Bort Alla Filmer",
|
||||||
"delete_all_tvseries_button": "Ta Bort Alla TV-Serier",
|
"delete_all_series_button": "Ta Bort Alla TV-Serier",
|
||||||
"delete_all_button": "Radera Allt",
|
"delete_all_button": "Radera Allt",
|
||||||
"delete_all_other_media_button": "Ta Bort Andra Videor",
|
"delete_all_other_media_button": "Ta Bort Andra Videor",
|
||||||
"active_download": "Aktiv Nedladdning",
|
"active_download": "Aktiv Nedladdning",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Du har inte behörighet att ladda ner filer.",
|
"you_are_not_allowed_to_download_files": "Du har inte behörighet att ladda ner filer.",
|
||||||
"deleted_all_movies_successfully": "Alla Filmer Har Tagits Bort!",
|
"deleted_all_movies_successfully": "Alla Filmer Har Tagits Bort!",
|
||||||
"failed_to_delete_all_movies": "Det Gick Inte Att Ta Bort Alla Filmer",
|
"failed_to_delete_all_movies": "Det Gick Inte Att Ta Bort Alla Filmer",
|
||||||
"deleted_all_tvseries_successfully": "Alla TV-Serier Har Tagits Bort!",
|
"deleted_all_series_successfully": "Alla TV-Serier Har Tagits Bort!",
|
||||||
"failed_to_delete_all_tvseries": "Det Gick Inte Att Ta Bort Alla TV-Serier",
|
"failed_to_delete_all_series": "Det Gick Inte Att Ta Bort Alla TV-Serier",
|
||||||
"deleted_media_successfully": "Andra Medier Har Tagits Bort!",
|
"deleted_media_successfully": "Andra Medier Har Tagits Bort!",
|
||||||
"failed_to_delete_media": "Kunde Inte Ta Bort Andra Medier",
|
"failed_to_delete_media": "Kunde Inte Ta Bort Andra Medier",
|
||||||
"download_deleted": "Nedladdning Borttagen",
|
"download_deleted": "Nedladdning Borttagen",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"tvseries": "TV-Series",
|
"series": "TV-Series",
|
||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
"other_media": "Other media",
|
"other_media": "Other media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "No Items in Queue",
|
"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_tvseries_button": "Delete All TV-Series",
|
"delete_all_series_button": "Delete All TV-Series",
|
||||||
"delete_all_button": "Delete All",
|
"delete_all_button": "Delete All",
|
||||||
"delete_all_other_media_button": "Delete other media",
|
"delete_all_other_media_button": "Delete other media",
|
||||||
"active_download": "Active Download",
|
"active_download": "Active Download",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
||||||
"deleted_all_movies_successfully": "Deleted All Movies Successfully!",
|
"deleted_all_movies_successfully": "Deleted All Movies Successfully!",
|
||||||
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
||||||
"deleted_all_tvseries_successfully": "Deleted All TV-Series Successfully!",
|
"deleted_all_series_successfully": "Deleted All TV-Series Successfully!",
|
||||||
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
|
"failed_to_delete_all_series": "Failed to Delete All TV-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_deleted": "Download Deleted",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Qaw' Doch",
|
"downloads_title": "Qaw' Doch",
|
||||||
"tvseries": "TV Hem",
|
"series": "TV Hem",
|
||||||
"movies": "DIS",
|
"movies": "DIS",
|
||||||
"queue": "ghom",
|
"queue": "ghom",
|
||||||
"other_media": "Other media",
|
"other_media": "Other media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "ghom Doch pagh",
|
"no_items_in_queue": "ghom Doch pagh",
|
||||||
"no_downloaded_items": "Qaw' Doch pagh",
|
"no_downloaded_items": "Qaw' Doch pagh",
|
||||||
"delete_all_movies_button": "Hoch DIS yIQaw'",
|
"delete_all_movies_button": "Hoch DIS yIQaw'",
|
||||||
"delete_all_tvseries_button": "Hoch TV Hem yIQaw'",
|
"delete_all_series_button": "Hoch TV Hem yIQaw'",
|
||||||
"delete_all_button": "Hoch yIQaw'",
|
"delete_all_button": "Hoch yIQaw'",
|
||||||
"delete_all_other_media_button": "Delete other media",
|
"delete_all_other_media_button": "Delete other media",
|
||||||
"active_download": "chu' Qaw'",
|
"active_download": "chu' Qaw'",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Doch Qaw' je'laHbe'.",
|
"you_are_not_allowed_to_download_files": "Doch Qaw' je'laHbe'.",
|
||||||
"deleted_all_movies_successfully": "Hoch DIS Qaw' Qapla'!",
|
"deleted_all_movies_successfully": "Hoch DIS Qaw' Qapla'!",
|
||||||
"failed_to_delete_all_movies": "Hoch DIS Qaw'laHbe'",
|
"failed_to_delete_all_movies": "Hoch DIS Qaw'laHbe'",
|
||||||
"deleted_all_tvseries_successfully": "Hoch TV Hem Qaw' Qapla'!",
|
"deleted_all_series_successfully": "Hoch TV Hem Qaw' Qapla'!",
|
||||||
"failed_to_delete_all_tvseries": "Hoch TV Hem Qaw'laHbe'",
|
"failed_to_delete_all_series": "Hoch TV Hem Qaw'laHbe'",
|
||||||
"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_deleted": "Download Deleted",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "İndirilenler",
|
"downloads_title": "İndirilenler",
|
||||||
"tvseries": "Diziler",
|
"series": "Diziler",
|
||||||
"movies": "Filmler",
|
"movies": "Filmler",
|
||||||
"queue": "Sıra",
|
"queue": "Sıra",
|
||||||
"other_media": "Diğer medya",
|
"other_media": "Diğer medya",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Sırada öğe yok",
|
"no_items_in_queue": "Sırada öğe yok",
|
||||||
"no_downloaded_items": "İndirilen öğe yok",
|
"no_downloaded_items": "İndirilen öğe yok",
|
||||||
"delete_all_movies_button": "Tüm Filmleri Sil",
|
"delete_all_movies_button": "Tüm Filmleri Sil",
|
||||||
"delete_all_tvseries_button": "Tüm Dizileri Sil",
|
"delete_all_series_button": "Tüm Dizileri Sil",
|
||||||
"delete_all_button": "Tümünü Sil",
|
"delete_all_button": "Tümünü Sil",
|
||||||
"delete_all_other_media_button": "Diğer medyayı sil",
|
"delete_all_other_media_button": "Diğer medyayı sil",
|
||||||
"active_download": "Aktif indirme",
|
"active_download": "Aktif indirme",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Dosyaları indirme izniniz yok.",
|
"you_are_not_allowed_to_download_files": "Dosyaları indirme izniniz yok.",
|
||||||
"deleted_all_movies_successfully": "Tüm filmler başarıyla silindi!",
|
"deleted_all_movies_successfully": "Tüm filmler başarıyla silindi!",
|
||||||
"failed_to_delete_all_movies": "Filmler silinemedi",
|
"failed_to_delete_all_movies": "Filmler silinemedi",
|
||||||
"deleted_all_tvseries_successfully": "Tüm diziler başarıyla silindi!",
|
"deleted_all_series_successfully": "Tüm diziler başarıyla silindi!",
|
||||||
"failed_to_delete_all_tvseries": "Diziler silinemedi",
|
"failed_to_delete_all_series": "Diziler silinemedi",
|
||||||
"deleted_media_successfully": "Diğer medya başarıyla silindi!",
|
"deleted_media_successfully": "Diğer medya başarıyla silindi!",
|
||||||
"failed_to_delete_media": "Failed to Delete other media",
|
"failed_to_delete_media": "Failed to Delete other media",
|
||||||
"download_deleted": "İndirme silindi",
|
"download_deleted": "İndirme silindi",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Завантаження",
|
"downloads_title": "Завантаження",
|
||||||
"tvseries": "ТБ-Серіали",
|
"series": "ТБ-Серіали",
|
||||||
"movies": "Фільми",
|
"movies": "Фільми",
|
||||||
"queue": "Черга",
|
"queue": "Черга",
|
||||||
"other_media": "Other media",
|
"other_media": "Other media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Нема елементів в черзі",
|
"no_items_in_queue": "Нема елементів в черзі",
|
||||||
"no_downloaded_items": "Нема завантажених елементів",
|
"no_downloaded_items": "Нема завантажених елементів",
|
||||||
"delete_all_movies_button": "Видалити всі Фільми",
|
"delete_all_movies_button": "Видалити всі Фільми",
|
||||||
"delete_all_tvseries_button": "Видалити всі ТБ-Серіали",
|
"delete_all_series_button": "Видалити всі ТБ-Серіали",
|
||||||
"delete_all_button": "Видалити Все",
|
"delete_all_button": "Видалити Все",
|
||||||
"delete_all_other_media_button": "Delete other media",
|
"delete_all_other_media_button": "Delete other media",
|
||||||
"active_download": "Активне завантаження",
|
"active_download": "Активне завантаження",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Вам не дозволено завантажувати файли.",
|
"you_are_not_allowed_to_download_files": "Вам не дозволено завантажувати файли.",
|
||||||
"deleted_all_movies_successfully": "Видалення всіх фільмів було успішне!",
|
"deleted_all_movies_successfully": "Видалення всіх фільмів було успішне!",
|
||||||
"failed_to_delete_all_movies": "Не вдалося видалити усі фільми",
|
"failed_to_delete_all_movies": "Не вдалося видалити усі фільми",
|
||||||
"deleted_all_tvseries_successfully": "Успішно видалено всі серіали!",
|
"deleted_all_series_successfully": "Успішно видалено всі серіали!",
|
||||||
"failed_to_delete_all_tvseries": "Не вдалося видалити всі телесеріали",
|
"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_deleted": "Download Deleted",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Tải xuống",
|
"downloads_title": "Tải xuống",
|
||||||
"tvseries": "Chương trình TV",
|
"series": "Chương trình TV",
|
||||||
"movies": "Phim",
|
"movies": "Phim",
|
||||||
"queue": "Hàng đợi",
|
"queue": "Hàng đợi",
|
||||||
"other_media": "Other media",
|
"other_media": "Other media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "Không có mục trong hàng đợi",
|
"no_items_in_queue": "Không có mục trong hàng đợi",
|
||||||
"no_downloaded_items": "Không có mục đã tải",
|
"no_downloaded_items": "Không có mục đã tải",
|
||||||
"delete_all_movies_button": "Xóa tất cả phim",
|
"delete_all_movies_button": "Xóa tất cả phim",
|
||||||
"delete_all_tvseries_button": "Xóa tất cả chương trình TV",
|
"delete_all_series_button": "Xóa tất cả chương trình TV",
|
||||||
"delete_all_button": "Xóa tất cả",
|
"delete_all_button": "Xóa tất cả",
|
||||||
"delete_all_other_media_button": "Delete other media",
|
"delete_all_other_media_button": "Delete other media",
|
||||||
"active_download": "Đang tải xuống",
|
"active_download": "Đang tải xuống",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "Bạn không có quyền tải nội dung.",
|
"you_are_not_allowed_to_download_files": "Bạn không có quyền tải nội dung.",
|
||||||
"deleted_all_movies_successfully": "Đã xóa tất cả phim thành công!",
|
"deleted_all_movies_successfully": "Đã xóa tất cả phim thành công!",
|
||||||
"failed_to_delete_all_movies": "Xóa phim thất bại",
|
"failed_to_delete_all_movies": "Xóa phim thất bại",
|
||||||
"deleted_all_tvseries_successfully": "Đã xóa tất cả chương trình TV thành công!",
|
"deleted_all_series_successfully": "Đã xóa tất cả chương trình TV thành công!",
|
||||||
"failed_to_delete_all_tvseries": "Xóa chương trình TV thất bại",
|
"failed_to_delete_all_series": "Xóa chương trình TV thất bại",
|
||||||
"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_deleted": "Download Deleted",
|
||||||
|
|||||||
@@ -224,14 +224,14 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "下载",
|
"downloads_title": "下载",
|
||||||
"tvseries": "剧集",
|
"series": "剧集",
|
||||||
"movies": "电影",
|
"movies": "电影",
|
||||||
"queue": "队列",
|
"queue": "队列",
|
||||||
"queue_hint": "应用重启后队列和下载将会丢失",
|
"queue_hint": "应用重启后队列和下载将会丢失",
|
||||||
"no_items_in_queue": "队列中无项目",
|
"no_items_in_queue": "队列中无项目",
|
||||||
"no_downloaded_items": "无已下载项目",
|
"no_downloaded_items": "无已下载项目",
|
||||||
"delete_all_movies_button": "删除所有电影",
|
"delete_all_movies_button": "删除所有电影",
|
||||||
"delete_all_tvseries_button": "删除所有剧集",
|
"delete_all_series_button": "删除所有剧集",
|
||||||
"delete_all_button": "删除全部",
|
"delete_all_button": "删除全部",
|
||||||
"active_download": "活跃下载",
|
"active_download": "活跃下载",
|
||||||
"no_active_downloads": "无活跃下载",
|
"no_active_downloads": "无活跃下载",
|
||||||
@@ -248,8 +248,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "您无权下载文件。",
|
"you_are_not_allowed_to_download_files": "您无权下载文件。",
|
||||||
"deleted_all_movies_successfully": "成功删除所有电影!",
|
"deleted_all_movies_successfully": "成功删除所有电影!",
|
||||||
"failed_to_delete_all_movies": "删除所有电影失败",
|
"failed_to_delete_all_movies": "删除所有电影失败",
|
||||||
"deleted_all_tvseries_successfully": "成功删除所有剧集!",
|
"deleted_all_series_successfully": "成功删除所有剧集!",
|
||||||
"failed_to_delete_all_tvseries": "删除所有剧集失败",
|
"failed_to_delete_all_series": "删除所有剧集失败",
|
||||||
"download_cancelled": "下载已取消",
|
"download_cancelled": "下载已取消",
|
||||||
"could_not_cancel_download": "无法取消下载",
|
"could_not_cancel_download": "无法取消下载",
|
||||||
"download_completed": "下载完成",
|
"download_completed": "下载完成",
|
||||||
|
|||||||
@@ -231,14 +231,14 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "下載",
|
"downloads_title": "下載",
|
||||||
"tvseries": "電視劇",
|
"series": "電視劇",
|
||||||
"movies": "電影",
|
"movies": "電影",
|
||||||
"queue": "隊列",
|
"queue": "隊列",
|
||||||
"queue_hint": "應用重啟後隊列和下載將會丟失",
|
"queue_hint": "應用重啟後隊列和下載將會丟失",
|
||||||
"no_items_in_queue": "隊列中無項目",
|
"no_items_in_queue": "隊列中無項目",
|
||||||
"no_downloaded_items": "無已下載項目",
|
"no_downloaded_items": "無已下載項目",
|
||||||
"delete_all_movies_button": "刪除所有電影",
|
"delete_all_movies_button": "刪除所有電影",
|
||||||
"delete_all_tvseries_button": "刪除所有電視劇",
|
"delete_all_series_button": "刪除所有電視劇",
|
||||||
"delete_all_button": "刪除全部",
|
"delete_all_button": "刪除全部",
|
||||||
"active_download": "活動下載",
|
"active_download": "活動下載",
|
||||||
"no_active_downloads": "無活動下載",
|
"no_active_downloads": "無活動下載",
|
||||||
@@ -255,8 +255,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "您無權下載文件。",
|
"you_are_not_allowed_to_download_files": "您無權下載文件。",
|
||||||
"deleted_all_movies_successfully": "成功刪除所有電影!",
|
"deleted_all_movies_successfully": "成功刪除所有電影!",
|
||||||
"failed_to_delete_all_movies": "刪除所有電影失敗",
|
"failed_to_delete_all_movies": "刪除所有電影失敗",
|
||||||
"deleted_all_tvseries_successfully": "成功刪除所有電視劇!",
|
"deleted_all_series_successfully": "成功刪除所有電視劇!",
|
||||||
"failed_to_delete_all_tvseries": "刪除所有電視劇失敗",
|
"failed_to_delete_all_series": "刪除所有電視劇失敗",
|
||||||
"download_cancelled": "下載已取消",
|
"download_cancelled": "下載已取消",
|
||||||
"could_not_cancel_download": "無法取消下載",
|
"could_not_cancel_download": "無法取消下載",
|
||||||
"download_completed": "下載完成",
|
"download_completed": "下載完成",
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"tvseries": "TV-Series",
|
"series": "TV-Series",
|
||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
"other_media": "Other media",
|
"other_media": "Other media",
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"no_items_in_queue": "No Items in Queue",
|
"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_tvseries_button": "Delete All TV-Series",
|
"delete_all_series_button": "Delete All TV-Series",
|
||||||
"delete_all_button": "Delete All",
|
"delete_all_button": "Delete All",
|
||||||
"delete_all_other_media_button": "Delete other media",
|
"delete_all_other_media_button": "Delete other media",
|
||||||
"active_download": "Active Download",
|
"active_download": "Active Download",
|
||||||
@@ -542,8 +542,8 @@
|
|||||||
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
||||||
"deleted_all_movies_successfully": "Deleted All Movies Successfully!",
|
"deleted_all_movies_successfully": "Deleted All Movies Successfully!",
|
||||||
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
||||||
"deleted_all_tvseries_successfully": "Deleted All TV-Series Successfully!",
|
"deleted_all_series_successfully": "Deleted All TV-Series Successfully!",
|
||||||
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
|
"failed_to_delete_all_series": "Failed to Delete All TV-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_deleted": "Download Deleted",
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,17 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
import { generateDeviceProfile } from "./native";
|
import type {
|
||||||
|
DeviceProfile,
|
||||||
/**
|
SubtitleProfile,
|
||||||
* @typedef {"auto" | "stereo" | "5.1" | "passthrough"} AudioTranscodeModeType
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
*/
|
import { type AudioTranscodeModeType, generateDeviceProfile } from "./native";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download-specific subtitle profiles.
|
* Download-specific subtitle profiles.
|
||||||
* These are more permissive than streaming profiles since we can embed subtitles.
|
* These are more permissive than streaming profiles since we can embed subtitles.
|
||||||
*/
|
*/
|
||||||
const downloadSubtitleProfiles = [
|
const downloadSubtitleProfiles: SubtitleProfile[] = [
|
||||||
// Official formats
|
// Official formats
|
||||||
{ Format: "vtt", Method: "Encode" },
|
{ Format: "vtt", Method: "Encode" },
|
||||||
{ Format: "webvtt", Method: "Encode" },
|
{ Format: "webvtt", Method: "Encode" },
|
||||||
@@ -46,11 +46,10 @@ const downloadSubtitleProfiles = [
|
|||||||
/**
|
/**
|
||||||
* Generates a device profile optimized for downloads.
|
* Generates a device profile optimized for downloads.
|
||||||
* Uses the same audio codec logic as streaming but with download-specific bitrate limits.
|
* Uses the same audio codec logic as streaming but with download-specific bitrate limits.
|
||||||
*
|
|
||||||
* @param {AudioTranscodeModeType} [audioMode="auto"] - Audio transcoding mode
|
|
||||||
* @returns {Object} Jellyfin device profile for downloads
|
|
||||||
*/
|
*/
|
||||||
export const generateDownloadProfile = (audioMode = "auto") => {
|
export const generateDownloadProfile = (
|
||||||
|
audioMode: AudioTranscodeModeType = "auto",
|
||||||
|
): DeviceProfile => {
|
||||||
// Get the base profile with proper audio codec configuration
|
// Get the base profile with proper audio codec configuration
|
||||||
const baseProfile = generateDeviceProfile({ audioMode });
|
const baseProfile = generateDeviceProfile({ audioMode });
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
import type { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import MediaTypes from "../../constants/MediaTypes";
|
import MediaTypes from "../../constants/MediaTypes";
|
||||||
import { getSubtitleProfiles } from "./subtitles";
|
import { getSubtitleProfiles } from "./subtitles";
|
||||||
@@ -193,7 +194,7 @@ export const generateDeviceProfile = (options: ProfileOptions = {}) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
SubtitleProfiles: getSubtitleProfiles(),
|
SubtitleProfiles: getSubtitleProfiles(),
|
||||||
};
|
} satisfies DeviceProfile;
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
import type { SubtitleProfile } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
|
||||||
// Image-based formats - these need to be burned in by Jellyfin (Encode method)
|
// Image-based formats - these need to be burned in by Jellyfin (Encode method)
|
||||||
// because MPV cannot load them externally over HTTP
|
// because MPV cannot load them externally over HTTP
|
||||||
@@ -13,7 +14,7 @@ const IMAGE_BASED_FORMATS = [
|
|||||||
"pgssub",
|
"pgssub",
|
||||||
"teletext",
|
"teletext",
|
||||||
"vobsub",
|
"vobsub",
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
// Text-based formats - these can be loaded externally by MPV
|
// Text-based formats - these can be loaded externally by MPV
|
||||||
const TEXT_BASED_FORMATS = [
|
const TEXT_BASED_FORMATS = [
|
||||||
@@ -37,10 +38,10 @@ const TEXT_BASED_FORMATS = [
|
|||||||
"text",
|
"text",
|
||||||
"vplayer",
|
"vplayer",
|
||||||
"xsub",
|
"xsub",
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export const getSubtitleProfiles = () => {
|
export const getSubtitleProfiles = (): SubtitleProfile[] => {
|
||||||
const profiles = [];
|
const profiles: SubtitleProfile[] = [];
|
||||||
|
|
||||||
// Image-based formats: Embed or Encode (burn-in), NOT External
|
// Image-based formats: Embed or Encode (burn-in), NOT External
|
||||||
for (const format of IMAGE_BASED_FORMATS) {
|
for (const format of IMAGE_BASED_FORMATS) {
|
||||||
@@ -58,4 +59,4 @@ export const getSubtitleProfiles = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Export for use in player filtering
|
// Export for use in player filtering
|
||||||
export const IMAGE_SUBTITLE_CODECS = IMAGE_BASED_FORMATS;
|
export const IMAGE_SUBTITLE_CODECS: readonly string[] = IMAGE_BASED_FORMATS;
|
||||||
19
utils/profiles/trackplayer.d.ts
vendored
19
utils/profiles/trackplayer.d.ts
vendored
@@ -1,19 +0,0 @@
|
|||||||
/**
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type PlatformType = "ios" | "android";
|
|
||||||
|
|
||||||
export interface TrackPlayerProfileOptions {
|
|
||||||
/** Target platform */
|
|
||||||
platform?: PlatformType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateTrackPlayerProfile(
|
|
||||||
options?: TrackPlayerProfileOptions,
|
|
||||||
): any;
|
|
||||||
|
|
||||||
declare const _default: any;
|
|
||||||
export default _default;
|
|
||||||
@@ -3,23 +3,26 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
import type {
|
||||||
|
CodecProfile,
|
||||||
|
DeviceProfile,
|
||||||
|
DirectPlayProfile,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import MediaTypes from "../../constants/MediaTypes";
|
import MediaTypes from "../../constants/MediaTypes";
|
||||||
|
|
||||||
/**
|
export type PlatformType = "ios" | "android";
|
||||||
* @typedef {"ios" | "android"} PlatformType
|
|
||||||
*
|
export interface TrackPlayerProfileOptions {
|
||||||
* @typedef {Object} TrackPlayerProfileOptions
|
/** Target platform */
|
||||||
* @property {PlatformType} [platform] - Target platform
|
platform?: PlatformType;
|
||||||
*/
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio direct play profiles for react-native-track-player.
|
* Audio direct play profiles for react-native-track-player.
|
||||||
* iOS uses AVPlayer, Android uses ExoPlayer - each has different codec support.
|
* iOS uses AVPlayer, Android uses ExoPlayer - each has different codec support.
|
||||||
*
|
|
||||||
* @param {PlatformType} platform
|
|
||||||
*/
|
*/
|
||||||
const getDirectPlayProfile = (platform) => {
|
const getDirectPlayProfile = (platform: PlatformType): DirectPlayProfile => {
|
||||||
if (platform === "ios") {
|
if (platform === "ios") {
|
||||||
// iOS AVPlayer supported formats
|
// iOS AVPlayer supported formats
|
||||||
return {
|
return {
|
||||||
@@ -39,10 +42,8 @@ const getDirectPlayProfile = (platform) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio codec profiles for react-native-track-player.
|
* Audio codec profiles for react-native-track-player.
|
||||||
*
|
|
||||||
* @param {PlatformType} platform
|
|
||||||
*/
|
*/
|
||||||
const getCodecProfile = (platform) => {
|
const getCodecProfile = (platform: PlatformType): CodecProfile => {
|
||||||
if (platform === "ios") {
|
if (platform === "ios") {
|
||||||
// iOS AVPlayer codec constraints
|
// iOS AVPlayer codec constraints
|
||||||
return {
|
return {
|
||||||
@@ -64,12 +65,11 @@ const getCodecProfile = (platform) => {
|
|||||||
* This profile is specifically for standalone audio playback using:
|
* This profile is specifically for standalone audio playback using:
|
||||||
* - AVPlayer on iOS
|
* - AVPlayer on iOS
|
||||||
* - ExoPlayer on Android
|
* - ExoPlayer on Android
|
||||||
*
|
|
||||||
* @param {TrackPlayerProfileOptions} [options] - Profile configuration options
|
|
||||||
* @returns {Object} Jellyfin device profile for track player
|
|
||||||
*/
|
*/
|
||||||
export const generateTrackPlayerProfile = (options = {}) => {
|
export const generateTrackPlayerProfile = (
|
||||||
const platform = options.platform || Platform.OS;
|
options: TrackPlayerProfileOptions = {},
|
||||||
|
): DeviceProfile => {
|
||||||
|
const platform = (options.platform || Platform.OS) as PlatformType;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Name: "Track Player",
|
Name: "Track Player",
|
||||||
94
utils/version.ts
Normal file
94
utils/version.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import * as Application from "expo-application";
|
||||||
|
import Constants from "expo-constants";
|
||||||
|
|
||||||
|
/** Raw marketing version (app.json `version`), e.g. "0.54.1". Exposed so the Jellyfin
|
||||||
|
* clientInfo auto-tracks the app version instead of a hardcoded string. */
|
||||||
|
export const APP_VERSION = Application.nativeApplicationVersion ?? "unknown";
|
||||||
|
|
||||||
|
/** Build metadata injected at build time by `app.config.js` into `extra.build`. */
|
||||||
|
export interface BuildMeta {
|
||||||
|
commit?: string | null;
|
||||||
|
branch?: string | null;
|
||||||
|
profile?: string | null;
|
||||||
|
runNumber?: string | null;
|
||||||
|
builtAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionInfo {
|
||||||
|
/** Marketing version (CFBundleShortVersionString / android versionName), e.g. "0.54.1". */
|
||||||
|
version: string | null;
|
||||||
|
/** Build number (CFBundleVersion / versionCode), e.g. "42". */
|
||||||
|
build: string | null;
|
||||||
|
/** Short git commit the build was made from, e.g. "a1b2c3d". */
|
||||||
|
commit: string | null;
|
||||||
|
/** Git branch the build was made from, e.g. "develop". */
|
||||||
|
branch: string | null;
|
||||||
|
/** EAS build profile, e.g. "production", "ci", "preview", or null for local. */
|
||||||
|
profile: string | null;
|
||||||
|
/** GitHub Actions run number the build came from, e.g. "2098". Null outside CI. */
|
||||||
|
runNumber: string | null;
|
||||||
|
isDev: boolean;
|
||||||
|
isProduction: boolean;
|
||||||
|
/** Graduated label for the Settings "App version" row (see tiering below). */
|
||||||
|
display: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a graduated version string for Settings.
|
||||||
|
*
|
||||||
|
* Tiering (most → least detailed):
|
||||||
|
* - dev / local build → `version · branch · commit` (full context for debugging)
|
||||||
|
* - develop / CI / preview → `version · commit · #run` (pin the exact source; the
|
||||||
|
* Actions run number maps the build to its run — artifacts + logs — without
|
||||||
|
* Expo access)
|
||||||
|
* - production (store / TestFlight) → `version` (build number intentionally
|
||||||
|
* not shown: TestFlight already displays it to testers, and the commit pins the
|
||||||
|
* binary better)
|
||||||
|
*/
|
||||||
|
export function getVersionInfo(): VersionInfo {
|
||||||
|
// Read native/config values defensively — a version string must never crash Settings
|
||||||
|
// (e.g. a dev build whose native expo-constants is out of sync with the JS).
|
||||||
|
const read = <T>(fn: () => T): T | null => {
|
||||||
|
try {
|
||||||
|
return fn() ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const version = read(() => Application.nativeApplicationVersion);
|
||||||
|
const build = read(() => Application.nativeBuildVersion);
|
||||||
|
const meta = (read(() => Constants.expoConfig?.extra?.build) ??
|
||||||
|
{}) as BuildMeta;
|
||||||
|
const commit = meta.commit ?? null;
|
||||||
|
const branch = meta.branch ?? null;
|
||||||
|
const profile = meta.profile ?? null;
|
||||||
|
const runNumber = meta.runNumber ?? null;
|
||||||
|
const isDev = __DEV__ === true;
|
||||||
|
const isProduction =
|
||||||
|
typeof profile === "string" && profile.startsWith("production");
|
||||||
|
|
||||||
|
let display: string;
|
||||||
|
if (isDev) {
|
||||||
|
display = [version ?? "dev", branch, commit].filter(Boolean).join(" · ");
|
||||||
|
} else if (isProduction) {
|
||||||
|
display = version ?? build ?? "N/A";
|
||||||
|
} else {
|
||||||
|
display =
|
||||||
|
[version, commit, runNumber && `#${runNumber}`]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" · ") || "N/A";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
build,
|
||||||
|
commit,
|
||||||
|
branch,
|
||||||
|
profile,
|
||||||
|
runNumber,
|
||||||
|
isDev,
|
||||||
|
isProduction,
|
||||||
|
display,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user