forked from Ninjalama/streamyfin_mirror
Compare commits
1 Commits
fix-eos-ca
...
lint/remov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cce8672898 |
79
.github/workflows/build-android.yml
vendored
79
.github/workflows/build-android.yml
vendored
@@ -1,79 +0,0 @@
|
|||||||
name: 🤖 Android APK Build
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
|
||||||
branches: [develop, master]
|
|
||||||
push:
|
|
||||||
branches: [develop, master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
name: 🏗️ Build Android APK
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 📥 Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
show-progress: false
|
|
||||||
submodules: recursive
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: 🍞 Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
|
||||||
with:
|
|
||||||
bun-version: '1.2.15'
|
|
||||||
|
|
||||||
- name: ☕ Setup JDK
|
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '17'
|
|
||||||
|
|
||||||
- name: 💾 Cache Bun dependencies
|
|
||||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
|
||||||
with:
|
|
||||||
path: ~/.bun/install/cache
|
|
||||||
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-cache-
|
|
||||||
|
|
||||||
- name: 📦 Install dependencies
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
bun run submodule-reload
|
|
||||||
|
|
||||||
- name: 💾 Cache Android dependencies
|
|
||||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
android/.gradle
|
|
||||||
key: ${{ runner.os }}-android-deps-${{ hashFiles('android/**/build.gradle') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-android-deps-
|
|
||||||
|
|
||||||
- name: 🛠️ Generate project files
|
|
||||||
run: bun run prebuild
|
|
||||||
|
|
||||||
- name: 🚀 Build APK via Bun
|
|
||||||
run: bun run build:android:local
|
|
||||||
|
|
||||||
- name: 📅 Set date tag
|
|
||||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: 📤 Upload APK artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: streamyfin-apk-${{ github.sha }}-${{ env.DATE_TAG }}
|
|
||||||
path: |
|
|
||||||
android/app/build/outputs/apk/release/*.apk
|
|
||||||
android/app/build/outputs/bundle/release/*.aab
|
|
||||||
retention-days: 7
|
|
||||||
49
.github/workflows/build-ios.yaml
vendored
Normal file
49
.github/workflows/build-ios.yaml
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name: Automatic Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-15
|
||||||
|
name: Build IOS
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
name: Check out repository
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
- run: |
|
||||||
|
bun i && bun run submodule-reload
|
||||||
|
npx expo prebuild
|
||||||
|
- uses: sparkfabrik/ios-build-action@v2.3.0
|
||||||
|
with:
|
||||||
|
upload-to-testflight: false
|
||||||
|
increment-build-number: false
|
||||||
|
build-pods: true
|
||||||
|
pods-path: "ios/Podfile"
|
||||||
|
configuration: Release
|
||||||
|
# Change later to app-store if wanted
|
||||||
|
export-method: appstore
|
||||||
|
#export-method: ad-hoc
|
||||||
|
workspace-path: "ios/Streamyfin.xcodeproj/project.xcworkspace/"
|
||||||
|
project-path: "ios/Streamyfin.xcodeproj"
|
||||||
|
scheme: Streamyfin
|
||||||
|
apple-key-id: ${{ secrets.APPLE_KEY_ID }}
|
||||||
|
apple-key-issuer-id: ${{ secrets.APPLE_KEY_ISSUER_ID }}
|
||||||
|
apple-key-content: ${{ secrets.APPLE_KEY_CONTENT }}
|
||||||
|
team-id: ${{ secrets.TEAM_ID }}
|
||||||
|
team-name: ${{ secrets.TEAM_NAME }}
|
||||||
|
#match-password: ${{ secrets.MATCH_PASSWORD }}
|
||||||
|
#match-git-url: ${{ secrets.MATCH_GIT_URL }}
|
||||||
|
#match-git-basic-authorization: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
|
||||||
|
#match-build-type: "appstore"
|
||||||
|
#browserstack-upload: true
|
||||||
|
#browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }}
|
||||||
|
#browserstack-access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
|
||||||
|
#fastlane-env: stage
|
||||||
|
ios-app-id: com.stetsed.teststreamyfin
|
||||||
|
output-path: build-${{ github.sha }}.ipa
|
||||||
70
.github/workflows/build-ios.yml
vendored
70
.github/workflows/build-ios.yml
vendored
@@ -1,70 +0,0 @@
|
|||||||
name: 🤖 iOS IPA Build
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
|
||||||
branches: [develop, master]
|
|
||||||
push:
|
|
||||||
branches: [develop, master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: macos-15
|
|
||||||
name: 🏗️ Build iOS IPA
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 📥 Check out repository
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
show-progress: false
|
|
||||||
submodules: recursive
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: 🍞 Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
|
||||||
with:
|
|
||||||
bun-version: '1.2.15'
|
|
||||||
|
|
||||||
- name: 💾 Cache Bun dependencies
|
|
||||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
|
||||||
with:
|
|
||||||
path: ~/.bun/install/cache
|
|
||||||
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-cache-
|
|
||||||
|
|
||||||
- name: 📦 Install & Prepare
|
|
||||||
run: |
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
bun run submodule-reload
|
|
||||||
|
|
||||||
- name: 🛠️ Generate project files
|
|
||||||
run: bun run prebuild
|
|
||||||
|
|
||||||
- name: 🏗 Setup EAS
|
|
||||||
uses: expo/expo-github-action@v8
|
|
||||||
with:
|
|
||||||
eas-version: 16.7.1
|
|
||||||
token: ${{ secrets.EXPO_TOKEN }}
|
|
||||||
|
|
||||||
- name: 🏗️ Build iOS app
|
|
||||||
run: |
|
|
||||||
eas build -p ios --local --non-interactive
|
|
||||||
|
|
||||||
- name: 📅 Set date tag
|
|
||||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: 📤 Upload IPA artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: streamyfin-ipa-${{ github.sha }}-${{ env.DATE_TAG }}
|
|
||||||
path: |
|
|
||||||
build-*.ipa
|
|
||||||
retention-days: 7
|
|
||||||
46
.github/workflows/check-lockfile.yml
vendored
46
.github/workflows/check-lockfile.yml
vendored
@@ -1,46 +0,0 @@
|
|||||||
name: 🔒 Lockfile Consistency Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [develop, master]
|
|
||||||
push:
|
|
||||||
branches: [develop, master]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-lockfile:
|
|
||||||
name: 🔍 Check bun.lock and package.json consistency
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 📥 Checkout repository
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
show-progress: false
|
|
||||||
submodules: recursive
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: 🍞 Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
|
||||||
with:
|
|
||||||
bun-version: '1.2.15'
|
|
||||||
|
|
||||||
- name: 💾 Cache Bun dependencies
|
|
||||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
|
||||||
|
|
||||||
- name: 🛡️ Verify lockfile consistency
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
echo "➡️ Checking for discrepancies between bun.lock and package.json..."
|
|
||||||
bun install --frozen-lockfile --dry-run --ignore-scripts
|
|
||||||
echo "✅ Lockfile is consistent with package.json!"
|
|
||||||
43
.github/workflows/ci-codeql.yml
vendored
43
.github/workflows/ci-codeql.yml
vendored
@@ -1,43 +0,0 @@
|
|||||||
name: 🛡️ CodeQL Analysis
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master, develop]
|
|
||||||
pull_request:
|
|
||||||
branches: [master, develop]
|
|
||||||
schedule:
|
|
||||||
- cron: '24 2 * * *'
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: 🔎 Analyze with CodeQL
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'javascript-typescript' ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 📥 Checkout repository
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
show-progress: false
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: 🏁 Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
queries: +security-extended,security-and-quality
|
|
||||||
|
|
||||||
- name: 🛠️ Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
|
||||||
|
|
||||||
- name: 🧪 Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
|
||||||
24
.github/workflows/conflict.yml
vendored
24
.github/workflows/conflict.yml
vendored
@@ -1,24 +0,0 @@
|
|||||||
name: 🏷️🔀Merge Conflict Labeler
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [develop]
|
|
||||||
pull_request_target:
|
|
||||||
branches: [develop]
|
|
||||||
types: [synchronize]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label:
|
|
||||||
name: 🏷️ Labeling Merge Conflicts
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
if: ${{ github.repository == 'streamyfin/streamyfin' }}
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: 🚩 Apply merge conflict label
|
|
||||||
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
|
||||||
with:
|
|
||||||
dirtyLabel: 'merge-conflict'
|
|
||||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
|
||||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
|
||||||
41
.github/workflows/lint-pr.yaml
vendored
Normal file
41
.github/workflows/lint-pr.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: "Lint PR"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
id: lint_pr_title
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
message: |
|
||||||
|
Hey there and thank you for opening this pull request! 👋🏼
|
||||||
|
|
||||||
|
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
```
|
||||||
|
${{ steps.lint_pr_title.outputs.error_message }}
|
||||||
|
```
|
||||||
|
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||||
|
uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
delete: true
|
||||||
28
.github/workflows/lint.yaml
vendored
Normal file
28
.github/workflows/lint.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ develop, master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
- name: Run linting checks
|
||||||
|
run: bun run check
|
||||||
95
.github/workflows/linting.yml
vendored
95
.github/workflows/linting.yml
vendored
@@ -1,95 +0,0 @@
|
|||||||
name: 🚦 Security & Quality Gate
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, edited, synchronize, reopened]
|
|
||||||
branches: [develop, master]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
validate_pr_title:
|
|
||||||
name: "📝 Validate PR Title"
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
|
|
||||||
id: lint_pr_title
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # v2.9.2
|
|
||||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
|
||||||
with:
|
|
||||||
header: pr-title-lint-error
|
|
||||||
message: |
|
|
||||||
Hey there and thank you for opening this pull request! 👋🏼
|
|
||||||
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/).
|
|
||||||
|
|
||||||
**Error details:**
|
|
||||||
```
|
|
||||||
${{ steps.lint_pr_title.outputs.error_message }}
|
|
||||||
```
|
|
||||||
|
|
||||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
|
||||||
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # v2.9.2
|
|
||||||
with:
|
|
||||||
header: pr-title-lint-error
|
|
||||||
delete: true
|
|
||||||
|
|
||||||
dependency-review:
|
|
||||||
name: 🔍 Vulnerable Dependencies
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Dependency Review
|
|
||||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
|
||||||
with:
|
|
||||||
fail-on-severity: high
|
|
||||||
deny-licenses: GPL-3.0, AGPL-3.0
|
|
||||||
base-ref: ${{ github.event.pull_request.base.sha || 'develop' }}
|
|
||||||
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
|
||||||
|
|
||||||
code_quality:
|
|
||||||
name: "🔍 Lint & Test (${{ matrix.command }})"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
command:
|
|
||||||
- "lint"
|
|
||||||
- "check"
|
|
||||||
steps:
|
|
||||||
- name: "📥 Checkout PR code"
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
submodules: recursive
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: "🟢 Setup Node.js"
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
||||||
with:
|
|
||||||
node-version: '20.x'
|
|
||||||
|
|
||||||
- name: "🍞 Setup Bun"
|
|
||||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
|
||||||
with:
|
|
||||||
bun-version: '1.2.15'
|
|
||||||
|
|
||||||
- name: "📦 Install dependencies"
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: "🚨 Run ${{ matrix.command }}"
|
|
||||||
run: bun run ${{ matrix.command }}
|
|
||||||
39
.github/workflows/main.yml
vendored
Normal file
39
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Handle Stale Issues
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *" # Runs at 1:30 UTC every day
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
# Issue specific settings
|
||||||
|
days-before-issue-stale: 90
|
||||||
|
days-before-issue-close: 7
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
stale-issue-message: |
|
||||||
|
This issue has been automatically marked as stale because it has had no activity in the last 30 days.
|
||||||
|
|
||||||
|
If this issue is still relevant, please leave a comment to keep it open.
|
||||||
|
Otherwise, it will be closed in 7 days if no further activity occurs.
|
||||||
|
|
||||||
|
Thank you for your contributions!
|
||||||
|
close-issue-message: |
|
||||||
|
This issue has been automatically closed because it has been inactive for 7 days since being marked as stale.
|
||||||
|
|
||||||
|
If you believe this issue is still relevant, please feel free to reopen it and add a comment explaining the current status.
|
||||||
|
|
||||||
|
# Pull request settings (disabled)
|
||||||
|
days-before-pr-stale: -1
|
||||||
|
days-before-pr-close: -1
|
||||||
|
|
||||||
|
# Other settings
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
operations-per-run: 100
|
||||||
|
exempt-issue-labels: "Roadmap v1,help needed,enhancement"
|
||||||
18
.github/workflows/notification.yaml
vendored
Normal file
18
.github/workflows/notification.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
name: Discord Pull Request Notification
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: joelwmale/webhook-action@master
|
||||||
|
with:
|
||||||
|
url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
body: |
|
||||||
|
{
|
||||||
|
"content": "New Pull Request: ${{ github.event.pull_request.title }}\nBy: ${{ github.event.pull_request.user.login }}\n\n${{ github.event.pull_request.html_url }}",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/193271640"
|
||||||
|
}
|
||||||
23
.github/workflows/notification.yml
vendored
23
.github/workflows/notification.yml
vendored
@@ -1,23 +0,0 @@
|
|||||||
name: 🛎️ Discord Pull Request Notification
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened]
|
|
||||||
branches: [develop]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
notify:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- name: 🛎️ Notify Discord
|
|
||||||
uses: Ilshidur/action-discord@0c4b27844ba47cb1c7bee539c8eead5284ce9fa9 # 0.3.2
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
|
||||||
DISCORD_AVATAR: https://avatars.githubusercontent.com/u/193271640
|
|
||||||
with:
|
|
||||||
args: |
|
|
||||||
📢 New Pull Request in **${{ github.repository }}**
|
|
||||||
**Title:** ${{ github.event.pull_request.title }}
|
|
||||||
**By:** ${{ github.event.pull_request.user.login }}
|
|
||||||
**Branch:** ${{ github.event.pull_request.head.ref }}
|
|
||||||
🔗 ${{ github.event.pull_request.html_url }}
|
|
||||||
49
.github/workflows/stale.yml
vendored
49
.github/workflows/stale.yml
vendored
@@ -1,49 +0,0 @@
|
|||||||
name: 🕒 Handle Stale Issues
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Runs daily at 1:30 AM UTC (3:30 AM CEST - France time)
|
|
||||||
- cron: "30 1 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale-issues:
|
|
||||||
name: 🗑️ Cleanup Stale Issues
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 🔄 Mark/Close Stale Issues
|
|
||||||
uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
|
||||||
with:
|
|
||||||
# Global settings
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
operations-per-run: 500 # Increase if you have >1000 issues
|
|
||||||
log-level: debug
|
|
||||||
|
|
||||||
# Issue configuration
|
|
||||||
days-before-issue-stale: 90
|
|
||||||
days-before-issue-close: 7
|
|
||||||
stale-issue-label: "stale"
|
|
||||||
exempt-issue-labels: "Roadmap v1,help needed,enhancement"
|
|
||||||
|
|
||||||
# Notifications messages
|
|
||||||
stale-issue-message: |
|
|
||||||
⏳ This issue has been automatically marked as **stale** because it has had no activity for 90 days.
|
|
||||||
|
|
||||||
**Next steps:**
|
|
||||||
- If this is still relevant, add a comment to keep it open
|
|
||||||
- Otherwise, it will be closed in 7 days
|
|
||||||
|
|
||||||
Thank you for your contributions! 🙌
|
|
||||||
|
|
||||||
close-issue-message: |
|
|
||||||
🚮 This issue has been automatically closed due to inactivity (7 days since being marked stale).
|
|
||||||
|
|
||||||
**Need to reopen?**
|
|
||||||
Click "Reopen" and add a comment explaining why this should stay open.
|
|
||||||
|
|
||||||
# Disable PR handling
|
|
||||||
days-before-pr-stale: -1
|
|
||||||
days-before-pr-close: -1
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,6 +11,7 @@ npm-debug.*
|
|||||||
web-build/
|
web-build/
|
||||||
modules/vlc-player/android/build
|
modules/vlc-player/android/build
|
||||||
modules/vlc-player/android/.gradle
|
modules/vlc-player/android/.gradle
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -19,7 +20,9 @@ expo-env.d.ts
|
|||||||
|
|
||||||
Streamyfin.app
|
Streamyfin.app
|
||||||
|
|
||||||
|
build-*
|
||||||
*.mp4
|
*.mp4
|
||||||
|
build-*
|
||||||
Streamyfin.app
|
Streamyfin.app
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
@@ -45,4 +48,3 @@ modules/hls-downloader/android/build
|
|||||||
streamyfin-4fec1-firebase-adminsdk.json
|
streamyfin-4fec1-firebase-adminsdk.json
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
*.aab
|
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<a href="https://www.buymeacoffee.com/fredrikbur3" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
<a href="https://www.buymeacoffee.com/fredrikbur3" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||||
|
|
||||||
Welcome to Streamyfin, a simple and user-friendly Jellyfin video streaming client built with Expo. If you're looking for an alternative to other Jellyfin clients, we hope you'll find Streamyfin to be a useful addition to your media streaming toolbox.
|
Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Expo. If you're looking for an alternative to other Jellyfin clients, we hope you'll find Streamyfin to be a useful addition to your media streaming toolbox.
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: row; gap: 8px">
|
<div style="display: flex; flex-direction: row; gap: 8px">
|
||||||
<img width=150 src="./assets/images/screenshots/screenshot1.png" />
|
<img width=150 src="./assets/images/screenshots/screenshot1.png" />
|
||||||
@@ -15,11 +15,11 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin video streaming clien
|
|||||||
|
|
||||||
- 🚀 **Skip Intro / Credits Support**
|
- 🚀 **Skip Intro / Credits Support**
|
||||||
- 🖼️ **Trickplay images**: The new golden standard for chapter previews when seeking.
|
- 🖼️ **Trickplay images**: The new golden standard for chapter previews when seeking.
|
||||||
|
- 🔊 **Background audio**: Stream music in the background, even when locking the phone.
|
||||||
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.
|
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.
|
||||||
- 📡 **Chromecast** (Experimental): Cast your media to any Chromecast-enabled device.
|
- 📡 **Chromecast** (Experimental): Cast your media to any Chromecast-enabled device.
|
||||||
- 📡 **Settings management** (Experimental): Manage app settings for all your users with a JF plugin.
|
- 📡 **Settings management** (Experimental): Manage app settings for all your users with a JF plugin.
|
||||||
- 🤖 **Jellyseerr integration**: Request media directly in the app.
|
- 🤖 **Jellyseerr integration**: Request media directly in the app.
|
||||||
- 👁️ **Sessions View:** View all active sessions currently streaming on your server.
|
|
||||||
|
|
||||||
## 🧪 Experimental Features
|
## 🧪 Experimental Features
|
||||||
|
|
||||||
@@ -31,16 +31,16 @@ Downloading works by using ffmpeg to convert an HLS stream into a video file on
|
|||||||
|
|
||||||
### Chromecast
|
### Chromecast
|
||||||
|
|
||||||
Chromecast support is still in development, and we're working on improving it. Currently, it supports casting videos, but we're working on adding support for subtitles and other features.
|
Chromecast support is still in development, and we're working on improving it. Currently, it supports casting videos and audio, but we're working on adding support for subtitles and other features.
|
||||||
|
|
||||||
### Streamyfin Plugin
|
### Streamyfin Plugin
|
||||||
|
|
||||||
The Jellyfin Plugin for Streamyfin is a plugin you install into Jellyfin that holds all settings for the client Streamyfin. This allows you to synchronize settings across all your users, like for example:
|
The Jellyfin Plugin for Streamyfin is a plugin you install into Jellyfin that hold all settings for the client Streamyfin. This allows you to syncronize settings accross all your users, like:
|
||||||
|
|
||||||
- Auto log in to Jellyseerr without the user having to do anything
|
- Auto log in to Jellyseerr without the user having to do anythin
|
||||||
- Choose the default languages
|
- Choose the default languages
|
||||||
- Set download method and search provider
|
- Set download method and search provider
|
||||||
- Customize home screen
|
- Customize homescreen
|
||||||
- And more...
|
- And more...
|
||||||
|
|
||||||
[Streamyfin Plugin](https://github.com/streamyfin/jellyfin-plugin-streamyfin)
|
[Streamyfin Plugin](https://github.com/streamyfin/jellyfin-plugin-streamyfin)
|
||||||
@@ -66,7 +66,7 @@ Or download the APKs [here on GitHub](https://github.com/streamyfin/streamyfin/r
|
|||||||
|
|
||||||
### Beta testing
|
### Beta testing
|
||||||
|
|
||||||
To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the 🧪-public-beta channel on Discord and I'll know that you have subscribed. This is where I post APKs and IPAs. This won't give automatic access to the TestFlight, however, so you need to send me a DM with the email you use for Apple so that I can manually add you.
|
To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the 🧪-public-beta channel on Discord and i'll know that you have subscribed. This is where I post APKs and IPAs. This won't give automatic access to the TestFlight, however, so you need to send me a DM with the email you use for Apple so that i can manually add you.
|
||||||
|
|
||||||
**Note**: Everyone who is actively contributing to the source code of Streamyfin will have automatic access to the betas.
|
**Note**: Everyone who is actively contributing to the source code of Streamyfin will have automatic access to the betas.
|
||||||
|
|
||||||
@@ -118,13 +118,6 @@ If you have questions or need support, feel free to reach out:
|
|||||||
- GitHub Issues: Report bugs or request features here.
|
- GitHub Issues: Report bugs or request features here.
|
||||||
- Email: [fredrik.burmester@gmail.com](mailto:fredrik.burmester@gmail.com)
|
- Email: [fredrik.burmester@gmail.com](mailto:fredrik.burmester@gmail.com)
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
1. Q: Why can't I see my libraries in Streamyfin?
|
|
||||||
A: Make sure your server is running one of the latest versions and that you have at least one library that isn't audio only.
|
|
||||||
2. Q: Why can't I see my music library?
|
|
||||||
A: We don't currently support music and are unlikely to support music in the near future.
|
|
||||||
|
|
||||||
## 📝 Credits
|
## 📝 Credits
|
||||||
|
|
||||||
Streamyfin is developed by [Fredrik Burmester](https://github.com/fredrikburmester) and is not affiliated with Jellyfin. The app is built with Expo, React Native, and other open-source libraries.
|
Streamyfin is developed by [Fredrik Burmester](https://github.com/fredrikburmester) and is not affiliated with Jellyfin. The app is built with Expo, React Native, and other open-source libraries.
|
||||||
|
|||||||
20
app.json
20
app.json
@@ -27,19 +27,13 @@
|
|||||||
"usesNonExemptEncryption": false
|
"usesNonExemptEncryption": false
|
||||||
},
|
},
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"bundleIdentifier": "com.fredrikburmester.streamyfin",
|
"bundleIdentifier": "com.fredrikburmester.streamyfin"
|
||||||
"icon": {
|
|
||||||
"dark": "./assets/images/icon-plain.png",
|
|
||||||
"light": "./assets/images/icon-ios-light.png",
|
|
||||||
"tinted": "./assets/images/icon-ios-tinted.png"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"jsEngine": "hermes",
|
"jsEngine": "hermes",
|
||||||
"versionCode": 56,
|
"versionCode": 54,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/images/icon-plain.png",
|
"foregroundImage": "./assets/images/adaptive_icon.png",
|
||||||
"monochromeImage": "./assets/images/icon-mono.png",
|
|
||||||
"backgroundColor": "#464646"
|
"backgroundColor": "#464646"
|
||||||
},
|
},
|
||||||
"package": "com.fredrikburmester.streamyfin",
|
"package": "com.fredrikburmester.streamyfin",
|
||||||
@@ -54,6 +48,7 @@
|
|||||||
"@react-native-tvos/config-tv",
|
"@react-native-tvos/config-tv",
|
||||||
"expo-router",
|
"expo-router",
|
||||||
"expo-font",
|
"expo-font",
|
||||||
|
"@config-plugins/ffmpeg-kit-react-native",
|
||||||
[
|
[
|
||||||
"react-native-video",
|
"react-native-video",
|
||||||
{
|
{
|
||||||
@@ -118,7 +113,6 @@
|
|||||||
["./plugins/withAndroidManifest.js"],
|
["./plugins/withAndroidManifest.js"],
|
||||||
["./plugins/withTrustLocalCerts.js"],
|
["./plugins/withTrustLocalCerts.js"],
|
||||||
["./plugins/withGradleProperties.js"],
|
["./plugins/withGradleProperties.js"],
|
||||||
["./plugins/withRNBackgroundDownloader.js"],
|
|
||||||
[
|
[
|
||||||
"expo-splash-screen",
|
"expo-splash-screen",
|
||||||
{
|
{
|
||||||
@@ -133,12 +127,6 @@
|
|||||||
"icon": "./assets/images/notification.png",
|
"icon": "./assets/images/notification.png",
|
||||||
"color": "#9333EA"
|
"color": "#9333EA"
|
||||||
}
|
}
|
||||||
],
|
|
||||||
[
|
|
||||||
"react-native-google-cast",
|
|
||||||
{
|
|
||||||
"useDefaultExpandedMediaControls": true
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
|
|||||||
@@ -18,18 +18,12 @@ import {
|
|||||||
HardwareAccelerationType,
|
HardwareAccelerationType,
|
||||||
type SessionInfoDto,
|
type SessionInfoDto,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import {
|
|
||||||
GeneralCommandType,
|
|
||||||
PlaystateCommand,
|
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
|
||||||
import { FlashList } from "@shopify/flash-list";
|
import { FlashList } from "@shopify/flash-list";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { get } from "lodash";
|
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const { sessions, isLoading } = useSessions({} as useSessionsProps);
|
const { sessions, isLoading } = useSessions({} as useSessionsProps);
|
||||||
@@ -116,77 +110,6 @@ const SessionCard = ({ session }: SessionCardProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle session controls
|
|
||||||
const [isControlLoading, setIsControlLoading] = useState<
|
|
||||||
Record<string, boolean>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
const handleSystemCommand = async (command: GeneralCommandType) => {
|
|
||||||
if (!api || !session.Id) return false;
|
|
||||||
|
|
||||||
setIsControlLoading({ ...isControlLoading, [command]: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
getSessionApi(api).sendSystemCommand({
|
|
||||||
sessionId: session.Id,
|
|
||||||
command,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error sending ${command} command:`, error);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
setIsControlLoading({ ...isControlLoading, [command]: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlaystateCommand = async (command: PlaystateCommand) => {
|
|
||||||
if (!api || !session.Id) return false;
|
|
||||||
|
|
||||||
setIsControlLoading({ ...isControlLoading, [command]: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
getSessionApi(api).sendPlaystateCommand({
|
|
||||||
sessionId: session.Id,
|
|
||||||
command,
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error sending playstate ${command} command:`, error);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
setIsControlLoading({ ...isControlLoading, [command]: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlayPause = async () => {
|
|
||||||
console.log("handlePlayPause");
|
|
||||||
await handlePlaystateCommand(PlaystateCommand.PlayPause);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStop = async () => {
|
|
||||||
await handlePlaystateCommand(PlaystateCommand.Stop);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrevious = async () => {
|
|
||||||
await handlePlaystateCommand(PlaystateCommand.PreviousTrack);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNext = async () => {
|
|
||||||
await handlePlaystateCommand(PlaystateCommand.NextTrack);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleMute = async () => {
|
|
||||||
await handleSystemCommand(GeneralCommandType.ToggleMute);
|
|
||||||
};
|
|
||||||
const handleVolumeUp = async () => {
|
|
||||||
await handleSystemCommand(GeneralCommandType.VolumeUp);
|
|
||||||
};
|
|
||||||
const handleVolumeDown = async () => {
|
|
||||||
await handleSystemCommand(GeneralCommandType.VolumeDown);
|
|
||||||
};
|
|
||||||
|
|
||||||
useInterval(tick, 1000);
|
useInterval(tick, 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -258,107 +181,6 @@ const SessionCard = ({ session }: SessionCardProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Session controls */}
|
|
||||||
<View className='flex flex-row mt-2 space-x-4 justify-center'>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={handlePrevious}
|
|
||||||
disabled={isControlLoading[PlaystateCommand.PreviousTrack]}
|
|
||||||
style={{
|
|
||||||
opacity: isControlLoading[PlaystateCommand.PreviousTrack]
|
|
||||||
? 0.5
|
|
||||||
: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name='skip-previous'
|
|
||||||
size={24}
|
|
||||||
color='white'
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={handlePlayPause}
|
|
||||||
disabled={isControlLoading[PlaystateCommand.PlayPause]}
|
|
||||||
style={{
|
|
||||||
opacity: isControlLoading[PlaystateCommand.PlayPause]
|
|
||||||
? 0.5
|
|
||||||
: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{session.PlayState?.IsPaused ? (
|
|
||||||
<Ionicons name='play' size={24} color='white' />
|
|
||||||
) : (
|
|
||||||
<Ionicons name='pause' size={24} color='white' />
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={handleStop}
|
|
||||||
disabled={isControlLoading[PlaystateCommand.Stop]}
|
|
||||||
style={{
|
|
||||||
opacity: isControlLoading[PlaystateCommand.Stop] ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons name='stop' size={24} color='white' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={handleNext}
|
|
||||||
disabled={isControlLoading[PlaystateCommand.NextTrack]}
|
|
||||||
style={{
|
|
||||||
opacity: isControlLoading[PlaystateCommand.NextTrack]
|
|
||||||
? 0.5
|
|
||||||
: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name='skip-next'
|
|
||||||
size={24}
|
|
||||||
color='white'
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={handleVolumeDown}
|
|
||||||
disabled={isControlLoading[GeneralCommandType.VolumeDown]}
|
|
||||||
style={{
|
|
||||||
opacity: isControlLoading[GeneralCommandType.VolumeDown]
|
|
||||||
? 0.5
|
|
||||||
: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons name='volume-low' size={24} color='white' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={handleToggleMute}
|
|
||||||
disabled={isControlLoading[GeneralCommandType.ToggleMute]}
|
|
||||||
style={{
|
|
||||||
opacity: isControlLoading[GeneralCommandType.ToggleMute]
|
|
||||||
? 0.5
|
|
||||||
: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name='volume-mute'
|
|
||||||
size={24}
|
|
||||||
color={session.PlayState?.IsMuted ? "red" : "white"}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={handleVolumeUp}
|
|
||||||
disabled={isControlLoading[GeneralCommandType.VolumeUp]}
|
|
||||||
style={{
|
|
||||||
opacity: isControlLoading[GeneralCommandType.VolumeUp]
|
|
||||||
? 0.5
|
|
||||||
: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons name='volume-high' size={24} color='white' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useLocalSearchParams, useNavigation, useRouter } from "expo-router";
|
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -46,7 +46,6 @@ const Page: React.FC = () => {
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } =
|
const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } =
|
||||||
params as unknown as {
|
params as unknown as {
|
||||||
@@ -237,65 +236,30 @@ const Page: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View className='mb-4'>
|
||||||
<GenreTags genres={details?.genres?.map((g) => g.name) || []} />
|
<GenreTags genres={details?.genres?.map((g) => g.name) || []} />
|
||||||
</View>
|
</View>
|
||||||
{isLoading || isFetching ? (
|
{isLoading || isFetching ? (
|
||||||
<Button
|
<Button loading={true} disabled={true} color='purple' />
|
||||||
loading={true}
|
|
||||||
disabled={true}
|
|
||||||
color='purple'
|
|
||||||
className='mt-4'
|
|
||||||
/>
|
|
||||||
) : canRequest ? (
|
) : canRequest ? (
|
||||||
<Button color='purple' onPress={request} className='mt-4'>
|
<Button color='purple' onPress={request}>
|
||||||
{t("jellyseerr.request_button")}
|
{t("jellyseerr.request_button")}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
details?.mediaInfo?.jellyfinMediaId && (
|
<Button
|
||||||
<View className='flex flex-row space-x-2 mt-4'>
|
className='bg-yellow-500/50 border-yellow-400 ring-yellow-400 text-yellow-100'
|
||||||
<Button
|
color='transparent'
|
||||||
className='flex-1 bg-yellow-500/50 border-yellow-400 ring-yellow-400 text-yellow-100'
|
onPress={() => bottomSheetModalRef?.current?.present()}
|
||||||
color='transparent'
|
iconLeft={
|
||||||
onPress={() => bottomSheetModalRef?.current?.present()}
|
<Ionicons name='warning-outline' size={24} color='white' />
|
||||||
iconLeft={
|
}
|
||||||
<Ionicons
|
style={{
|
||||||
name='warning-outline'
|
borderWidth: 1,
|
||||||
size={20}
|
borderStyle: "solid",
|
||||||
color='white'
|
}}
|
||||||
/>
|
>
|
||||||
}
|
{t("jellyseerr.report_issue_button")}
|
||||||
style={{
|
</Button>
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text className='text-sm'>
|
|
||||||
{t("jellyseerr.report_issue_button")}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className='flex-1 bg-purple-600/50 border-purple-400 ring-purple-400 text-purple-100'
|
|
||||||
onPress={() => {
|
|
||||||
const url =
|
|
||||||
mediaType === MediaType.MOVIE
|
|
||||||
? `/(auth)/(tabs)/(search)/items/page?id=${details?.mediaInfo.jellyfinMediaId}`
|
|
||||||
: `/(auth)/(tabs)/(search)/series/${details?.mediaInfo.jellyfinMediaId}`;
|
|
||||||
// @ts-expect-error
|
|
||||||
router.push(url);
|
|
||||||
}}
|
|
||||||
iconLeft={
|
|
||||||
<Ionicons name='play-outline' size={20} color='white' />
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text className='text-sm'>Play</Text>
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
<OverviewText text={result.overview} className='mt-4' />
|
<OverviewText text={result.overview} className='mt-4' />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -367,15 +367,7 @@ const Page = () => {
|
|||||||
className='mr-1'
|
className='mr-1'
|
||||||
id={libraryId}
|
id={libraryId}
|
||||||
queryKey='sortBy'
|
queryKey='sortBy'
|
||||||
queryFn={async () =>
|
queryFn={async () => sortOptions.map((s) => s.key)}
|
||||||
sortOptions
|
|
||||||
.filter(
|
|
||||||
(s) =>
|
|
||||||
library?.CollectionType !== "movies" ||
|
|
||||||
s.key !== SortByOption.DateLastContentAdded,
|
|
||||||
)
|
|
||||||
.map((s) => s.key)
|
|
||||||
}
|
|
||||||
set={setSortBy}
|
set={setSortBy}
|
||||||
values={sortBy}
|
values={sortBy}
|
||||||
title={t("library.filters.sort_by")}
|
title={t("library.filters.sort_by")}
|
||||||
@@ -441,6 +433,15 @@ const Page = () => {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (flatData.length === 0)
|
||||||
|
return (
|
||||||
|
<View className='h-full w-full flex justify-center items-center'>
|
||||||
|
<Text className='text-lg text-neutral-500'>
|
||||||
|
{t("library.no_items_found")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
key={orientation}
|
key={orientation}
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ export default function search() {
|
|||||||
<View className={l1 || l2 ? "opacity-0" : "opacity-100"}>
|
<View className={l1 || l2 ? "opacity-0" : "opacity-100"}>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
header={t("search.movies")}
|
header={t("search.movies")}
|
||||||
items={movies}
|
ids={movies?.map((m) => m.Id!)}
|
||||||
renderItem={(item: BaseItemDto) => (
|
renderItem={(item: BaseItemDto) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
key={item.Id}
|
key={item.Id}
|
||||||
@@ -349,7 +349,7 @@ export default function search() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
items={series}
|
ids={series?.map((m) => m.Id!)}
|
||||||
header={t("search.series")}
|
header={t("search.series")}
|
||||||
renderItem={(item: BaseItemDto) => (
|
renderItem={(item: BaseItemDto) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
@@ -368,7 +368,7 @@ export default function search() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
items={episodes}
|
ids={episodes?.map((m) => m.Id!)}
|
||||||
header={t("search.episodes")}
|
header={t("search.episodes")}
|
||||||
renderItem={(item: BaseItemDto) => (
|
renderItem={(item: BaseItemDto) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
@@ -382,7 +382,7 @@ export default function search() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
items={collections}
|
ids={collections?.map((m) => m.Id!)}
|
||||||
header={t("search.collections")}
|
header={t("search.collections")}
|
||||||
renderItem={(item: BaseItemDto) => (
|
renderItem={(item: BaseItemDto) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
@@ -398,7 +398,7 @@ export default function search() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
items={actors}
|
ids={actors?.map((m) => m.Id!)}
|
||||||
header={t("search.actors")}
|
header={t("search.actors")}
|
||||||
renderItem={(item: BaseItemDto) => (
|
renderItem={(item: BaseItemDto) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
@@ -434,10 +434,7 @@ export default function search() {
|
|||||||
<View className='mt-4 flex flex-col items-center space-y-2'>
|
<View className='mt-4 flex flex-col items-center space-y-2'>
|
||||||
{exampleSearches.map((e) => (
|
{exampleSearches.map((e) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => setSearch(e)}
|
||||||
setSearch(e);
|
|
||||||
searchBarRef.current?.setText(e);
|
|
||||||
}}
|
|
||||||
key={e}
|
key={e}
|
||||||
className='mb-2'
|
className='mb-2'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import generateDeviceProfile from "@/utils/profiles/native";
|
import native from "@/utils/profiles/native";
|
||||||
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
||||||
import {
|
import {
|
||||||
type BaseItemDto,
|
type BaseItemDto,
|
||||||
@@ -60,7 +60,6 @@ export default function page() {
|
|||||||
const [showControls, _setShowControls] = useState(true);
|
const [showControls, _setShowControls] = useState(true);
|
||||||
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
|
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
|
||||||
const [isBuffering, setIsBuffering] = useState(true);
|
const [isBuffering, setIsBuffering] = useState(true);
|
||||||
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
||||||
const [isPipStarted, setIsPipStarted] = useState(false);
|
const [isPipStarted, setIsPipStarted] = useState(false);
|
||||||
@@ -68,10 +67,6 @@ export default function page() {
|
|||||||
const progress = useSharedValue(0);
|
const progress = useSharedValue(0);
|
||||||
const isSeeking = useSharedValue(false);
|
const isSeeking = useSharedValue(false);
|
||||||
const cacheProgress = useSharedValue(0);
|
const cacheProgress = useSharedValue(0);
|
||||||
const VolumeManager = Platform.isTV
|
|
||||||
? null
|
|
||||||
: require("react-native-volume-manager");
|
|
||||||
|
|
||||||
let getDownloadedItem = null;
|
let getDownloadedItem = null;
|
||||||
if (!Platform.isTV) {
|
if (!Platform.isTV) {
|
||||||
getDownloadedItem = downloadProvider.useDownload();
|
getDownloadedItem = downloadProvider.useDownload();
|
||||||
@@ -137,10 +132,11 @@ export default function page() {
|
|||||||
fetchedItem = res.data;
|
fetchedItem = res.data;
|
||||||
}
|
}
|
||||||
setItem(fetchedItem);
|
setItem(fetchedItem);
|
||||||
setItemStatus({ isLoading: false, isError: false });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch item:", error);
|
console.error("Failed to fetch item:", error);
|
||||||
setItemStatus({ isLoading: false, isError: true });
|
setItemStatus({ isLoading: false, isError: true });
|
||||||
|
} finally {
|
||||||
|
setItemStatus({ isLoading: false, isError: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -163,8 +159,6 @@ export default function page() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStreamData = async () => {
|
const fetchStreamData = async () => {
|
||||||
setStreamStatus({ isLoading: true, isError: false });
|
|
||||||
const native = await generateDeviceProfile();
|
|
||||||
try {
|
try {
|
||||||
let result: Stream | null = null;
|
let result: Stream | null = null;
|
||||||
if (offline && !Platform.isTV) {
|
if (offline && !Platform.isTV) {
|
||||||
@@ -198,10 +192,11 @@ export default function page() {
|
|||||||
result = { mediaSource, sessionId, url };
|
result = { mediaSource, sessionId, url };
|
||||||
}
|
}
|
||||||
setStream(result);
|
setStream(result);
|
||||||
setStreamStatus({ isLoading: false, isError: false });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch stream:", error);
|
console.error("Failed to fetch stream:", error);
|
||||||
setStreamStatus({ isLoading: false, isError: true });
|
setStreamStatus({ isLoading: false, isError: true });
|
||||||
|
} finally {
|
||||||
|
setStreamStatus({ isLoading: false, isError: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchStreamData();
|
fetchStreamData();
|
||||||
@@ -224,7 +219,7 @@ export default function page() {
|
|||||||
setIsPlaying(!isPlaying);
|
setIsPlaying(!isPlaying);
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
await videoRef.current?.pause();
|
await videoRef.current?.pause();
|
||||||
reportPlaybackProgress();
|
reportPlaybackStopped();
|
||||||
} else {
|
} else {
|
||||||
videoRef.current?.play();
|
videoRef.current?.play();
|
||||||
await getPlaystateApi(api!).reportPlaybackStart({
|
await getPlaystateApi(api!).reportPlaybackStart({
|
||||||
@@ -244,15 +239,7 @@ export default function page() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
revalidateProgressCache();
|
revalidateProgressCache();
|
||||||
}, [
|
}, [api, item, mediaSourceId, stream]);
|
||||||
api,
|
|
||||||
item,
|
|
||||||
mediaSourceId,
|
|
||||||
stream,
|
|
||||||
progress,
|
|
||||||
offline,
|
|
||||||
revalidateProgressCache,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
const stop = useCallback(() => {
|
||||||
reportPlaybackStopped();
|
reportPlaybackStopped();
|
||||||
@@ -278,7 +265,7 @@ export default function page() {
|
|||||||
isPaused: !isPlaying,
|
isPaused: !isPlaying,
|
||||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||||
playSessionId: stream.sessionId,
|
playSessionId: stream.sessionId,
|
||||||
isMuted: isMuted,
|
isMuted: false,
|
||||||
canSeek: true,
|
canSeek: true,
|
||||||
repeatMode: RepeatMode.RepeatNone,
|
repeatMode: RepeatMode.RepeatNone,
|
||||||
playbackOrder: PlaybackOrder.Default,
|
playbackOrder: PlaybackOrder.Default,
|
||||||
@@ -342,84 +329,13 @@ export default function page() {
|
|||||||
return item?.UserData?.PlaybackPositionTicks
|
return item?.UserData?.PlaybackPositionTicks
|
||||||
? ticksToSeconds(item.UserData.PlaybackPositionTicks)
|
? ticksToSeconds(item.UserData.PlaybackPositionTicks)
|
||||||
: 0;
|
: 0;
|
||||||
}, [item, offline]);
|
}, [item]);
|
||||||
|
|
||||||
const volumeUpCb = useCallback(async () => {
|
|
||||||
if (Platform.isTV) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { volume: currentVolume } = await VolumeManager.getVolume();
|
|
||||||
const newVolume = Math.min(currentVolume + 0.1, 1.0);
|
|
||||||
|
|
||||||
await VolumeManager.setVolume(newVolume);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error adjusting volume:", error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
const [previousVolume, setPreviousVolume] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const toggleMuteCb = useCallback(async () => {
|
|
||||||
if (Platform.isTV) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { volume: currentVolume } = await VolumeManager.getVolume();
|
|
||||||
const currentVolumePercent = currentVolume * 100;
|
|
||||||
|
|
||||||
if (currentVolumePercent > 0) {
|
|
||||||
// Currently not muted, so mute
|
|
||||||
setPreviousVolume(currentVolumePercent);
|
|
||||||
await VolumeManager.setVolume(0);
|
|
||||||
setIsMuted(true);
|
|
||||||
} else {
|
|
||||||
// Currently muted, so restore previous volume
|
|
||||||
const volumeToRestore = previousVolume || 50; // Default to 50% if no previous volume
|
|
||||||
await VolumeManager.setVolume(volumeToRestore / 100);
|
|
||||||
setPreviousVolume(null);
|
|
||||||
setIsMuted(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error toggling mute:", error);
|
|
||||||
}
|
|
||||||
}, [previousVolume]);
|
|
||||||
const volumeDownCb = useCallback(async () => {
|
|
||||||
if (Platform.isTV) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { volume: currentVolume } = await VolumeManager.getVolume();
|
|
||||||
const newVolume = Math.max(currentVolume - 0.1, 0); // Decrease by 10%
|
|
||||||
console.log(
|
|
||||||
"Volume Down",
|
|
||||||
Math.round(currentVolume * 100),
|
|
||||||
"→",
|
|
||||||
Math.round(newVolume * 100),
|
|
||||||
);
|
|
||||||
await VolumeManager.setVolume(newVolume);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error adjusting volume:", error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setVolumeCb = useCallback(async (newVolume: number) => {
|
|
||||||
if (Platform.isTV) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const clampedVolume = Math.max(0, Math.min(newVolume, 100));
|
|
||||||
console.log("Setting volume to", clampedVolume);
|
|
||||||
await VolumeManager.setVolume(clampedVolume / 100);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error setting volume:", error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useWebSocket({
|
useWebSocket({
|
||||||
isPlaying: isPlaying,
|
isPlaying: isPlaying,
|
||||||
togglePlay: togglePlay,
|
togglePlay: togglePlay,
|
||||||
stopPlayback: stop,
|
stopPlayback: stop,
|
||||||
offline,
|
offline,
|
||||||
toggleMute: toggleMuteCb,
|
|
||||||
volumeUp: volumeUpCb,
|
|
||||||
volumeDown: volumeDownCb,
|
|
||||||
setVolume: setVolumeCb,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const onPlaybackStateChanged = useCallback(
|
const onPlaybackStateChanged = useCallback(
|
||||||
@@ -506,7 +422,7 @@ export default function page() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemStatus.isError || streamStatus.isError)
|
if (!item || !stream || itemStatus.isError || streamStatus.isError)
|
||||||
return (
|
return (
|
||||||
<View className='w-screen h-screen flex flex-col items-center justify-center bg-black'>
|
<View className='w-screen h-screen flex flex-col items-center justify-center bg-black'>
|
||||||
<Text className='text-white'>{t("player.error")}</Text>
|
<Text className='text-white'>{t("player.error")}</Text>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 305 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 136 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 326 KiB |
89
bun.lock
89
bun.lock
@@ -5,8 +5,9 @@
|
|||||||
"name": "streamyfin",
|
"name": "streamyfin",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bottom-tabs/react-navigation": "0.8.6",
|
"@bottom-tabs/react-navigation": "0.8.6",
|
||||||
|
"@config-plugins/ffmpeg-kit-react-native": "^9.0.0",
|
||||||
"@expo/config-plugins": "~9.0.15",
|
"@expo/config-plugins": "~9.0.15",
|
||||||
"@expo/react-native-action-sheet": "^4.1.1",
|
"@expo/react-native-action-sheet": "^4.1.0",
|
||||||
"@expo/vector-icons": "^14.0.4",
|
"@expo/vector-icons": "^14.0.4",
|
||||||
"@futurejj/react-native-visibility-sensor": "^1.3.10",
|
"@futurejj/react-native-visibility-sensor": "^1.3.10",
|
||||||
"@gorhom/bottom-sheet": "^5.1.0",
|
"@gorhom/bottom-sheet": "^5.1.0",
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
"expo-task-manager": "~12.0.5",
|
"expo-task-manager": "~12.0.5",
|
||||||
"expo-updates": "~0.26.17",
|
"expo-updates": "~0.26.17",
|
||||||
"expo-web-browser": "~14.0.2",
|
"expo-web-browser": "~14.0.2",
|
||||||
|
"ffmpeg-kit-react-native": "^6.0.2",
|
||||||
"i18next": "^24.2.2",
|
"i18next": "^24.2.2",
|
||||||
"jotai": "^2.11.3",
|
"jotai": "^2.11.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -57,7 +59,7 @@
|
|||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-native": "npm:react-native-tvos@~0.77.2-0",
|
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
"react-native-bottom-tabs": "0.8.6",
|
"react-native-bottom-tabs": "0.8.6",
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
@@ -107,7 +109,6 @@
|
|||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"@types/react-test-renderer": "^19.0.0",
|
"@types/react-test-renderer": "^19.0.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^15.5.0",
|
"lint-staged": "^15.5.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
@@ -399,6 +400,8 @@
|
|||||||
|
|
||||||
"@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.8.6", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-hLlyBAUz4ahaVK2Op2VcJeAkCSpm3KKho4IojkPyXsos4WEHtO44EYWC71TDbVGeOP5HQ9k7FSwAW3IiZs0wHw=="],
|
"@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.8.6", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-hLlyBAUz4ahaVK2Op2VcJeAkCSpm3KKho4IojkPyXsos4WEHtO44EYWC71TDbVGeOP5HQ9k7FSwAW3IiZs0wHw=="],
|
||||||
|
|
||||||
|
"@config-plugins/ffmpeg-kit-react-native": ["@config-plugins/ffmpeg-kit-react-native@9.0.0", "", { "dependencies": { "semver": "^7.3.5" }, "peerDependencies": { "expo": "^52" } }, "sha512-04bXwdq7pmUPoGqYV0YGsrW/8Db+TNicn2Hznb5t+Dl740z9QkNGP4A38y1Mdz7mCU2EW0riASwl/JTH+6rBvw=="],
|
||||||
|
|
||||||
"@dominicstop/ts-event-emitter": ["@dominicstop/ts-event-emitter@1.1.0", "", {}, "sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw=="],
|
"@dominicstop/ts-event-emitter": ["@dominicstop/ts-event-emitter@1.1.0", "", {}, "sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw=="],
|
||||||
|
|
||||||
"@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="],
|
"@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="],
|
||||||
@@ -437,7 +440,7 @@
|
|||||||
|
|
||||||
"@expo/prebuild-config": ["@expo/prebuild-config@8.0.28", "", { "dependencies": { "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/config-types": "^52.0.4", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@react-native/normalize-colors": "0.76.7", "debug": "^4.3.1", "fs-extra": "^9.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-SDDgCKKS1wFNNm3de2vBP8Q5bnxcabuPDE9Mnk9p7Gb4qBavhwMbAtrLcAyZB+WRb4QM+yan3z3K95vvCfI/+A=="],
|
"@expo/prebuild-config": ["@expo/prebuild-config@8.0.28", "", { "dependencies": { "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/config-types": "^52.0.4", "@expo/image-utils": "^0.6.5", "@expo/json-file": "^9.0.2", "@react-native/normalize-colors": "0.76.7", "debug": "^4.3.1", "fs-extra": "^9.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-SDDgCKKS1wFNNm3de2vBP8Q5bnxcabuPDE9Mnk9p7Gb4qBavhwMbAtrLcAyZB+WRb4QM+yan3z3K95vvCfI/+A=="],
|
||||||
|
|
||||||
"@expo/react-native-action-sheet": ["@expo/react-native-action-sheet@4.1.1", "", { "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "react": ">=18.0.0" } }, "sha512-4KRaba2vhqDRR7ObBj6nrD5uJw8ePoNHdIOMETTpgGTX7StUbrF4j/sfrP1YUyaPEa1P8FXdwG6pB+2WtrJd1A=="],
|
"@expo/react-native-action-sheet": ["@expo/react-native-action-sheet@4.1.0", "", { "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "react": ">=18.0.0" } }, "sha512-RILoWhREgjMdr1NUSmZa/cHg8onV2YPDAMOy0iIP1c3H7nT9QQZf5dQNHK8ehcLM82sarVxriBJyYSSHAx7j6w=="],
|
||||||
|
|
||||||
"@expo/rudder-sdk-node": ["@expo/rudder-sdk-node@1.1.1", "", { "dependencies": { "@expo/bunyan": "^4.0.0", "@segment/loosely-validate-event": "^2.0.0", "fetch-retry": "^4.1.1", "md5": "^2.2.1", "node-fetch": "^2.6.1", "remove-trailing-slash": "^0.1.0", "uuid": "^8.3.2" } }, "sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ=="],
|
"@expo/rudder-sdk-node": ["@expo/rudder-sdk-node@1.1.1", "", { "dependencies": { "@expo/bunyan": "^4.0.0", "@segment/loosely-validate-event": "^2.0.0", "fetch-retry": "^4.1.1", "md5": "^2.2.1", "node-fetch": "^2.6.1", "remove-trailing-slash": "^0.1.0", "uuid": "^8.3.2" } }, "sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ=="],
|
||||||
|
|
||||||
@@ -625,27 +628,27 @@
|
|||||||
|
|
||||||
"@react-native-tvos/config-tv": ["@react-native-tvos/config-tv@0.1.1", "", { "dependencies": { "getenv": "^1.0.0" }, "peerDependencies": { "expo": "^52" } }, "sha512-Le/5wGElcNarDcoafCbvk/HMxcG3s0/468xXMWqAsOtBhGAdGtyXtjWEgp/uEr4GgZJlEIdM3ZqiuB8P7p8sjw=="],
|
"@react-native-tvos/config-tv": ["@react-native-tvos/config-tv@0.1.1", "", { "dependencies": { "getenv": "^1.0.0" }, "peerDependencies": { "expo": "^52" } }, "sha512-Le/5wGElcNarDcoafCbvk/HMxcG3s0/468xXMWqAsOtBhGAdGtyXtjWEgp/uEr4GgZJlEIdM3ZqiuB8P7p8sjw=="],
|
||||||
|
|
||||||
"@react-native-tvos/virtualized-lists": ["@react-native-tvos/virtualized-lists@0.77.2-0", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^18.2.6", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-9l51YsjgrUv6f3Q8bmQPIPRuID6gLfc29CjLLQ3+RIeHFF1xzT/xwOp0+s7JMhDdZOZ5mcn9RiN7BbmcPej08A=="],
|
"@react-native-tvos/virtualized-lists": ["@react-native-tvos/virtualized-lists@0.77.0-0", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^18.2.6", "react": "*", "react-native-tvos": "*" }, "optionalPeers": ["@types/react"] }, "sha512-em0PMjOD8XQvlygbFoNT4R76rSIRxekZ9TL6EbTIC/kJUDrSPB3W9RafA6n6p4OLoWgEF7MIJ9W+zfibdiVXbw=="],
|
||||||
|
|
||||||
"@react-native/assets-registry": ["@react-native/assets-registry@0.77.2", "", {}, "sha512-AcEhFjndzBWVVhaHaASk36vhA83iDVkQbFYb0D0vATzjuJ67vhhHVLae0+JtHl5jhghotUFDg4Vj/1QbZNDyyQ=="],
|
"@react-native/assets-registry": ["@react-native/assets-registry@0.77.0", "", {}, "sha512-Ms4tYYAMScgINAXIhE4riCFJPPL/yltughHS950l0VP5sm5glbimn9n7RFn9Tc8cipX74/ddbk19+ydK2iDMmA=="],
|
||||||
|
|
||||||
"@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.76.7", "", { "dependencies": { "@react-native/codegen": "0.76.7" } }, "sha512-+8H4DXJREM4l/pwLF/wSVMRzVhzhGDix5jLezNrMD9J1U1AMfV2aSkWA1XuqR7pjPs/Vqf6TaPL7vJMZ4LU05Q=="],
|
"@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.76.7", "", { "dependencies": { "@react-native/codegen": "0.76.7" } }, "sha512-+8H4DXJREM4l/pwLF/wSVMRzVhzhGDix5jLezNrMD9J1U1AMfV2aSkWA1XuqR7pjPs/Vqf6TaPL7vJMZ4LU05Q=="],
|
||||||
|
|
||||||
"@react-native/babel-preset": ["@react-native/babel-preset@0.76.7", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.76.7", "babel-plugin-syntax-hermes-parser": "^0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-/c5DYZ6y8tyg+g8tgXKndDT7mWnGmkZ9F+T3qNDfoE3Qh7ucrNeC2XWvU9h5pk8eRtj9l4SzF4aO1phzwoibyg=="],
|
"@react-native/babel-preset": ["@react-native/babel-preset@0.76.7", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.76.7", "babel-plugin-syntax-hermes-parser": "^0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-/c5DYZ6y8tyg+g8tgXKndDT7mWnGmkZ9F+T3qNDfoE3Qh7ucrNeC2XWvU9h5pk8eRtj9l4SzF4aO1phzwoibyg=="],
|
||||||
|
|
||||||
"@react-native/codegen": ["@react-native/codegen@0.77.2", "", { "dependencies": { "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", "jscodeshift": "^17.0.0", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" } }, "sha512-uJSGm9Sp9K5XAhb17cty6iOc2lZpORQKMpS61/B3gYwe9LNz9TJpcfq1L2+3Mv6lppqsulOH9+fslapo0OTfSQ=="],
|
"@react-native/codegen": ["@react-native/codegen@0.77.0", "", { "dependencies": { "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", "jscodeshift": "^17.0.0", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" } }, "sha512-rE9lXx41ZjvE8cG7e62y/yGqzUpxnSvJ6me6axiX+aDewmI4ZrddvRGYyxCnawxy5dIBHSnrpZse3P87/4Lm7w=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.77.2", "", { "dependencies": { "@react-native/dev-middleware": "0.77.2", "@react-native/metro-babel-transformer": "0.77.2", "chalk": "^4.0.0", "debug": "^2.2.0", "invariant": "^2.2.4", "metro": "^0.81.3", "metro-config": "^0.81.3", "metro-core": "^0.81.3", "readline": "^1.3.0", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*" }, "optionalPeers": ["@react-native-community/cli"] }, "sha512-Dc93eXHhzhnRy+vF3wOdM8C4dplLpT7ItpUpYrDeA1ffHUImwWpcupB6vpX9+l3UaaJ1cPfdxTjB2d1ACVKOaA=="],
|
"@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.77.0", "", { "dependencies": { "@react-native/dev-middleware": "0.77.0", "@react-native/metro-babel-transformer": "0.77.0", "chalk": "^4.0.0", "debug": "^2.2.0", "invariant": "^2.2.4", "metro": "^0.81.0", "metro-config": "^0.81.0", "metro-core": "^0.81.0", "readline": "^1.3.0", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli-server-api": "*" }, "optionalPeers": ["@react-native-community/cli-server-api"] }, "sha512-GRshwhCHhtupa3yyCbel14SlQligV8ffNYN5L1f8HCo2SeGPsBDNjhj2U+JTrMPnoqpwowPGvkCwyqwqYff4MQ=="],
|
||||||
|
|
||||||
"@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.76.7", "", {}, "sha512-89ZtZXt7ZxE94i7T94qzZMhp4Gfcpr/QVpGqEaejAxZD+gvDCH21cYSF+/Rz2ttBazm0rk5MZ0mFqb0Iqp1jmw=="],
|
"@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.76.7", "", {}, "sha512-89ZtZXt7ZxE94i7T94qzZMhp4Gfcpr/QVpGqEaejAxZD+gvDCH21cYSF+/Rz2ttBazm0rk5MZ0mFqb0Iqp1jmw=="],
|
||||||
|
|
||||||
"@react-native/dev-middleware": ["@react-native/dev-middleware@0.76.7", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.76.7", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "selfsigned": "^2.4.1", "serve-static": "^1.13.1", "ws": "^6.2.3" } }, "sha512-Jsw8g9DyLPnR9yHEGuT09yHZ7M88/GL9CtU9WmyChlBwdXSeE3AmRqLegsV3XcgULQ1fqdemokaOZ/MwLYkjdA=="],
|
"@react-native/dev-middleware": ["@react-native/dev-middleware@0.76.7", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.76.7", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "selfsigned": "^2.4.1", "serve-static": "^1.13.1", "ws": "^6.2.3" } }, "sha512-Jsw8g9DyLPnR9yHEGuT09yHZ7M88/GL9CtU9WmyChlBwdXSeE3AmRqLegsV3XcgULQ1fqdemokaOZ/MwLYkjdA=="],
|
||||||
|
|
||||||
"@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.77.2", "", {}, "sha512-M3kU6xnn/06CGdezd31wn64v/BuKdw19K3GjOcRe1L+zKYEeezRovEVgzCNsXLcNtXUfJvmrIN4uYnqmgrJGfg=="],
|
"@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.77.0", "", {}, "sha512-rmfh93jzbndSq7kihYHUQ/EGHTP8CCd3GDCmg5SbxSOHAaAYx2HZ28ZG7AVcGUsWeXp+e/90zGIyfOzDRx0Zaw=="],
|
||||||
|
|
||||||
"@react-native/js-polyfills": ["@react-native/js-polyfills@0.77.2", "", {}, "sha512-qwKeYqRANL8CKzeVWOdhRZJ7LBqqoiXR+cb5yGwVKQxqesrx5Y7gYyq6GP1zRMnhv9iQAY7Rwub8TvDxi2YP6Q=="],
|
"@react-native/js-polyfills": ["@react-native/js-polyfills@0.77.0", "", {}, "sha512-kHFcMJVkGb3ptj3yg1soUsMHATqal4dh0QTGAbYihngJ6zy+TnP65J3GJq4UlwqFE9K1RZkeCmTwlmyPFHOGvA=="],
|
||||||
|
|
||||||
"@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.77.2", "", { "dependencies": { "@babel/core": "^7.25.2", "@react-native/babel-preset": "0.77.2", "hermes-parser": "0.25.1", "nullthrows": "^1.1.1" } }, "sha512-vSG1/d5peUo50aqaBbNnVGE5QxQTSY3j0OWmixfJqiX11wwO3tR2niKxH8OjB3WuSsROgJzosMe9kMsQJQ3ONA=="],
|
"@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.77.0", "", { "dependencies": { "@babel/core": "^7.25.2", "@react-native/babel-preset": "0.77.0", "hermes-parser": "0.25.1", "nullthrows": "^1.1.1" } }, "sha512-19GfvhBRKCU3UDWwCnDR4QjIzz3B2ZuwhnxMRwfAgPxz7QY9uKour9RGmBAVUk1Wxi/SP7dLEvWnmnuBO39e2A=="],
|
||||||
|
|
||||||
"@react-native/normalize-colors": ["@react-native/normalize-colors@0.76.7", "", {}, "sha512-ST1xxBuYVIXPdD81dR6+tzIgso7m3pa9+6rOBXTh5Xm7KEEFik7tnQX+GydXYMp3wr1gagJjragdXkPnxK6WNg=="],
|
"@react-native/normalize-colors": ["@react-native/normalize-colors@0.76.7", "", {}, "sha512-ST1xxBuYVIXPdD81dR6+tzIgso7m3pa9+6rOBXTh5Xm7KEEFik7tnQX+GydXYMp3wr1gagJjragdXkPnxK6WNg=="],
|
||||||
|
|
||||||
@@ -985,8 +988,6 @@
|
|||||||
|
|
||||||
"cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
|
"cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
|
||||||
|
|
||||||
"cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="],
|
|
||||||
|
|
||||||
"cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="],
|
"cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
@@ -1249,6 +1250,8 @@
|
|||||||
|
|
||||||
"fetch-retry": ["fetch-retry@4.1.1", "", {}, "sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA=="],
|
"fetch-retry": ["fetch-retry@4.1.1", "", {}, "sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA=="],
|
||||||
|
|
||||||
|
"ffmpeg-kit-react-native": ["ffmpeg-kit-react-native@6.0.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-r9uSmahq8TeyIb7fXf3ft+uUXyoeWRFa99+khjo0TAzWO9y0z9wU7eGnab9JLw1MmCB9v64o4yojNluJhVm9nQ=="],
|
||||||
|
|
||||||
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
|
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
|
||||||
|
|
||||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
@@ -1577,33 +1580,33 @@
|
|||||||
|
|
||||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
|
|
||||||
"metro": ["metro@0.81.5", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^2.2.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.25.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.81.5", "metro-cache": "0.81.5", "metro-cache-key": "0.81.5", "metro-config": "0.81.5", "metro-core": "0.81.5", "metro-file-map": "0.81.5", "metro-resolver": "0.81.5", "metro-runtime": "0.81.5", "metro-source-map": "0.81.5", "metro-symbolicate": "0.81.5", "metro-transform-plugins": "0.81.5", "metro-transform-worker": "0.81.5", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-YpFF0DDDpDVygeca2mAn7K0+us+XKmiGk4rIYMz/CRdjFoCGqAei/IQSpV0UrGfQbToSugpMQeQJveaWSH88Hg=="],
|
"metro": ["metro@0.81.1", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^2.2.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.25.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.6.3", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.81.1", "metro-cache": "0.81.1", "metro-cache-key": "0.81.1", "metro-config": "0.81.1", "metro-core": "0.81.1", "metro-file-map": "0.81.1", "metro-resolver": "0.81.1", "metro-runtime": "0.81.1", "metro-source-map": "0.81.1", "metro-symbolicate": "0.81.1", "metro-transform-plugins": "0.81.1", "metro-transform-worker": "0.81.1", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-fqRu4fg8ONW7VfqWFMGgKAcOuMzyoQah2azv9Y3VyFXAmG+AoTU6YIFWqAADESCGVWuWEIvxTJhMf3jxU6jwjA=="],
|
||||||
|
|
||||||
"metro-babel-transformer": ["metro-babel-transformer@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.25.1", "nullthrows": "^1.1.1" } }, "sha512-oKCQuajU5srm+ZdDcFg86pG/U8hkSjBlkyFjz380SZ4TTIiI5F+OQB830i53D8hmqmcosa4wR/pnKv8y4Q3dLw=="],
|
"metro-babel-transformer": ["metro-babel-transformer@0.81.1", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.25.1", "nullthrows": "^1.1.1" } }, "sha512-JECKDrQaUnDmj0x/Q/c8c5YwsatVx38Lu+BfCwX9fR8bWipAzkvJocBpq5rOAJRDXRgDcPv2VO4Q4nFYrpYNQg=="],
|
||||||
|
|
||||||
"metro-cache": ["metro-cache@0.81.5", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "metro-core": "0.81.5" } }, "sha512-wOsXuEgmZMZ5DMPoz1pEDerjJ11AuMy9JifH4yNW7NmWS0ghCRqvDxk13LsElzLshey8C+my/tmXauXZ3OqZgg=="],
|
"metro-cache": ["metro-cache@0.81.1", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "metro-core": "0.81.1" } }, "sha512-Uqcmn6sZ+Y0VJHM88VrG5xCvSeU7RnuvmjPmSOpEcyJJBe02QkfHL05MX2ZyGDTyZdbKCzaX0IijrTe4hN3F0Q=="],
|
||||||
|
|
||||||
"metro-cache-key": ["metro-cache-key@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-lGWnGVm1UwO8faRZ+LXQUesZSmP1LOg14OVR+KNPBip8kbMECbQJ8c10nGesw28uQT7AE0lwQThZPXlxDyCLKQ=="],
|
"metro-cache-key": ["metro-cache-key@0.81.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-5fDaHR1yTvpaQuwMAeEoZGsVyvjrkw9IFAS7WixSPvaNY5YfleqoJICPc6hbXFJjvwCCpwmIYFkjqzR/qJ6yqA=="],
|
||||||
|
|
||||||
"metro-config": ["metro-config@0.81.5", "", { "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.81.5", "metro-cache": "0.81.5", "metro-core": "0.81.5", "metro-runtime": "0.81.5" } }, "sha512-oDRAzUvj6RNRxratFdcVAqtAsg+T3qcKrGdqGZFUdwzlFJdHGR9Z413sW583uD2ynsuOjA2QB6US8FdwiBdNKg=="],
|
"metro-config": ["metro-config@0.81.1", "", { "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.6.3", "metro": "0.81.1", "metro-cache": "0.81.1", "metro-core": "0.81.1", "metro-runtime": "0.81.1" } }, "sha512-VAAJmxsKIZ+Fz5/z1LVgxa32gE6+2TvrDSSx45g85WoX4EtLmdBGP3DSlpQW3DqFUfNHJCGwMLGXpJnxifd08g=="],
|
||||||
|
|
||||||
"metro-core": ["metro-core@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.81.5" } }, "sha512-+2R0c8ByfV2N7CH5wpdIajCWa8escUFd8TukfoXyBq/vb6yTCsznoA25FhNXJ+MC/cz1L447Zj3vdUfCXIZBwg=="],
|
"metro-core": ["metro-core@0.81.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.81.1" } }, "sha512-4d2/+02IYqOwJs4dmM0dC8hIZqTzgnx2nzN4GTCaXb3Dhtmi/SJ3v6744zZRnithhN4lxf8TTJSHnQV75M7SSA=="],
|
||||||
|
|
||||||
"metro-file-map": ["metro-file-map@0.81.5", "", { "dependencies": { "debug": "^2.2.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-mW1PKyiO3qZvjeeVjj1brhkmIotObA3/9jdbY1fQQYvEWM6Ml7bN/oJCRDGn2+bJRlG+J8pwyJ+DgdrM4BsKyg=="],
|
"metro-file-map": ["metro-file-map@0.81.1", "", { "dependencies": { "debug": "^2.2.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.6.3", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-aY72H2ujmRfFxcsbyh83JgqFF+uQ4HFN1VhV2FmcfQG4s1bGKf2Vbkk+vtZ1+EswcBwDZFbkpvAjN49oqwGzAA=="],
|
||||||
|
|
||||||
"metro-minify-terser": ["metro-minify-terser@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-/mn4AxjANnsSS3/Bb+zA1G5yIS5xygbbz/OuPaJYs0CPcZCaWt66D+65j4Ft/nJkffUxcwE9mk4ubpkl3rjgtw=="],
|
"metro-minify-terser": ["metro-minify-terser@0.81.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-p/Qz3NNh1nebSqMlxlUALAnESo6heQrnvgHtAuxufRPtKvghnVDq9hGGex8H7z7YYLsqe42PWdt4JxTA3mgkvg=="],
|
||||||
|
|
||||||
"metro-resolver": ["metro-resolver@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-6BX8Nq3g3go3FxcyXkVbWe7IgctjDTk6D9flq+P201DfHHQ28J+DWFpVelFcrNTn4tIfbP/Bw7u/0g2BGmeXfQ=="],
|
"metro-resolver": ["metro-resolver@0.81.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-E61t6fxRoYRkl6Zo3iUfCKW4DYfum/bLjcejXBMt1y3I7LFkK84TCR/Rs9OAwsMCY/7GOPB4+CREYZOtCC7CNA=="],
|
||||||
|
|
||||||
"metro-runtime": ["metro-runtime@0.81.5", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-M/Gf71ictUKP9+77dV/y8XlAWg7xl76uhU7ggYFUwEdOHHWPG6gLBr1iiK0BmTjPFH8yRo/xyqMli4s3oGorPQ=="],
|
"metro-runtime": ["metro-runtime@0.81.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-pqu5j5d01rjF85V/K8SDDJ0NR3dRp6bE3z5bKVVb5O2Rx0nbR9KreUxYALQCRCcQHaYySqCg5fYbGKBHC295YQ=="],
|
||||||
|
|
||||||
"metro-source-map": ["metro-source-map@0.81.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.81.5", "nullthrows": "^1.1.1", "ob1": "0.81.5", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-Jz+CjvCKLNbJZYJTBeN3Kq9kIJf6b61MoLBdaOQZJ5Ajhw6Pf95Nn21XwA8BwfUYgajsi6IXsp/dTZsYJbN00Q=="],
|
"metro-source-map": ["metro-source-map@0.81.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.81.1", "nullthrows": "^1.1.1", "ob1": "0.81.1", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-1i8ROpNNiga43F0ZixAXoFE/SS3RqcRDCCslpynb+ytym0VI7pkTH1woAN2HI9pczYtPrp3Nq0AjRpsuY35ieA=="],
|
||||||
|
|
||||||
"metro-symbolicate": ["metro-symbolicate@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.81.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-X3HV3n3D6FuTE11UWFICqHbFMdTavfO48nXsSpnNGFkUZBexffu0Xd+fYKp+DJLNaQr3S+lAs8q9CgtDTlRRuA=="],
|
"metro-symbolicate": ["metro-symbolicate@0.81.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.81.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-Lgk0qjEigtFtsM7C0miXITbcV47E1ZYIfB+m/hCraihiwRWkNUQEPCWvqZmwXKSwVE5mXA0EzQtghAvQSjZDxw=="],
|
||||||
|
|
||||||
"metro-transform-plugins": ["metro-transform-plugins@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-MmHhVx/1dJC94FN7m3oHgv5uOjKH8EX8pBeu1pnPMxbJrx6ZuIejO0k84zTSaQTZ8RxX1wqwzWBpXAWPjEX8mA=="],
|
"metro-transform-plugins": ["metro-transform-plugins@0.81.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-7L1lI44/CyjIoBaORhY9fVkoNe8hrzgxjSCQ/lQlcfrV31cZb7u0RGOQrKmUX7Bw4FpejrB70ArQ7Mse9mk7+Q=="],
|
||||||
|
|
||||||
"metro-transform-worker": ["metro-transform-worker@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.81.5", "metro-babel-transformer": "0.81.5", "metro-cache": "0.81.5", "metro-cache-key": "0.81.5", "metro-minify-terser": "0.81.5", "metro-source-map": "0.81.5", "metro-transform-plugins": "0.81.5", "nullthrows": "^1.1.1" } }, "sha512-lUFyWVHa7lZFRSLJEv+m4jH8WrR5gU7VIjUlg4XmxQfV8ngY4V10ARKynLhMYPeQGl7Qvf+Ayg0eCZ272YZ4Mg=="],
|
"metro-transform-worker": ["metro-transform-worker@0.81.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.81.1", "metro-babel-transformer": "0.81.1", "metro-cache": "0.81.1", "metro-cache-key": "0.81.1", "metro-minify-terser": "0.81.1", "metro-source-map": "0.81.1", "metro-transform-plugins": "0.81.1", "nullthrows": "^1.1.1" } }, "sha512-M+2hVT3rEy5K7PBmGDgQNq3Zx53TjScOcO/CieyLnCRFtBGWZiSJ2+bLAXXOKyKa/y3bI3i0owxtyxuPGDwbZg=="],
|
||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
@@ -1679,7 +1682,7 @@
|
|||||||
|
|
||||||
"nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="],
|
"nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="],
|
||||||
|
|
||||||
"ob1": ["ob1@0.81.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-iNpbeXPLmaiT9I5g16gFFFjsF3sGxLpYG2EGP3dfFB4z+l9X60mp/yRzStHhMtuNt8qmf7Ww80nOPQHngHhnIQ=="],
|
"ob1": ["ob1@0.81.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-1PEbvI+AFvOcgdNcO79FtDI1TUO8S3lhiKOyAiyWQF3sFDDKS+aw2/BZvGlArFnSmqckwOOB9chQuIX0/OahoQ=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
@@ -1851,7 +1854,7 @@
|
|||||||
|
|
||||||
"react-is": ["react-is@19.0.0", "", {}, "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="],
|
"react-is": ["react-is@19.0.0", "", {}, "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="],
|
||||||
|
|
||||||
"react-native": ["react-native-tvos@0.77.2-0", "", { "dependencies": { "@jest/create-cache-key-function": "^29.6.3", "@react-native-tvos/virtualized-lists": "0.77.2-0", "@react-native/assets-registry": "0.77.2", "@react-native/codegen": "0.77.2", "@react-native/community-cli-plugin": "0.77.2", "@react-native/gradle-plugin": "0.77.2", "@react-native/js-polyfills": "0.77.2", "@react-native/normalize-colors": "0.77.2", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.25.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.6.3", "jsc-android": "^250231.0.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.81.3", "metro-source-map": "^0.81.3", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.0.1", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.24.0-canary-efb381bbf-20230505", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^18.2.6", "react": "^18.2.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-Ys0tka4VRxClE8oGV4itR0CaeQwtI7jQ51uO7DedmUpt3m8I5uUUFQANgH8IhdEeTtvyPFbnCUffbpcFm59jKg=="],
|
"react-native": ["react-native-tvos@0.77.0-0", "", { "dependencies": { "@jest/create-cache-key-function": "^29.6.3", "@react-native-tvos/virtualized-lists": "0.77.0-0", "@react-native/assets-registry": "0.77.0", "@react-native/codegen": "0.77.0", "@react-native/community-cli-plugin": "0.77.0", "@react-native/gradle-plugin": "0.77.0", "@react-native/js-polyfills": "0.77.0", "@react-native/normalize-colors": "0.77.0", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.25.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.6.3", "jsc-android": "^250231.0.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.81.0", "metro-source-map": "^0.81.0", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.0.1", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.24.0-canary-efb381bbf-20230505", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^18.2.6", "react": "^18.2.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-edIOqGrPadpXHmt5R/LuhekHHLx/0DyrfY5A9odS2AlS+03S0ada7H5oDvusOUVcyq1vc3isrwZpUSQzudoR1g=="],
|
||||||
|
|
||||||
"react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="],
|
"react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="],
|
||||||
|
|
||||||
@@ -1903,6 +1906,8 @@
|
|||||||
|
|
||||||
"react-native-tab-view": ["react-native-tab-view@4.0.5", "", { "dependencies": { "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-Xn3TpYo4yvKRC/f4+cOcvsXlitdnSaYkacshckrEI3JiDmFKNFIRVNxtZFggm4MwbJafq2RzuzR6xrgKoxgkTw=="],
|
"react-native-tab-view": ["react-native-tab-view@4.0.5", "", { "dependencies": { "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-Xn3TpYo4yvKRC/f4+cOcvsXlitdnSaYkacshckrEI3JiDmFKNFIRVNxtZFggm4MwbJafq2RzuzR6xrgKoxgkTw=="],
|
||||||
|
|
||||||
|
"react-native-tvos": ["react-native-tvos@0.77.0-0", "", { "dependencies": { "@jest/create-cache-key-function": "^29.6.3", "@react-native-tvos/virtualized-lists": "0.77.0-0", "@react-native/assets-registry": "0.77.0", "@react-native/codegen": "0.77.0", "@react-native/community-cli-plugin": "0.77.0", "@react-native/gradle-plugin": "0.77.0", "@react-native/js-polyfills": "0.77.0", "@react-native/normalize-colors": "0.77.0", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.25.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.6.3", "jsc-android": "^250231.0.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.81.0", "metro-source-map": "^0.81.0", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.0.1", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.24.0-canary-efb381bbf-20230505", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^18.2.6", "react": "^18.2.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-edIOqGrPadpXHmt5R/LuhekHHLx/0DyrfY5A9odS2AlS+03S0ada7H5oDvusOUVcyq1vc3isrwZpUSQzudoR1g=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"react-native-uitextview": ["react-native-uitextview@1.4.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-itm/frzkn/ma3+lwmKn2CkBOXPNo4bL8iVwQwjlzix5gVO59T2+axdfoj/Wi+Ra6F76KzNKxSah+7Y8dYmCHbQ=="],
|
"react-native-uitextview": ["react-native-uitextview@1.4.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-itm/frzkn/ma3+lwmKn2CkBOXPNo4bL8iVwQwjlzix5gVO59T2+axdfoj/Wi+Ra6F76KzNKxSah+7Y8dYmCHbQ=="],
|
||||||
@@ -2313,6 +2318,8 @@
|
|||||||
|
|
||||||
"@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
|
"@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
|
||||||
|
|
||||||
|
"@config-plugins/ffmpeg-kit-react-native/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||||
|
|
||||||
"@expo/bunyan/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
"@expo/bunyan/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||||
|
|
||||||
"@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
"@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||||
@@ -2423,7 +2430,7 @@
|
|||||||
|
|
||||||
"@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.77.2", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.77.2", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "selfsigned": "^2.4.1", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-LBK0kY4XxE4vHVHJ3TwBGXmjl2ad9dsbbwnVgXwYNL/mkkWb2MHlmgHj6xlCMe1gtLtem2TpEF17TKg50ykPJw=="],
|
"@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.77.0", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.77.0", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "nullthrows": "^1.1.1", "open": "^7.0.3", "selfsigned": "^2.4.1", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-DAlEYujm43O+Dq98KP2XfLSX5c/TEGtt+JBDEIOQewk374uYY52HzRb1+Gj6tNaEj/b33no4GibtdxbO5zmPhg=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"@react-native/community-cli-plugin/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
@@ -2433,7 +2440,7 @@
|
|||||||
|
|
||||||
"@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
"@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
||||||
|
|
||||||
"@react-native/metro-babel-transformer/@react-native/babel-preset": ["@react-native/babel-preset@0.77.2", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.77.2", "babel-plugin-syntax-hermes-parser": "0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-If6X4I0z6W5aVzqZS4JOrN7sh08w1QzEL8Q66i3g0wI8K8ZK+V+/ARlEmboy14VtcOYlmmjXEqSCv+Z2o9cuKg=="],
|
"@react-native/metro-babel-transformer/@react-native/babel-preset": ["@react-native/babel-preset@0.77.0", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.77.0", "babel-plugin-syntax-hermes-parser": "0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-Z4yxE66OvPyQ/iAlaETI1ptRLcDm7Tk6ZLqtCPuUX3AMg+JNgIA86979T4RSk486/JrBUBH5WZe2xjj7eEHXsA=="],
|
||||||
|
|
||||||
"@react-navigation/core/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
"@react-navigation/core/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||||
|
|
||||||
@@ -2591,7 +2598,7 @@
|
|||||||
|
|
||||||
"react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
"react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||||
|
|
||||||
"react-native/@react-native/normalize-colors": ["@react-native/normalize-colors@0.77.2", "", {}, "sha512-knKStQKX4KM8GkieeayotcSTO7I7PIZxwI71nhK/zBeRPqhDTJMNJQh5TnZJ63fO1Y+EZclWkRIKEj+aFRsssw=="],
|
"react-native/@react-native/normalize-colors": ["@react-native/normalize-colors@0.77.0", "", {}, "sha512-qjmxW3xRZe4T0ZBEaXZNHtuUbRgyfybWijf1yUuQwjBt24tSapmIslwhCjpKidA0p93ssPcepquhY0ykH25mew=="],
|
||||||
|
|
||||||
"react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
"react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||||
|
|
||||||
@@ -2601,6 +2608,16 @@
|
|||||||
|
|
||||||
"react-native/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
"react-native/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||||
|
|
||||||
|
"react-native-tvos/@react-native/normalize-colors": ["@react-native/normalize-colors@0.77.0", "", {}, "sha512-qjmxW3xRZe4T0ZBEaXZNHtuUbRgyfybWijf1yUuQwjBt24tSapmIslwhCjpKidA0p93ssPcepquhY0ykH25mew=="],
|
||||||
|
|
||||||
|
"react-native-tvos/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||||
|
|
||||||
|
"react-native-tvos/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
|
"react-native-tvos/scheduler": ["scheduler@0.24.0-canary-efb381bbf-20230505", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA=="],
|
||||||
|
|
||||||
|
"react-native-tvos/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||||
|
|
||||||
"react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="],
|
"react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="],
|
||||||
|
|
||||||
"react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
|
"react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
|
||||||
@@ -2747,7 +2764,7 @@
|
|||||||
|
|
||||||
"@react-native/codegen/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"@react-native/codegen/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.77.2", "", {}, "sha512-MRLjQLJr9C0M/TggoycEgYR7lUEZph4cg5PhUwBoNyRquV7lGHqMKNkfMBYBT09cuwKn9O+cFvQOmMNVqsPLxw=="],
|
"@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.77.0", "", {}, "sha512-glOvSEjCbVXw+KtfiOAmrq21FuLE1VsmBsyT7qud4KWbXP43aUEhzn70mWyFuiIdxnzVPKe2u8iWTQTdJksR1w=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
"@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
||||||
|
|
||||||
@@ -2755,7 +2772,7 @@
|
|||||||
|
|
||||||
"@react-native/dev-middleware/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"@react-native/dev-middleware/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.77.2", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.77.2" } }, "sha512-2PShbsfsa4NZS+Zt0y2tl1AoWza5podKFmPE5qcYjJoN915VoH3BRkiTVlSpYNKmdvs31o1aQuXAMQDTh7DZ/g=="],
|
"@react-native/metro-babel-transformer/@react-native/babel-preset/@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.77.0", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.77.0" } }, "sha512-5TYPn1k+jdDOZJU4EVb1kZ0p9TCVICXK3uplRev5Gul57oWesAaiWGZOzfRS3lonWeuR4ij8v8PFfIHOaq0vmA=="],
|
||||||
|
|
||||||
"ansi-fragments/slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
"ansi-fragments/slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||||
|
|
||||||
@@ -2839,6 +2856,8 @@
|
|||||||
|
|
||||||
"pkg-dir/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
"pkg-dir/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
||||||
|
|
||||||
|
"react-native-tvos/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
"react-native/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"react-native/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
"readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
"readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||||
@@ -2931,6 +2950,8 @@
|
|||||||
|
|
||||||
"pkg-dir/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
"pkg-dir/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||||
|
|
||||||
|
"react-native-tvos/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|
||||||
"react-native/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
"react-native/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|
||||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { queueActions, queueAtom } from "@/utils/atoms/queue";
|
import { queueActions, queueAtom } from "@/utils/atoms/queue";
|
||||||
@@ -151,7 +152,18 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
}
|
}
|
||||||
closeModal();
|
closeModal();
|
||||||
|
|
||||||
initiateDownload(...itemsNotDownloaded);
|
if (usingOptimizedServer) initiateDownload(...itemsNotDownloaded);
|
||||||
|
else {
|
||||||
|
queueActions.enqueue(
|
||||||
|
queue,
|
||||||
|
setQueue,
|
||||||
|
...itemsNotDownloaded.map((item) => ({
|
||||||
|
id: item.Id!,
|
||||||
|
execute: async () => await initiateDownload(item),
|
||||||
|
item,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
t("home.downloads.toasts.you_are_not_allowed_to_download_files"),
|
t("home.downloads.toasts.you_are_not_allowed_to_download_files"),
|
||||||
@@ -191,6 +203,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
mediaSource = defaults.mediaSource;
|
mediaSource = defaults.mediaSource;
|
||||||
audioIndex = defaults.audioIndex;
|
audioIndex = defaults.audioIndex;
|
||||||
subtitleIndex = defaults.subtitleIndex;
|
subtitleIndex = defaults.subtitleIndex;
|
||||||
|
// Keep using the selected bitrate for consistency across all downloads
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getStreamUrl({
|
const res = await getStreamUrl({
|
||||||
@@ -203,8 +216,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
mediaSourceId: mediaSource?.Id,
|
mediaSourceId: mediaSource?.Id,
|
||||||
subtitleStreamIndex: subtitleIndex,
|
subtitleStreamIndex: subtitleIndex,
|
||||||
deviceProfile: download,
|
deviceProfile: download,
|
||||||
download: true,
|
|
||||||
// deviceId: mediaSource?.Id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
@@ -219,8 +230,12 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
|
|
||||||
if (!url || !source) throw new Error("No url");
|
if (!url || !source) throw new Error("No url");
|
||||||
|
|
||||||
saveDownloadItemInfoToDiskTmp(item, source, url);
|
if (usingOptimizedServer) {
|
||||||
await startBackgroundDownload(url, item, source, maxBitrate);
|
saveDownloadItemInfoToDiskTmp(item, source, url);
|
||||||
|
await startBackgroundDownload(url, item, source);
|
||||||
|
} else {
|
||||||
|
//await startRemuxing(item, url, source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -234,6 +249,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
maxBitrate,
|
maxBitrate,
|
||||||
usingOptimizedServer,
|
usingOptimizedServer,
|
||||||
startBackgroundDownload,
|
startBackgroundDownload,
|
||||||
|
//startRemuxing,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { useImageColors } from "@/hooks/useImageColors";
|
|||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
||||||
import type {
|
import type {
|
||||||
@@ -35,7 +34,6 @@ import { ItemHeader } from "./ItemHeader";
|
|||||||
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
|
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
|
||||||
import { MediaSourceSelector } from "./MediaSourceSelector";
|
import { MediaSourceSelector } from "./MediaSourceSelector";
|
||||||
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
||||||
import { PlayInRemoteSessionButton } from "./PlayInRemoteSession";
|
|
||||||
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
|
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
|
||||||
|
|
||||||
export type SelectedOptions = {
|
export type SelectedOptions = {
|
||||||
@@ -52,8 +50,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
const { orientation } = useOrientation();
|
const { orientation } = useOrientation();
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [user] = useAtom(userAtom);
|
|
||||||
|
|
||||||
useImageColors({ item });
|
useImageColors({ item });
|
||||||
|
|
||||||
const [loadingLogo, setLoadingLogo] = useState(true);
|
const [loadingLogo, setLoadingLogo] = useState(true);
|
||||||
@@ -101,10 +97,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
{!Platform.isTV && (
|
{!Platform.isTV && (
|
||||||
<DownloadSingleItem item={item} size='large' />
|
<DownloadSingleItem item={item} size='large' />
|
||||||
)}
|
)}
|
||||||
{user?.Policy?.IsAdministrator && (
|
|
||||||
<PlayInRemoteSessionButton item={item} size='large' />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PlayedStatus items={[item]} size='large' />
|
<PlayedStatus items={[item]} size='large' />
|
||||||
<AddToFavorites item={item} />
|
<AddToFavorites item={item} />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
|||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
import { chromecast } from "@/utils/profiles/chromecast";
|
import { chromecast } from "@/utils/profiles/chromecast";
|
||||||
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
|
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
|
||||||
|
import ios from "@/utils/profiles/ios";
|
||||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||||
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
@@ -66,14 +67,11 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const startColor = useSharedValue(colorAtom);
|
const startColor = useSharedValue(colorAtom);
|
||||||
const widthProgress = useSharedValue(0);
|
const widthProgress = useSharedValue(0);
|
||||||
const colorChangeProgress = useSharedValue(0);
|
const colorChangeProgress = useSharedValue(0);
|
||||||
const [settings, updateSettings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
(q: string) => {
|
(q: string) => {
|
||||||
if (settings.maxAutoPlayEpisodeCount.value !== -1) {
|
|
||||||
updateSettings({ autoPlayEpisodeCount: 0 });
|
|
||||||
}
|
|
||||||
router.push(`/player/direct-player?${q}`);
|
router.push(`/player/direct-player?${q}`);
|
||||||
},
|
},
|
||||||
[router],
|
[router],
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
import { useAllSessions, type useSessionsProps } from "@/hooks/useSessions";
|
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import {
|
|
||||||
type BaseItemDto,
|
|
||||||
PlayCommand,
|
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import {
|
|
||||||
FlatList,
|
|
||||||
Modal,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { Loader } from "./Loader";
|
|
||||||
import { RoundButton } from "./RoundButton";
|
|
||||||
import { Text } from "./common/Text";
|
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof View> {
|
|
||||||
item: BaseItemDto;
|
|
||||||
size?: "default" | "large";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PlayInRemoteSessionButton: React.FC<Props> = ({
|
|
||||||
item,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
|
||||||
const api = useAtomValue(apiAtom);
|
|
||||||
const { sessions, isLoading } = useAllSessions({} as useSessionsProps);
|
|
||||||
const handlePlayInSession = async (sessionId: string) => {
|
|
||||||
if (!api || !item.Id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`Playing ${item.Name} in session ${sessionId}`);
|
|
||||||
getSessionApi(api).play({
|
|
||||||
sessionId,
|
|
||||||
itemIds: [item.Id],
|
|
||||||
playCommand: PlayCommand.PlayNow,
|
|
||||||
});
|
|
||||||
|
|
||||||
setModalVisible(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error playing in remote session:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View {...props}>
|
|
||||||
<RoundButton
|
|
||||||
icon='play-circle-outline'
|
|
||||||
onPress={() => setModalVisible(true)}
|
|
||||||
size={props.size}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
animationType='slide'
|
|
||||||
transparent={true}
|
|
||||||
visible={modalVisible}
|
|
||||||
onRequestClose={() => setModalVisible(false)}
|
|
||||||
>
|
|
||||||
<View style={styles.centeredView}>
|
|
||||||
<View style={styles.modalView}>
|
|
||||||
<View style={styles.modalHeader}>
|
|
||||||
<Text style={styles.modalTitle}>Select Session</Text>
|
|
||||||
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
|
||||||
<Ionicons name='close' size={24} color='white' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.modalContent}>
|
|
||||||
{isLoading ? (
|
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
<Loader />
|
|
||||||
</View>
|
|
||||||
) : !sessions || sessions.length === 0 ? (
|
|
||||||
<Text style={styles.noSessionsText}>
|
|
||||||
No active sessions found
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<FlatList
|
|
||||||
data={sessions}
|
|
||||||
keyExtractor={(session) => session.Id || "unknown"}
|
|
||||||
renderItem={({ item: session }) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.sessionItem}
|
|
||||||
onPress={() => handlePlayInSession(session.Id || "")}
|
|
||||||
>
|
|
||||||
<View style={styles.sessionInfo}>
|
|
||||||
<Text style={styles.sessionName}>
|
|
||||||
{session.DeviceName}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.sessionDetails}>
|
|
||||||
{session.UserName} • {session.Client}
|
|
||||||
</Text>
|
|
||||||
{session.NowPlayingItem && (
|
|
||||||
<Text style={styles.nowPlaying} numberOfLines={1}>
|
|
||||||
Now playing:{" "}
|
|
||||||
{session.NowPlayingItem.SeriesName
|
|
||||||
? `${session.NowPlayingItem.SeriesName} :`
|
|
||||||
: ""}
|
|
||||||
{session.NowPlayingItem.Name}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<Ionicons name='play-sharp' size={20} color='#888' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
contentContainerStyle={styles.listContent}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
centeredView: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
||||||
},
|
|
||||||
modalView: {
|
|
||||||
width: "90%",
|
|
||||||
maxHeight: "80%",
|
|
||||||
backgroundColor: "#1c1c1c",
|
|
||||||
borderRadius: 20,
|
|
||||||
overflow: "hidden",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
},
|
|
||||||
modalHeader: {
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 12,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: "#333",
|
|
||||||
},
|
|
||||||
modalContent: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
modalTitle: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: "600",
|
|
||||||
},
|
|
||||||
loadingContainer: {
|
|
||||||
padding: 40,
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
noSessionsText: {
|
|
||||||
padding: 40,
|
|
||||||
textAlign: "center",
|
|
||||||
color: "#888",
|
|
||||||
},
|
|
||||||
listContent: {
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
sessionItem: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
paddingVertical: 12,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: "#333",
|
|
||||||
},
|
|
||||||
sessionInfo: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
sessionName: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: "500",
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
sessionDetails: {
|
|
||||||
fontSize: 13,
|
|
||||||
opacity: 0.7,
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
|
||||||
nowPlaying: {
|
|
||||||
fontSize: 12,
|
|
||||||
opacity: 0.5,
|
|
||||||
fontStyle: "italic",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -23,6 +23,9 @@ import { Button } from "../Button";
|
|||||||
const BackGroundDownloader = !Platform.isTV
|
const BackGroundDownloader = !Platform.isTV
|
||||||
? require("@kesha-antonov/react-native-background-downloader")
|
? require("@kesha-antonov/react-native-background-downloader")
|
||||||
: null;
|
: null;
|
||||||
|
//const FFmpegKitProvider = !Platform.isTV
|
||||||
|
// ? require("ffmpeg-kit-react-native")
|
||||||
|
// : null;
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
@@ -69,18 +72,23 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
|||||||
mutationFn: async (id: string) => {
|
mutationFn: async (id: string) => {
|
||||||
if (!process) throw new Error("No active download");
|
if (!process) throw new Error("No active download");
|
||||||
|
|
||||||
try {
|
if (settings?.downloadMethod === DownloadMethod.Optimized) {
|
||||||
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
try {
|
||||||
for (const task of tasks) {
|
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
||||||
if (task.id === id) {
|
for (const task of tasks) {
|
||||||
task.stop();
|
if (task.id === id) {
|
||||||
|
task.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} finally {
|
||||||
} finally {
|
await removeProcess(id);
|
||||||
await removeProcess(id);
|
|
||||||
if (settings?.downloadMethod === DownloadMethod.Optimized) {
|
|
||||||
await queryClient.refetchQueries({ queryKey: ["jobs"] });
|
await queryClient.refetchQueries({ queryKey: ["jobs"] });
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
//FFmpegKitProvider.FFmpegKit.cancel(Number(id));
|
||||||
|
setProcesses((prev: any[]) =>
|
||||||
|
prev.filter((p: { id: string }) => p.id !== id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ const SeriesPoster: React.FC<MoviePosterProps> = ({ item }) => {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{<WatchedIndicator item={item} />}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { PropsWithChildren } from "react";
|
|||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
|
|
||||||
type SearchItemWrapperProps<T> = {
|
type SearchItemWrapperProps<T> = {
|
||||||
|
ids?: string[] | null;
|
||||||
items?: T[];
|
items?: T[];
|
||||||
renderItem: (item: any) => React.ReactNode;
|
renderItem: (item: any) => React.ReactNode;
|
||||||
header?: string;
|
header?: string;
|
||||||
@@ -16,6 +17,7 @@ type SearchItemWrapperProps<T> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SearchItemWrapper = <T,>({
|
export const SearchItemWrapper = <T,>({
|
||||||
|
ids,
|
||||||
items,
|
items,
|
||||||
renderItem,
|
renderItem,
|
||||||
header,
|
header,
|
||||||
@@ -24,7 +26,33 @@ export const SearchItemWrapper = <T,>({
|
|||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
|
||||||
if (!items || items.length === 0) return null;
|
const { data, isLoading: l1 } = useQuery({
|
||||||
|
queryKey: ["items", ids],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!user?.Id || !api || !ids || ids.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemPromises = ids.map((id) =>
|
||||||
|
getUserItemData({
|
||||||
|
api,
|
||||||
|
userId: user.Id,
|
||||||
|
itemId: id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = await Promise.all(itemPromises);
|
||||||
|
|
||||||
|
// Filter out null items
|
||||||
|
return results.filter(
|
||||||
|
(item) => item !== null,
|
||||||
|
) as unknown as BaseItemDto[];
|
||||||
|
},
|
||||||
|
enabled: !!ids && ids.length > 0 && !!api && !!user?.Id,
|
||||||
|
staleTime: Number.POSITIVE_INFINITY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data && (!items || items.length === 0)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -39,7 +67,7 @@ export const SearchItemWrapper = <T,>({
|
|||||||
keyExtractor={(_, index) => index.toString()}
|
keyExtractor={(_, index) => index.toString()}
|
||||||
estimatedItemSize={250}
|
estimatedItemSize={250}
|
||||||
/*@ts-ignore */
|
/*@ts-ignore */
|
||||||
data={items}
|
data={data || items}
|
||||||
onEndReachedThreshold={1}
|
onEndReachedThreshold={1}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Platform,
|
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -88,12 +87,6 @@ export const HomeIndex = () => {
|
|||||||
|
|
||||||
const { downloadedFiles, cleanCacheDirectory } = useDownload();
|
const { downloadedFiles, cleanCacheDirectory } = useDownload();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.isTV) {
|
|
||||||
navigation.setOptions({
|
|
||||||
headerLeft: () => null,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hasDownloads = downloadedFiles && downloadedFiles.length > 0;
|
const hasDownloads = downloadedFiles && downloadedFiles.length > 0;
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerLeft: () => (
|
headerLeft: () => (
|
||||||
@@ -213,43 +206,19 @@ export const HomeIndex = () => {
|
|||||||
queryKey,
|
queryKey,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!api) return [];
|
if (!api) return [];
|
||||||
|
return (
|
||||||
const response = await getItemsApi(api).getItems({
|
(
|
||||||
userId: user?.Id,
|
await getUserLibraryApi(api).getLatestMedia({
|
||||||
limit: 40,
|
userId: user?.Id,
|
||||||
recursive: true,
|
limit: 20,
|
||||||
includeItemTypes,
|
fields: ["PrimaryImageAspectRatio", "Path"],
|
||||||
sortBy: ["DateCreated"],
|
imageTypeLimit: 1,
|
||||||
sortOrder: ["Descending"],
|
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||||
fields: ["PrimaryImageAspectRatio", "Path"],
|
includeItemTypes,
|
||||||
parentId,
|
parentId,
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
})
|
||||||
});
|
).data || []
|
||||||
|
);
|
||||||
let items = response.data.Items || [];
|
|
||||||
|
|
||||||
if (includeItemTypes.includes("Episode")) {
|
|
||||||
// Removes individual episodes from the list if they are part of a series
|
|
||||||
// and only keeps the series item
|
|
||||||
// Note: The 'Latest' API endpoint does not work well with combining batch episode imports
|
|
||||||
// and will either only show the series or the episodes, not both.
|
|
||||||
// This is a workaround to filter out the episodes from the list
|
|
||||||
const seriesIds = new Set(
|
|
||||||
items.filter((i) => i.Type === "Series").map((i) => i.Id),
|
|
||||||
);
|
|
||||||
|
|
||||||
items = items.filter(
|
|
||||||
(i) =>
|
|
||||||
i.Type === "Series" ||
|
|
||||||
(i.Type === "Episode" && !seriesIds.has(i.SeriesId!)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (items.length > 20) {
|
|
||||||
items = items.slice(0, 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
},
|
},
|
||||||
type: "ScrollingCollectionList",
|
type: "ScrollingCollectionList",
|
||||||
}),
|
}),
|
||||||
@@ -263,7 +232,7 @@ export const HomeIndex = () => {
|
|||||||
|
|
||||||
const latestMediaViews = collections.map((c) => {
|
const latestMediaViews = collections.map((c) => {
|
||||||
const includeItemTypes: BaseItemKind[] =
|
const includeItemTypes: BaseItemKind[] =
|
||||||
c.CollectionType === "tvshows" ? ["Episode", "Series"] : ["Movie"];
|
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
|
||||||
const title = t("home.recently_added_in", { libraryName: c.Name });
|
const title = t("home.recently_added_in", { libraryName: c.Name });
|
||||||
const queryKey = [
|
const queryKey = [
|
||||||
"home",
|
"home",
|
||||||
@@ -389,10 +358,10 @@ export const HomeIndex = () => {
|
|||||||
const response = await getTvShowsApi(api).getNextUp({
|
const response = await getTvShowsApi(api).getNextUp({
|
||||||
userId: user?.Id,
|
userId: user?.Id,
|
||||||
fields: ["MediaSourceCount"],
|
fields: ["MediaSourceCount"],
|
||||||
limit: section.nextUp?.limit || 25,
|
limit: section.items?.limit || 25,
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||||
enableResumable: section.nextUp?.enableResumable,
|
enableResumable: section.items?.enableResumable,
|
||||||
enableRewatching: section.nextUp?.enableRewatching,
|
enableRewatching: section.items?.enableRewatching,
|
||||||
});
|
});
|
||||||
return response.data.Items || [];
|
return response.data.Items || [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "@/utils/background-tasks";
|
} from "@/utils/background-tasks";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import i18n, { TFunction } from "i18next";
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -252,46 +251,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem title={t("home.settings.other.max_auto_play_episode_count")}>
|
|
||||||
<Dropdown
|
|
||||||
data={AUTOPLAY_EPISODES_COUNT(t)}
|
|
||||||
keyExtractor={(item) => item.key}
|
|
||||||
titleExtractor={(item) => item.key}
|
|
||||||
title={
|
|
||||||
<TouchableOpacity className='flex flex-row items-center justify-between py-3 pl-3'>
|
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
|
||||||
{t(settings?.maxAutoPlayEpisodeCount.key)}
|
|
||||||
</Text>
|
|
||||||
<Ionicons
|
|
||||||
name='chevron-expand-sharp'
|
|
||||||
size={18}
|
|
||||||
color='#5A5960'
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
}
|
|
||||||
label={t("home.settings.other.max_auto_play_episode_count")}
|
|
||||||
onSelected={(maxAutoPlayEpisodeCount) =>
|
|
||||||
updateSettings({ maxAutoPlayEpisodeCount })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUTOPLAY_EPISODES_COUNT = (
|
|
||||||
t: TFunction<"translation", undefined>,
|
|
||||||
): {
|
|
||||||
key: string;
|
|
||||||
value: number;
|
|
||||||
}[] => [
|
|
||||||
{ key: t("home.settings.other.disabled"), value: -1 },
|
|
||||||
{ key: "1", value: 1 },
|
|
||||||
{ key: "2", value: 2 },
|
|
||||||
{ key: "3", value: 3 },
|
|
||||||
{ key: "4", value: 4 },
|
|
||||||
{ key: "5", value: 5 },
|
|
||||||
{ key: "6", value: 6 },
|
|
||||||
{ key: "7", value: 7 },
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import { Button } from "@/components/Button";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import React from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export interface ContinueWatchingOverlayProps {
|
|
||||||
goToNextItem: (options: {
|
|
||||||
isAutoPlay: boolean;
|
|
||||||
resetWatchCount: boolean;
|
|
||||||
}) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({
|
|
||||||
goToNextItem,
|
|
||||||
}) => {
|
|
||||||
const [settings] = useSettings();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return settings.autoPlayEpisodeCount >=
|
|
||||||
settings.maxAutoPlayEpisodeCount.value ? (
|
|
||||||
<View
|
|
||||||
className={
|
|
||||||
"absolute top-0 bottom-0 left-0 right-0 flex flex-col px-4 items-center justify-center bg-[#000000B3]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text className='text-2xl font-bold text-white py-4 '>
|
|
||||||
Are you still watching ?
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
onPress={() => {
|
|
||||||
goToNextItem({ isAutoPlay: false, resetWatchCount: true });
|
|
||||||
}}
|
|
||||||
color={"purple"}
|
|
||||||
className='my-4 w-2/3'
|
|
||||||
>
|
|
||||||
{t("player.continue_watching")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button onPress={router.back} color={"transparent"} className='w-2/3'>
|
|
||||||
{t("player.go_back")}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ContinueWatchingOverlay;
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
|
|
||||||
import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes";
|
import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes";
|
||||||
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
@@ -29,7 +28,7 @@ import { Image } from "expo-image";
|
|||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, {
|
import {
|
||||||
type Dispatch,
|
type Dispatch,
|
||||||
type FC,
|
type FC,
|
||||||
type MutableRefObject,
|
type MutableRefObject,
|
||||||
@@ -122,7 +121,7 @@ export const Controls: FC<Props> = ({
|
|||||||
enableTrickplay = true,
|
enableTrickplay = true,
|
||||||
isVlc = false,
|
isVlc = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [settings, updateSettings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
@@ -237,76 +236,15 @@ export const Controls: FC<Props> = ({
|
|||||||
goToItemCommon(previousItem);
|
goToItemCommon(previousItem);
|
||||||
}, [previousItem, goToItemCommon]);
|
}, [previousItem, goToItemCommon]);
|
||||||
|
|
||||||
const goToNextItem = useCallback(
|
const goToNextItem = useCallback(() => {
|
||||||
({
|
if (!nextItem) return;
|
||||||
isAutoPlay,
|
goToItemCommon(nextItem);
|
||||||
resetWatchCount,
|
}, [nextItem, goToItemCommon]);
|
||||||
}: { isAutoPlay?: boolean; resetWatchCount?: boolean }) => {
|
|
||||||
if (!nextItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAutoPlay) {
|
|
||||||
// if we are not autoplaying, we won't update anything, we just go to the next item
|
|
||||||
goToItemCommon(nextItem);
|
|
||||||
if (resetWatchCount) {
|
|
||||||
updateSettings({
|
|
||||||
autoPlayEpisodeCount: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip autoplay logic if maxAutoPlayEpisodeCount is -1
|
|
||||||
if (settings.maxAutoPlayEpisodeCount.value === -1) {
|
|
||||||
goToItemCommon(nextItem);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
settings.autoPlayEpisodeCount + 1 <
|
|
||||||
settings.maxAutoPlayEpisodeCount.value
|
|
||||||
) {
|
|
||||||
goToItemCommon(nextItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the autoPlayEpisodeCount is less than maxAutoPlayEpisodeCount for the autoPlay
|
|
||||||
if (
|
|
||||||
settings.autoPlayEpisodeCount < settings.maxAutoPlayEpisodeCount.value
|
|
||||||
) {
|
|
||||||
// update the autoPlayEpisodeCount in settings
|
|
||||||
updateSettings({
|
|
||||||
autoPlayEpisodeCount: settings.autoPlayEpisodeCount + 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[nextItem, goToItemCommon],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add a memoized handler for autoplay next episode
|
|
||||||
const handleNextEpisodeAutoPlay = useCallback(() => {
|
|
||||||
goToNextItem({ isAutoPlay: true });
|
|
||||||
}, [goToNextItem]);
|
|
||||||
|
|
||||||
// Add a memoized handler for manual next episode
|
|
||||||
const handleNextEpisodeManual = useCallback(() => {
|
|
||||||
goToNextItem({ isAutoPlay: false });
|
|
||||||
}, [goToNextItem]);
|
|
||||||
|
|
||||||
// Add a memoized handler for ContinueWatchingOverlay
|
|
||||||
const handleContinueWatching = useCallback(
|
|
||||||
(options: { isAutoPlay?: boolean; resetWatchCount?: boolean }) => {
|
|
||||||
goToNextItem(options);
|
|
||||||
},
|
|
||||||
[goToNextItem],
|
|
||||||
);
|
|
||||||
|
|
||||||
const goToItem = useCallback(
|
const goToItem = useCallback(
|
||||||
async (itemId: string) => {
|
async (itemId: string) => {
|
||||||
const gotoItem = await getItemById(api, itemId);
|
const gotoItem = await getItemById(api, itemId);
|
||||||
if (!gotoItem) {
|
if (!gotoItem) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
goToItemCommon(gotoItem);
|
goToItemCommon(gotoItem);
|
||||||
},
|
},
|
||||||
[goToItemCommon, api],
|
[goToItemCommon, api],
|
||||||
@@ -362,9 +300,7 @@ export const Controls: FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSliderStart = useCallback(() => {
|
const handleSliderStart = useCallback(() => {
|
||||||
if (!showControls) {
|
if (!showControls) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSliding(true);
|
setIsSliding(true);
|
||||||
wasPlayingRef.current = isPlaying;
|
wasPlayingRef.current = isPlaying;
|
||||||
@@ -403,9 +339,7 @@ export const Controls: FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSkipBackward = useCallback(async () => {
|
const handleSkipBackward = useCallback(async () => {
|
||||||
if (!settings?.rewindSkipTime) {
|
if (!settings?.rewindSkipTime) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
wasPlayingRef.current = isPlaying;
|
wasPlayingRef.current = isPlaying;
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
try {
|
try {
|
||||||
@@ -437,9 +371,7 @@ export const Controls: FC<Props> = ({
|
|||||||
? curr + secondsToMs(settings.forwardSkipTime)
|
? curr + secondsToMs(settings.forwardSkipTime)
|
||||||
: ticksToSeconds(curr) + settings.forwardSkipTime;
|
: ticksToSeconds(curr) + settings.forwardSkipTime;
|
||||||
seek(Math.max(0, newTime));
|
seek(Math.max(0, newTime));
|
||||||
if (wasPlayingRef.current) {
|
if (wasPlayingRef.current) play();
|
||||||
play();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
writeToLog("ERROR", "Error seeking video forwards", error);
|
writeToLog("ERROR", "Error seeking video forwards", error);
|
||||||
@@ -614,7 +546,7 @@ export const Controls: FC<Props> = ({
|
|||||||
|
|
||||||
{nextItem && !offline && (
|
{nextItem && !offline && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => goToNextItem({ isAutoPlay: false })}
|
onPress={goToNextItem}
|
||||||
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
||||||
>
|
>
|
||||||
<Ionicons name='play-skip-forward' size={24} color='white' />
|
<Ionicons name='play-skip-forward' size={24} color='white' />
|
||||||
@@ -809,21 +741,17 @@ export const Controls: FC<Props> = ({
|
|||||||
onPress={skipCredit}
|
onPress={skipCredit}
|
||||||
buttonText='Skip Credits'
|
buttonText='Skip Credits'
|
||||||
/>
|
/>
|
||||||
{(settings.maxAutoPlayEpisodeCount.value === -1 ||
|
<NextEpisodeCountDownButton
|
||||||
settings.autoPlayEpisodeCount <
|
show={
|
||||||
settings.maxAutoPlayEpisodeCount.value) && (
|
!nextItem
|
||||||
<NextEpisodeCountDownButton
|
? false
|
||||||
show={
|
: isVlc
|
||||||
!nextItem
|
? remainingTime < 10000
|
||||||
? false
|
: remainingTime < 10
|
||||||
: isVlc
|
}
|
||||||
? remainingTime < 10000
|
onFinish={goToNextItem}
|
||||||
: remainingTime < 10
|
onPress={goToNextItem}
|
||||||
}
|
/>
|
||||||
onFinish={handleNextEpisodeAutoPlay}
|
|
||||||
onPress={handleNextEpisodeManual}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
@@ -871,9 +799,6 @@ export const Controls: FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{settings.maxAutoPlayEpisodeCount.value !== -1 && (
|
|
||||||
<ContinueWatchingOverlay goToNextItem={handleContinueWatching} />
|
|
||||||
)}
|
|
||||||
</ControlProvider>
|
</ControlProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
|
|||||||
>
|
>
|
||||||
<Animated.View style={animatedStyle} />
|
<Animated.View style={animatedStyle} />
|
||||||
<View className='px-3 py-3'>
|
<View className='px-3 py-3'>
|
||||||
<Text numberOfLines={1} className='text-center font-bold'>
|
<Text className='text-center font-bold'>
|
||||||
{t("player.next_episode")}
|
{t("player.next_episode")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { TrackInfo } from "@/modules/VlcPlayer.types";
|
import type { TrackInfo } from "@/modules/VlcPlayer.types";
|
||||||
import { VideoPlayer, useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import {
|
import {
|
||||||
@@ -48,7 +47,6 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [audioTracks, setAudioTracks] = useState<Track[] | null>(null);
|
const [audioTracks, setAudioTracks] = useState<Track[] | null>(null);
|
||||||
const [subtitleTracks, setSubtitleTracks] = useState<Track[] | null>(null);
|
const [subtitleTracks, setSubtitleTracks] = useState<Track[] | null>(null);
|
||||||
const [settings] = useSettings();
|
|
||||||
|
|
||||||
const ControlContext = useControlContext();
|
const ControlContext = useControlContext();
|
||||||
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
||||||
@@ -134,7 +132,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Step 2: Apply VLC indexing logic
|
// Step 2: Apply VLC indexing logic
|
||||||
let textSubIndex = settings.defaultPlayer === VideoPlayer.VLC_4 ? 0 : 1;
|
let textSubIndex = 0;
|
||||||
const processedSubs: Track[] = sortedSubs?.map((sub) => {
|
const processedSubs: Track[] = sortedSubs?.map((sub) => {
|
||||||
// Always increment for non-transcoding subtitles
|
// Always increment for non-transcoding subtitles
|
||||||
// Only increment for text-based subtitles when transcoding
|
// Only increment for text-based subtitles when transcoding
|
||||||
|
|||||||
231
hooks/useRemuxHlsToMp4.ts
Normal file
231
hooks/useRemuxHlsToMp4.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { getItemImage } from "@/utils/getItemImage";
|
||||||
|
import { writeErrorLog, writeInfoLog } from "@/utils/log";
|
||||||
|
import type {
|
||||||
|
BaseItemDto,
|
||||||
|
MediaSourceInfo,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import * as FileSystem from "expo-file-system";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
|
// import { FFmpegKit, FFmpegSession, Statistics } from "ffmpeg-kit-react-native";
|
||||||
|
const FFMPEGKitReactNative = !Platform.isTV
|
||||||
|
? require("ffmpeg-kit-react-native")
|
||||||
|
: null;
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import useDownloadHelper from "@/utils/download";
|
||||||
|
import type { JobStatus } from "@/utils/optimize-server";
|
||||||
|
import type { Api } from "@jellyfin/sdk";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import { toast } from "sonner-native";
|
||||||
|
import useImageStorage from "./useImageStorage";
|
||||||
|
|
||||||
|
type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession;
|
||||||
|
type Statistics = typeof FFMPEGKitReactNative.Statistics;
|
||||||
|
const FFmpegKit = Platform.isTV
|
||||||
|
? null
|
||||||
|
: (FFMPEGKitReactNative.FFmpegKit as typeof FFMPEGKitReactNative.FFmpegKit);
|
||||||
|
const createFFmpegCommand = (url: string, output: string) => [
|
||||||
|
"-y", // overwrite output files without asking
|
||||||
|
"-thread_queue_size 512", // https://ffmpeg.org/ffmpeg.html#toc-Advanced-options
|
||||||
|
|
||||||
|
// region ffmpeg protocol commands // https://ffmpeg.org/ffmpeg-protocols.html
|
||||||
|
"-protocol_whitelist file,http,https,tcp,tls,crypto", // whitelist
|
||||||
|
"-multiple_requests 1", // http
|
||||||
|
"-tcp_nodelay 1", // http
|
||||||
|
// endregion ffmpeg protocol commands
|
||||||
|
|
||||||
|
"-fflags +genpts", // format flags
|
||||||
|
`-i ${url}`, // infile
|
||||||
|
"-map 0:v -map 0:a", // select all streams for video & audio
|
||||||
|
"-c copy", // streamcopy, preventing transcoding
|
||||||
|
"-bufsize 25M", // amount of data processed before calculating current bitrate
|
||||||
|
"-max_muxing_queue_size 4096", // sets the size of stream buffer in packets for output
|
||||||
|
output,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for remuxing HLS to MP4 using FFmpeg.
|
||||||
|
*
|
||||||
|
* @param url - The URL of the HLS stream
|
||||||
|
* @param item - The BaseItemDto object representing the media item
|
||||||
|
* @returns An object with remuxing-related functions
|
||||||
|
*/
|
||||||
|
export const useRemuxHlsToMp4 = () => {
|
||||||
|
const api = useAtomValue(apiAtom);
|
||||||
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [settings] = useSettings();
|
||||||
|
const { saveImage } = useImageStorage();
|
||||||
|
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
||||||
|
const {
|
||||||
|
saveDownloadedItemInfo,
|
||||||
|
setProcesses,
|
||||||
|
processes,
|
||||||
|
APP_CACHE_DOWNLOAD_DIRECTORY,
|
||||||
|
} = useDownload();
|
||||||
|
|
||||||
|
const onSaveAssets = async (api: Api, item: BaseItemDto) => {
|
||||||
|
await saveSeriesPrimaryImage(item);
|
||||||
|
const itemImage = getItemImage({
|
||||||
|
item,
|
||||||
|
api,
|
||||||
|
variant: "Primary",
|
||||||
|
quality: 90,
|
||||||
|
width: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
await saveImage(item.Id, itemImage?.uri);
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeCallback = useCallback(
|
||||||
|
async (session: FFmpegSession, item: BaseItemDto) => {
|
||||||
|
try {
|
||||||
|
console.log("completeCallback");
|
||||||
|
const returnCode = await session.getReturnCode();
|
||||||
|
|
||||||
|
if (returnCode.isValueSuccess()) {
|
||||||
|
const stat = await session.getLastReceivedStatistics();
|
||||||
|
await FileSystem.moveAsync({
|
||||||
|
from: `${APP_CACHE_DOWNLOAD_DIRECTORY}${item.Id}.mp4`,
|
||||||
|
to: `${FileSystem.documentDirectory}${item.Id}.mp4`,
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["downloadedItems"],
|
||||||
|
});
|
||||||
|
saveDownloadedItemInfo(item, stat.getSize());
|
||||||
|
toast.success(t("home.downloads.toasts.download_completed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
setProcesses((prev: any[]) => {
|
||||||
|
return prev.filter(
|
||||||
|
(process: { itemId: string | undefined }) =>
|
||||||
|
process.itemId !== item.Id,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("completeCallback ~ end");
|
||||||
|
},
|
||||||
|
[processes, setProcesses],
|
||||||
|
);
|
||||||
|
|
||||||
|
const statisticsCallback = useCallback(
|
||||||
|
(statistics: Statistics, item: BaseItemDto) => {
|
||||||
|
const videoLength =
|
||||||
|
(item.MediaSources?.[0]?.RunTimeTicks || 0) / 10000000; // In seconds
|
||||||
|
const fps = item.MediaStreams?.[0]?.RealFrameRate || 25;
|
||||||
|
const totalFrames = videoLength * fps;
|
||||||
|
const processedFrames = statistics.getVideoFrameNumber();
|
||||||
|
const speed = statistics.getSpeed();
|
||||||
|
|
||||||
|
const percentage =
|
||||||
|
totalFrames > 0 ? Math.floor((processedFrames / totalFrames) * 100) : 0;
|
||||||
|
|
||||||
|
if (!item.Id) throw new Error("Item is undefined");
|
||||||
|
setProcesses((prev: JobStatus[]) => {
|
||||||
|
return prev.map((process: JobStatus) => {
|
||||||
|
if (process.itemId === item.Id) {
|
||||||
|
return {
|
||||||
|
...process,
|
||||||
|
id: statistics.getSessionId().toString(),
|
||||||
|
progress: percentage,
|
||||||
|
speed: Math.max(speed, 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return process;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setProcesses, completeCallback],
|
||||||
|
);
|
||||||
|
|
||||||
|
const startRemuxing = useCallback(
|
||||||
|
async (item: BaseItemDto, url: string, mediaSource: MediaSourceInfo) => {
|
||||||
|
const cacheDir = await FileSystem.getInfoAsync(
|
||||||
|
APP_CACHE_DOWNLOAD_DIRECTORY,
|
||||||
|
);
|
||||||
|
if (!cacheDir.exists) {
|
||||||
|
await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {
|
||||||
|
intermediates: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = `${APP_CACHE_DOWNLOAD_DIRECTORY}${item.Id}.mp4`;
|
||||||
|
|
||||||
|
if (!api) throw new Error("API is not defined");
|
||||||
|
if (!item.Id) throw new Error("Item must have an Id");
|
||||||
|
|
||||||
|
// First lets save any important assets we want to present to the user offline
|
||||||
|
await onSaveAssets(api, item);
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
t("home.downloads.toasts.download_started_for", { item: item.Name }),
|
||||||
|
{
|
||||||
|
action: {
|
||||||
|
label: "Go to download",
|
||||||
|
onClick: () => {
|
||||||
|
router.push("/downloads");
|
||||||
|
toast.dismiss();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const job: JobStatus = {
|
||||||
|
id: "",
|
||||||
|
deviceId: "",
|
||||||
|
inputUrl: url,
|
||||||
|
item: item,
|
||||||
|
itemId: item.Id!,
|
||||||
|
outputPath: output,
|
||||||
|
progress: 0,
|
||||||
|
status: "downloading",
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
writeInfoLog(`useRemuxHlsToMp4 ~ startRemuxing for item ${item.Name}`);
|
||||||
|
setProcesses((prev: any) => [...prev, job]);
|
||||||
|
|
||||||
|
await FFmpegKit.executeAsync(
|
||||||
|
createFFmpegCommand(url, output).join(" "),
|
||||||
|
(session: any) => completeCallback(session, item),
|
||||||
|
undefined,
|
||||||
|
(s: any) => statisticsCallback(s, item),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as Error;
|
||||||
|
console.error("Failed to remux:", error);
|
||||||
|
writeErrorLog(
|
||||||
|
`useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name},
|
||||||
|
Error: ${error.message}, Stack: ${error.stack}`,
|
||||||
|
);
|
||||||
|
setProcesses((prev: any[]) => {
|
||||||
|
return prev.filter(
|
||||||
|
(process: { itemId: string | undefined }) =>
|
||||||
|
process.itemId !== item.Id,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
throw error; // Re-throw the error to propagate it to the caller
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[settings, processes, setProcesses, completeCallback, statisticsCallback],
|
||||||
|
);
|
||||||
|
|
||||||
|
const cancelRemuxing = useCallback(() => {
|
||||||
|
FFmpegKit.cancel();
|
||||||
|
setProcesses([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { startRemuxing, cancelRemuxing };
|
||||||
|
};
|
||||||
@@ -44,27 +44,3 @@ export const useSessions = ({
|
|||||||
|
|
||||||
return { sessions: data, isLoading };
|
return { sessions: data, isLoading };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAllSessions = ({
|
|
||||||
refetchInterval = 5 * 1000,
|
|
||||||
activeWithinSeconds = 360,
|
|
||||||
}: useSessionsProps) => {
|
|
||||||
const [api] = useAtom(apiAtom);
|
|
||||||
const [user] = useAtom(userAtom);
|
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
|
||||||
queryKey: ["allSessions"],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api || !user || !user.Policy?.IsAdministrator) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const response = await getSessionApi(api).getSessions({
|
|
||||||
activeWithinSeconds: activeWithinSeconds,
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
refetchInterval: refetchInterval,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { sessions: data, isLoading };
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -9,38 +9,6 @@ interface UseWebSocketProps {
|
|||||||
togglePlay: () => void;
|
togglePlay: () => void;
|
||||||
stopPlayback: () => void;
|
stopPlayback: () => void;
|
||||||
offline: boolean;
|
offline: boolean;
|
||||||
|
|
||||||
nextTrack?: () => void;
|
|
||||||
previousTrack?: () => void;
|
|
||||||
rewindPlayback?: () => void;
|
|
||||||
fastForwardPlayback?: () => void;
|
|
||||||
seekPlayback?: (positionTicks: number) => void;
|
|
||||||
volumeUp?: () => void;
|
|
||||||
volumeDown?: () => void;
|
|
||||||
toggleMute?: () => void;
|
|
||||||
toggleOsd?: () => void;
|
|
||||||
toggleFullscreen?: () => void;
|
|
||||||
goHome?: () => void;
|
|
||||||
goToSettings?: () => void;
|
|
||||||
setAudioStreamIndex?: (index: number) => void;
|
|
||||||
setSubtitleStreamIndex?: (index: number) => void;
|
|
||||||
|
|
||||||
moveUp?: () => void;
|
|
||||||
moveDown?: () => void;
|
|
||||||
moveLeft?: () => void;
|
|
||||||
moveRight?: () => void;
|
|
||||||
select?: () => void;
|
|
||||||
pageUp?: () => void;
|
|
||||||
pageDown?: () => void;
|
|
||||||
setVolume?: (volume: number) => void;
|
|
||||||
setRepeatMode?: (mode: string) => void;
|
|
||||||
setShuffleMode?: (mode: string) => void;
|
|
||||||
togglePictureInPicture?: () => void;
|
|
||||||
takeScreenshot?: () => void;
|
|
||||||
sendString?: (text: string) => void;
|
|
||||||
sendKey?: (key: string) => void;
|
|
||||||
playMediaSource?: (itemIds: string[], startPositionTicks?: number) => void;
|
|
||||||
playTrailers?: (itemId: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWebSocket = ({
|
export const useWebSocket = ({
|
||||||
@@ -48,270 +16,38 @@ export const useWebSocket = ({
|
|||||||
togglePlay,
|
togglePlay,
|
||||||
stopPlayback,
|
stopPlayback,
|
||||||
offline,
|
offline,
|
||||||
nextTrack,
|
|
||||||
previousTrack,
|
|
||||||
rewindPlayback,
|
|
||||||
fastForwardPlayback,
|
|
||||||
seekPlayback,
|
|
||||||
volumeUp,
|
|
||||||
volumeDown,
|
|
||||||
toggleMute,
|
|
||||||
toggleOsd,
|
|
||||||
toggleFullscreen,
|
|
||||||
goHome,
|
|
||||||
goToSettings,
|
|
||||||
setAudioStreamIndex,
|
|
||||||
setSubtitleStreamIndex,
|
|
||||||
moveUp,
|
|
||||||
moveDown,
|
|
||||||
moveLeft,
|
|
||||||
moveRight,
|
|
||||||
select,
|
|
||||||
pageUp,
|
|
||||||
pageDown,
|
|
||||||
setVolume,
|
|
||||||
setRepeatMode,
|
|
||||||
setShuffleMode,
|
|
||||||
togglePictureInPicture,
|
|
||||||
takeScreenshot,
|
|
||||||
sendString,
|
|
||||||
sendKey,
|
|
||||||
playMediaSource,
|
|
||||||
playTrailers,
|
|
||||||
}: UseWebSocketProps) => {
|
}: UseWebSocketProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { lastMessage } = useWebSocketContext();
|
const { ws } = useWebSocketContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { clearLastMessage } = useWebSocketContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastMessage) return;
|
if (!ws) return;
|
||||||
if (offline) return;
|
if (offline) return;
|
||||||
|
|
||||||
const messageType = lastMessage.MessageType;
|
ws.onmessage = (e) => {
|
||||||
const command: string | undefined =
|
const json = JSON.parse(e.data);
|
||||||
lastMessage?.Data?.Command || lastMessage?.Data?.Name;
|
const command = json?.Data?.Command;
|
||||||
|
|
||||||
const args = lastMessage?.Data?.Arguments as
|
console.log("[WS] ~ ", json);
|
||||||
| Record<string, string>
|
|
||||||
| undefined; // Arguments are Dictionary<string, string>
|
|
||||||
|
|
||||||
console.log("[WS] ~ ", lastMessage);
|
if (command === "PlayPause") {
|
||||||
|
console.log("Command ~ PlayPause");
|
||||||
if (command === "PlayPause") {
|
|
||||||
console.log("Command ~ PlayPause");
|
|
||||||
togglePlay();
|
|
||||||
} else if (command === "Stop") {
|
|
||||||
console.log("Command ~ Stop");
|
|
||||||
stopPlayback();
|
|
||||||
router.canGoBack() && router.back();
|
|
||||||
} else if (command === "Pause") {
|
|
||||||
console.log("Command ~ Pause");
|
|
||||||
if (isPlaying) {
|
|
||||||
togglePlay();
|
togglePlay();
|
||||||
|
} else if (command === "Stop") {
|
||||||
|
console.log("Command ~ Stop");
|
||||||
|
stopPlayback();
|
||||||
|
router.canGoBack() && router.back();
|
||||||
|
} else if (json?.Data?.Name === "DisplayMessage") {
|
||||||
|
console.log("Command ~ DisplayMessage");
|
||||||
|
const title = json?.Data?.Arguments?.Header;
|
||||||
|
const body = json?.Data?.Arguments?.Text;
|
||||||
|
Alert.alert(t("player.message_from_server", { message: title }), body);
|
||||||
}
|
}
|
||||||
} else if (command === "Unpause") {
|
};
|
||||||
console.log("Command ~ Unpause");
|
|
||||||
if (!isPlaying) {
|
|
||||||
togglePlay();
|
|
||||||
}
|
|
||||||
} else if (command === "NextTrack") {
|
|
||||||
console.log("Command ~ NextTrack");
|
|
||||||
nextTrack?.();
|
|
||||||
} else if (command === "PreviousTrack") {
|
|
||||||
console.log("Command ~ PreviousTrack");
|
|
||||||
previousTrack?.();
|
|
||||||
} else if (command === "Rewind") {
|
|
||||||
console.log("Command ~ Rewind");
|
|
||||||
rewindPlayback?.();
|
|
||||||
} else if (command === "FastForward") {
|
|
||||||
console.log("Command ~ FastForward");
|
|
||||||
fastForwardPlayback?.();
|
|
||||||
} else if (command === "Seek") {
|
|
||||||
const positionStr = args?.SeekPositionTicks;
|
|
||||||
console.log("Command ~ Seek", { positionStr });
|
|
||||||
if (positionStr) {
|
|
||||||
const position = Number.parseInt(positionStr, 10);
|
|
||||||
if (!Number.isNaN(position)) {
|
|
||||||
seekPlayback?.(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (command === "Back") {
|
|
||||||
console.log("Command ~ Back");
|
|
||||||
if (router.canGoBack()) {
|
|
||||||
router.back();
|
|
||||||
}
|
|
||||||
} else if (command === "GoHome") {
|
|
||||||
console.log("Command ~ GoHome");
|
|
||||||
goHome ? goHome() : router.push("/");
|
|
||||||
} else if (command === "GoToSettings") {
|
|
||||||
console.log("Command ~ GoToSettings");
|
|
||||||
goToSettings ? goToSettings() : router.push("/settings");
|
|
||||||
} else if (command === "VolumeUp") {
|
|
||||||
console.log("Command ~ VolumeUp");
|
|
||||||
volumeUp?.();
|
|
||||||
} else if (command === "VolumeDown") {
|
|
||||||
console.log("Command ~ VolumeDown");
|
|
||||||
volumeDown?.();
|
|
||||||
} else if (command === "ToggleMute") {
|
|
||||||
console.log("Command ~ ToggleMute");
|
|
||||||
|
|
||||||
toggleMute?.();
|
return () => {
|
||||||
} else if (command === "ToggleOsd") {
|
ws.onmessage = null;
|
||||||
console.log("Command ~ ToggleOsd");
|
};
|
||||||
toggleOsd?.();
|
}, [ws, stopPlayback, togglePlay, isPlaying, router]);
|
||||||
} else if (command === "ToggleFullscreen") {
|
|
||||||
console.log("Command ~ ToggleFullscreen");
|
|
||||||
toggleFullscreen?.();
|
|
||||||
} else if (command === "SetAudioStreamIndex") {
|
|
||||||
const indexStr = args?.Index;
|
|
||||||
console.log("Command ~ SetAudioStreamIndex", { indexStr });
|
|
||||||
if (indexStr) {
|
|
||||||
const index = Number.parseInt(indexStr, 10);
|
|
||||||
if (!Number.isNaN(index)) {
|
|
||||||
setAudioStreamIndex?.(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (command === "SetSubtitleStreamIndex") {
|
|
||||||
const indexStr = args?.Index;
|
|
||||||
console.log("Command ~ SetSubtitleStreamIndex", { indexStr });
|
|
||||||
if (indexStr) {
|
|
||||||
const index = Number.parseInt(indexStr, 10);
|
|
||||||
if (!Number.isNaN(index)) {
|
|
||||||
setSubtitleStreamIndex?.(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Neue Befehle hier implementieren
|
|
||||||
else if (command === "MoveUp") {
|
|
||||||
console.log("Command ~ MoveUp");
|
|
||||||
moveUp?.();
|
|
||||||
} else if (command === "MoveDown") {
|
|
||||||
console.log("Command ~ MoveDown");
|
|
||||||
moveDown?.();
|
|
||||||
} else if (command === "MoveLeft") {
|
|
||||||
console.log("Command ~ MoveLeft");
|
|
||||||
moveLeft?.();
|
|
||||||
} else if (command === "MoveRight") {
|
|
||||||
console.log("Command ~ MoveRight");
|
|
||||||
moveRight?.();
|
|
||||||
} else if (command === "Select") {
|
|
||||||
console.log("Command ~ Select");
|
|
||||||
select?.();
|
|
||||||
} else if (command === "PageUp") {
|
|
||||||
console.log("Command ~ PageUp");
|
|
||||||
pageUp?.();
|
|
||||||
} else if (command === "PageDown") {
|
|
||||||
console.log("Command ~ PageDown");
|
|
||||||
pageDown?.();
|
|
||||||
} else if (command === "SetVolume") {
|
|
||||||
const volumeStr = args?.Volume;
|
|
||||||
console.log("Command ~ SetVolume", { volumeStr });
|
|
||||||
if (volumeStr) {
|
|
||||||
const volumeValue = Number.parseInt(volumeStr, 10);
|
|
||||||
if (!Number.isNaN(volumeValue)) {
|
|
||||||
setVolume?.(volumeValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (command === "SetRepeatMode") {
|
|
||||||
const mode = args?.Mode;
|
|
||||||
console.log("Command ~ SetRepeatMode", { mode });
|
|
||||||
if (mode) {
|
|
||||||
setRepeatMode?.(mode);
|
|
||||||
}
|
|
||||||
} else if (command === "SetShuffleMode") {
|
|
||||||
const mode = args?.Mode;
|
|
||||||
console.log("Command ~ SetShuffleMode", { mode });
|
|
||||||
if (mode) {
|
|
||||||
setShuffleMode?.(mode);
|
|
||||||
}
|
|
||||||
} else if (command === "TogglePictureInPicture") {
|
|
||||||
console.log("Command ~ TogglePictureInPicture");
|
|
||||||
togglePictureInPicture?.();
|
|
||||||
} else if (command === "TakeScreenshot") {
|
|
||||||
console.log("Command ~ TakeScreenshot");
|
|
||||||
takeScreenshot?.();
|
|
||||||
} else if (command === "SendString") {
|
|
||||||
const text = args?.Text;
|
|
||||||
console.log("Command ~ SendString", { text });
|
|
||||||
if (text) {
|
|
||||||
sendString?.(text);
|
|
||||||
}
|
|
||||||
} else if (command === "SendKey") {
|
|
||||||
const key = args?.Key;
|
|
||||||
console.log("Command ~ SendKey", { key });
|
|
||||||
if (key) {
|
|
||||||
sendKey?.(key);
|
|
||||||
}
|
|
||||||
} else if (command === "PlayMediaSource") {
|
|
||||||
const itemIdsStr = args?.ItemIds;
|
|
||||||
const startPositionTicksStr = args?.StartPositionTicks;
|
|
||||||
console.log("Command ~ PlayMediaSource", {
|
|
||||||
itemIdsStr,
|
|
||||||
startPositionTicksStr,
|
|
||||||
});
|
|
||||||
if (itemIdsStr) {
|
|
||||||
const itemIds = itemIdsStr.split(",");
|
|
||||||
let startPositionTicks: number | undefined = undefined;
|
|
||||||
if (startPositionTicksStr) {
|
|
||||||
const parsedTicks = Number.parseInt(startPositionTicksStr, 10);
|
|
||||||
if (!Number.isNaN(parsedTicks)) {
|
|
||||||
startPositionTicks = parsedTicks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
playMediaSource?.(itemIds, startPositionTicks);
|
|
||||||
}
|
|
||||||
} else if (command === "PlayTrailers") {
|
|
||||||
const itemId = args?.ItemId;
|
|
||||||
console.log("Command ~ PlayTrailers", { itemId });
|
|
||||||
if (itemId) {
|
|
||||||
playTrailers?.(itemId);
|
|
||||||
}
|
|
||||||
} else if (command === "DisplayMessage") {
|
|
||||||
console.log("Command ~ DisplayMessage");
|
|
||||||
const title = args?.Header;
|
|
||||||
const body = args?.Text;
|
|
||||||
Alert.alert(t("player.message_from_server", { message: title }), body);
|
|
||||||
}
|
|
||||||
clearLastMessage();
|
|
||||||
}, [
|
|
||||||
lastMessage,
|
|
||||||
offline,
|
|
||||||
isPlaying,
|
|
||||||
togglePlay,
|
|
||||||
stopPlayback,
|
|
||||||
router,
|
|
||||||
nextTrack,
|
|
||||||
previousTrack,
|
|
||||||
rewindPlayback,
|
|
||||||
fastForwardPlayback,
|
|
||||||
seekPlayback,
|
|
||||||
volumeUp,
|
|
||||||
volumeDown,
|
|
||||||
toggleMute,
|
|
||||||
toggleOsd,
|
|
||||||
toggleFullscreen,
|
|
||||||
goHome,
|
|
||||||
goToSettings,
|
|
||||||
setAudioStreamIndex,
|
|
||||||
setSubtitleStreamIndex,
|
|
||||||
moveUp,
|
|
||||||
moveDown,
|
|
||||||
moveLeft,
|
|
||||||
moveRight,
|
|
||||||
select,
|
|
||||||
pageUp,
|
|
||||||
pageDown,
|
|
||||||
setVolume,
|
|
||||||
setRepeatMode,
|
|
||||||
setShuffleMode,
|
|
||||||
togglePictureInPicture,
|
|
||||||
takeScreenshot,
|
|
||||||
sendString,
|
|
||||||
sendKey,
|
|
||||||
playMediaSource,
|
|
||||||
playTrailers,
|
|
||||||
t,
|
|
||||||
clearLastMessage,
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|||||||
15
i18n.ts
15
i18n.ts
@@ -4,7 +4,6 @@ import { initReactI18next } from "react-i18next";
|
|||||||
import { getLocales } from "expo-localization";
|
import { getLocales } from "expo-localization";
|
||||||
import de from "./translations/de.json";
|
import de from "./translations/de.json";
|
||||||
import en from "./translations/en.json";
|
import en from "./translations/en.json";
|
||||||
import eo from "./translations/eo.json";
|
|
||||||
import es from "./translations/es.json";
|
import es from "./translations/es.json";
|
||||||
import fr from "./translations/fr.json";
|
import fr from "./translations/fr.json";
|
||||||
import it from "./translations/it.json";
|
import it from "./translations/it.json";
|
||||||
@@ -12,11 +11,10 @@ import ja from "./translations/ja.json";
|
|||||||
import nl from "./translations/nl.json";
|
import nl from "./translations/nl.json";
|
||||||
import pl from "./translations/pl.json";
|
import pl from "./translations/pl.json";
|
||||||
import ptBR from "./translations/pt-BR.json";
|
import ptBR from "./translations/pt-BR.json";
|
||||||
import ru from "./translations/ru.json";
|
|
||||||
import sv from "./translations/sv.json";
|
import sv from "./translations/sv.json";
|
||||||
import tlh from "./translations/tlh.json";
|
import ru from "./translations/ru.json";
|
||||||
import tr from "./translations/tr.json";
|
import tr from "./translations/tr.json";
|
||||||
import uk from "./translations/uk.json";
|
import ua from "./translations/ua.json";
|
||||||
import zhCN from "./translations/zh-CN.json";
|
import zhCN from "./translations/zh-CN.json";
|
||||||
import zhTW from "./translations/zh-TW.json";
|
import zhTW from "./translations/zh-TW.json";
|
||||||
|
|
||||||
@@ -24,19 +22,16 @@ export const APP_LANGUAGES = [
|
|||||||
{ label: "Deutsch", value: "de" },
|
{ label: "Deutsch", value: "de" },
|
||||||
{ label: "English", value: "en" },
|
{ label: "English", value: "en" },
|
||||||
{ label: "Español", value: "es" },
|
{ label: "Español", value: "es" },
|
||||||
{ label: "Esperanto", value: "eo" },
|
|
||||||
{ label: "Français", value: "fr" },
|
{ label: "Français", value: "fr" },
|
||||||
{ label: "Italiano", value: "it" },
|
{ label: "Italiano", value: "it" },
|
||||||
{ label: "日本語", value: "ja" },
|
{ label: "日本語", value: "ja" },
|
||||||
{ label: "Klingon", value: "tlh" },
|
|
||||||
{ label: "Türkçe", value: "tr" },
|
{ label: "Türkçe", value: "tr" },
|
||||||
{ label: "Nederlands", value: "nl" },
|
{ label: "Nederlands", value: "nl" },
|
||||||
{ label: "Polski", value: "pl" },
|
{ label: "Polski", value: "pl" },
|
||||||
{ label: "Português (Brasil)", value: "pt-BR" },
|
{ label: "Português (Brasil)", value: "pt-BR" },
|
||||||
{ label: "Svenska", value: "sv" },
|
{ label: "Svenska", value: "sv" },
|
||||||
{ label: "Русский", value: "ru" },
|
{ label: "Русский", value: "ru" },
|
||||||
{ label: "Українська", value: "uk" },
|
{ label: "Українська", value: "ua" },
|
||||||
{ label: "Українська", value: "uk" },
|
|
||||||
{ label: "简体中文", value: "zh-CN" },
|
{ label: "简体中文", value: "zh-CN" },
|
||||||
{ label: "繁體中文", value: "zh-TW" },
|
{ label: "繁體中文", value: "zh-TW" },
|
||||||
];
|
];
|
||||||
@@ -47,7 +42,6 @@ i18n.use(initReactI18next).init({
|
|||||||
de: { translation: de },
|
de: { translation: de },
|
||||||
en: { translation: en },
|
en: { translation: en },
|
||||||
es: { translation: es },
|
es: { translation: es },
|
||||||
eo: { translation: eo },
|
|
||||||
fr: { translation: fr },
|
fr: { translation: fr },
|
||||||
it: { translation: it },
|
it: { translation: it },
|
||||||
ja: { translation: ja },
|
ja: { translation: ja },
|
||||||
@@ -57,8 +51,7 @@ i18n.use(initReactI18next).init({
|
|||||||
sv: { translation: sv },
|
sv: { translation: sv },
|
||||||
ru: { translation: ru },
|
ru: { translation: ru },
|
||||||
tr: { translation: tr },
|
tr: { translation: tr },
|
||||||
tlh: { translation: tlh },
|
ua: { translation: ua },
|
||||||
uk: { translation: uk },
|
|
||||||
"zh-CN": { translation: zhCN },
|
"zh-CN": { translation: zhCN },
|
||||||
"zh-TW": { translation: zhTW },
|
"zh-TW": { translation: zhTW },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import ExpoModulesCore
|
import ExpoModulesCore
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
import TVVLCKit
|
import TVVLCKit
|
||||||
#else
|
#else
|
||||||
import MobileVLCKit
|
import MobileVLCKit
|
||||||
#endif
|
#endif
|
||||||
|
import UIKit
|
||||||
|
|
||||||
class VlcPlayer3View: ExpoView {
|
class VlcPlayer3View: ExpoView {
|
||||||
private var mediaPlayer: VLCMediaPlayer?
|
private var mediaPlayer: VLCMediaPlayer?
|
||||||
@@ -16,7 +16,7 @@ class VlcPlayer3View: ExpoView {
|
|||||||
private var lastReportedIsPlaying: Bool?
|
private var lastReportedIsPlaying: Bool?
|
||||||
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
||||||
private var startPosition: Int32 = 0
|
private var startPosition: Int32 = 0
|
||||||
private var externalSubtitles: [[String: String]]?
|
private var isMediaReady: Bool = false
|
||||||
private var externalTrack: [String: String]?
|
private var externalTrack: [String: String]?
|
||||||
private var progressTimer: DispatchSourceTimer?
|
private var progressTimer: DispatchSourceTimer?
|
||||||
private var isStopping: Bool = false // Define isStopping here
|
private var isStopping: Bool = false // Define isStopping here
|
||||||
@@ -61,7 +61,7 @@ class VlcPlayer3View: ExpoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public Methods
|
// MARK: - Public Methods
|
||||||
func startPictureInPicture() {}
|
func startPictureInPicture() { }
|
||||||
|
|
||||||
@objc func play() {
|
@objc func play() {
|
||||||
self.mediaPlayer?.play()
|
self.mediaPlayer?.play()
|
||||||
@@ -109,7 +109,6 @@ class VlcPlayer3View: ExpoView {
|
|||||||
self.externalTrack = source["externalTrack"] as? [String: String]
|
self.externalTrack = source["externalTrack"] as? [String: String]
|
||||||
var initOptions = source["initOptions"] as? [Any] ?? []
|
var initOptions = source["initOptions"] as? [Any] ?? []
|
||||||
self.startPosition = source["startPosition"] as? Int32 ?? 0
|
self.startPosition = source["startPosition"] as? Int32 ?? 0
|
||||||
self.externalSubtitles = source["externalSubtitles"] as? [[String: String]]
|
|
||||||
initOptions.append("--start-time=\(self.startPosition)")
|
initOptions.append("--start-time=\(self.startPosition)")
|
||||||
|
|
||||||
guard let uri = source["uri"] as? String, !uri.isEmpty else {
|
guard let uri = source["uri"] as? String, !uri.isEmpty else {
|
||||||
@@ -144,8 +143,8 @@ class VlcPlayer3View: ExpoView {
|
|||||||
media.addOptions(mediaOptions)
|
media.addOptions(mediaOptions)
|
||||||
|
|
||||||
self.mediaPlayer?.media = media
|
self.mediaPlayer?.media = media
|
||||||
self.setInitialExternalSubtitles()
|
|
||||||
self.hasSource = true
|
self.hasSource = true
|
||||||
|
|
||||||
if autoplay {
|
if autoplay {
|
||||||
print("Playing...")
|
print("Playing...")
|
||||||
self.play()
|
self.play()
|
||||||
@@ -183,9 +182,9 @@ class VlcPlayer3View: ExpoView {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: false)
|
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
||||||
if let result = result {
|
if let result = result {
|
||||||
let internalName = "Track \(self.customSubtitles.count)"
|
let internalName = "Track \(self.customSubtitles.count + 1)"
|
||||||
print("Subtitle added with result: \(result) \(internalName)")
|
print("Subtitle added with result: \(result) \(internalName)")
|
||||||
self.customSubtitles.append((internalName: internalName, originalName: name))
|
self.customSubtitles.append((internalName: internalName, originalName: name))
|
||||||
} else {
|
} else {
|
||||||
@@ -193,19 +192,6 @@ class VlcPlayer3View: ExpoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setInitialExternalSubtitles() {
|
|
||||||
if let externalSubtitles = self.externalSubtitles {
|
|
||||||
for subtitle in externalSubtitles {
|
|
||||||
if let subtitleName = subtitle["name"],
|
|
||||||
let subtitleURL = subtitle["DeliveryUrl"]
|
|
||||||
{
|
|
||||||
print("Setting external subtitle: \(subtitleName) \(subtitleURL)")
|
|
||||||
self.setSubtitleURL(subtitleURL, name: subtitleName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func getSubtitleTracks() -> [[String: Any]]? {
|
@objc func getSubtitleTracks() -> [[String: Any]]? {
|
||||||
guard let mediaPlayer = self.mediaPlayer else {
|
guard let mediaPlayer = self.mediaPlayer else {
|
||||||
return nil
|
return nil
|
||||||
@@ -290,6 +276,16 @@ class VlcPlayer3View: ExpoView {
|
|||||||
|
|
||||||
print("Debug: Current time: \(currentTimeMs)")
|
print("Debug: Current time: \(currentTimeMs)")
|
||||||
if currentTimeMs >= 0 && currentTimeMs < durationMs {
|
if currentTimeMs >= 0 && currentTimeMs < durationMs {
|
||||||
|
if player.isPlaying && !self.isMediaReady {
|
||||||
|
self.isMediaReady = true
|
||||||
|
// Set external track subtitle when starting.
|
||||||
|
if let externalTrack = self.externalTrack {
|
||||||
|
if let name = externalTrack["name"], !name.isEmpty {
|
||||||
|
let deliveryUrl = externalTrack["DeliveryUrl"] ?? ""
|
||||||
|
self.setSubtitleURL(deliveryUrl, name: name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
self.onVideoProgress?([
|
self.onVideoProgress?([
|
||||||
"currentTime": currentTimeMs,
|
"currentTime": currentTimeMs,
|
||||||
"duration": durationMs,
|
"duration": durationMs,
|
||||||
|
|||||||
23
package.json
23
package.json
@@ -6,21 +6,21 @@
|
|||||||
"submodule-reload": "git submodule update --init --remote --recursive",
|
"submodule-reload": "git submodule update --init --remote --recursive",
|
||||||
"clean": "echo y | expo prebuild --clean",
|
"clean": "echo y | expo prebuild --clean",
|
||||||
"start": "bun run submodule-reload && expo start",
|
"start": "bun run submodule-reload && expo start",
|
||||||
"ios": "cross-env EXPO_TV=0 expo run:ios",
|
"ios": "EXPO_TV=0 expo run:ios",
|
||||||
"ios:tv": "cross-env EXPO_TV=1 expo run:ios",
|
"ios:tv": "EXPO_TV=1 expo run:ios",
|
||||||
"android": "cross-env EXPO_TV=0 expo run:android",
|
"android": "EXPO_TV=0 expo run:android",
|
||||||
"android:tv": "cross-env EXPO_TV=1 expo run:android",
|
"android:tv": "EXPO_TV=1 expo run:android",
|
||||||
"prebuild": "cross-env EXPO_TV=0 bun run clean",
|
"prebuild": "EXPO_TV=0 bun run clean",
|
||||||
"prebuild:tv": "cross-env EXPO_TV=1 bun run clean",
|
"prebuild:tv": "EXPO_TV=1 bun run clean",
|
||||||
"build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease",
|
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"check": "biome check .",
|
"check": "biome check .",
|
||||||
"lint": "biome check --write --unsafe"
|
"lint": "biome check --write --unsafe"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bottom-tabs/react-navigation": "0.8.6",
|
"@bottom-tabs/react-navigation": "0.8.6",
|
||||||
|
"@config-plugins/ffmpeg-kit-react-native": "^9.0.0",
|
||||||
"@expo/config-plugins": "~9.0.15",
|
"@expo/config-plugins": "~9.0.15",
|
||||||
"@expo/react-native-action-sheet": "^4.1.1",
|
"@expo/react-native-action-sheet": "^4.1.0",
|
||||||
"@expo/vector-icons": "^14.0.4",
|
"@expo/vector-icons": "^14.0.4",
|
||||||
"@futurejj/react-native-visibility-sensor": "^1.3.10",
|
"@futurejj/react-native-visibility-sensor": "^1.3.10",
|
||||||
"@gorhom/bottom-sheet": "^5.1.0",
|
"@gorhom/bottom-sheet": "^5.1.0",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"expo-router": "~4.0.17",
|
"expo-router": "~4.0.17",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
"expo-sensors": "~14.0.2",
|
"expo-sensors": "~14.0.2",
|
||||||
"expo-sharing": "~13.0.1",
|
"expo-sharing": "~13.1.0",
|
||||||
"expo-splash-screen": "~0.29.22",
|
"expo-splash-screen": "~0.29.22",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
"expo-system-ui": "~4.0.8",
|
"expo-system-ui": "~4.0.8",
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-native": "npm:react-native-tvos@~0.77.2-0",
|
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
"react-native-bottom-tabs": "0.8.6",
|
"react-native-bottom-tabs": "0.8.6",
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
@@ -121,7 +121,6 @@
|
|||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"@types/react-test-renderer": "^19.0.0",
|
"@types/react-test-renderer": "^19.0.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^15.5.0",
|
"lint-staged": "^15.5.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
@@ -136,6 +135,6 @@
|
|||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx}": ["biome check --write --unsafe"],
|
"*.{js,jsx,ts,tsx}": ["biome check --write --unsafe"],
|
||||||
"*.{json}": ["biome format --write"]
|
"*.{json,md}": ["biome format --write"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import useImageStorage from "@/hooks/useImageStorage";
|
import useImageStorage from "@/hooks/useImageStorage";
|
||||||
import { useInterval } from "@/hooks/useInterval";
|
|
||||||
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
|
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
|
||||||
import { getOrSetDeviceId } from "@/utils/device";
|
import { getOrSetDeviceId } from "@/utils/device";
|
||||||
import useDownloadHelper from "@/utils/download";
|
import useDownloadHelper from "@/utils/download";
|
||||||
@@ -19,7 +18,6 @@ import type {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
|
||||||
import BackGroundDownloader from "@kesha-antonov/react-native-background-downloader";
|
import BackGroundDownloader from "@kesha-antonov/react-native-background-downloader";
|
||||||
import { focusManager, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { focusManager, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -40,7 +38,6 @@ import {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AppState, type AppStateStatus, Platform } from "react-native";
|
import { AppState, type AppStateStatus, Platform } from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { Bitrate } from "../components/BitrateSelector";
|
|
||||||
import { apiAtom } from "./JellyfinProvider";
|
import { apiAtom } from "./JellyfinProvider";
|
||||||
|
|
||||||
export type DownloadedItem = {
|
export type DownloadedItem = {
|
||||||
@@ -69,7 +66,7 @@ function useDownloadProvider() {
|
|||||||
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
||||||
const { saveImage } = useImageStorage();
|
const { saveImage } = useImageStorage();
|
||||||
|
|
||||||
let [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
||||||
|
|
||||||
const successHapticFeedback = useHaptic("success");
|
const successHapticFeedback = useHaptic("success");
|
||||||
|
|
||||||
@@ -77,17 +74,6 @@ function useDownloadProvider() {
|
|||||||
return api?.accessToken;
|
return api?.accessToken;
|
||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
const usingOptimizedServer = useMemo(
|
|
||||||
() => settings?.downloadMethod === DownloadMethod.Optimized,
|
|
||||||
[settings],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getDownloadUrl = (process: JobStatus) => {
|
|
||||||
return usingOptimizedServer
|
|
||||||
? `${settings.optimizedVersionsServerUrl}download/${process.id}`
|
|
||||||
: process.inputUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: downloadedFiles, refetch } = useQuery({
|
const { data: downloadedFiles, refetch } = useQuery({
|
||||||
queryKey: ["downloadedItems"],
|
queryKey: ["downloadedItems"],
|
||||||
queryFn: getAllDownloadedItems,
|
queryFn: getAllDownloadedItems,
|
||||||
@@ -178,64 +164,6 @@ function useDownloadProvider() {
|
|||||||
enabled: settings?.downloadMethod === DownloadMethod.Optimized,
|
enabled: settings?.downloadMethod === DownloadMethod.Optimized,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Cant use the background downloader callback. As its not triggered if size is unknown.
|
|
||||||
const updateProgress = async () => {
|
|
||||||
if (settings?.downloadMethod === DownloadMethod.Optimized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// const response = await getSessionApi(api).getSessions({
|
|
||||||
// activeWithinSeconds: 300,
|
|
||||||
// });
|
|
||||||
|
|
||||||
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
|
||||||
|
|
||||||
// check if processes are missing
|
|
||||||
const missingProcesses = tasks
|
|
||||||
.filter((t) => !processes.some((p) => p.id === t.id))
|
|
||||||
.map((t) => {
|
|
||||||
return t.metadata;
|
|
||||||
});
|
|
||||||
|
|
||||||
processes = [...processes, ...missingProcesses];
|
|
||||||
|
|
||||||
const updatedProcesses = processes.map((p) => {
|
|
||||||
// const result = response.data.find((s) => s.Id == p.sessionId);
|
|
||||||
// if (result) {
|
|
||||||
// return {
|
|
||||||
// ...p,
|
|
||||||
// progress: result.TranscodingInfo?.CompletionPercentage,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fallback. Doesn't really work for transcodes as they may be a lot smaller.
|
|
||||||
// We make an wild guess by comparing bitrates
|
|
||||||
const task = tasks.find((s) => s.id === p.id);
|
|
||||||
if (task) {
|
|
||||||
let progress = p.progress;
|
|
||||||
let size = p.mediaSource.Size;
|
|
||||||
const maxBitrate = p.maxBitrate.value;
|
|
||||||
if (maxBitrate && maxBitrate < p.mediaSource.Bitrate) {
|
|
||||||
size = (size / p.mediaSource.Bitrate) * maxBitrate;
|
|
||||||
}
|
|
||||||
progress = (100 / size) * task.bytesDownloaded;
|
|
||||||
if (progress >= 100) {
|
|
||||||
progress = 99;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...p,
|
|
||||||
progress,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
|
|
||||||
setProcesses(updatedProcesses);
|
|
||||||
};
|
|
||||||
|
|
||||||
useInterval(updateProgress, 2000);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkIfShouldStartDownload = async () => {
|
const checkIfShouldStartDownload = async () => {
|
||||||
if (processes.length === 0) return;
|
if (processes.length === 0) return;
|
||||||
@@ -248,25 +176,18 @@ function useDownloadProvider() {
|
|||||||
const removeProcess = useCallback(
|
const removeProcess = useCallback(
|
||||||
async (id: string) => {
|
async (id: string) => {
|
||||||
const deviceId = await getOrSetDeviceId();
|
const deviceId = await getOrSetDeviceId();
|
||||||
if (!deviceId || !authHeader) return;
|
if (!deviceId || !authHeader || !settings?.optimizedVersionsServerUrl)
|
||||||
|
return;
|
||||||
|
|
||||||
if (usingOptimizedServer) {
|
try {
|
||||||
try {
|
await cancelJobById({
|
||||||
await cancelJobById({
|
authHeader,
|
||||||
authHeader,
|
id,
|
||||||
id,
|
url: settings?.optimizedVersionsServerUrl,
|
||||||
url: settings?.optimizedVersionsServerUrl,
|
});
|
||||||
});
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(error);
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setProcesses((prev: any[]) => {
|
|
||||||
return prev.filter(
|
|
||||||
(process: { itemId: string | undefined }) => process.id !== id,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[settings?.optimizedVersionsServerUrl, authHeader],
|
[settings?.optimizedVersionsServerUrl, authHeader],
|
||||||
);
|
);
|
||||||
@@ -317,9 +238,8 @@ function useDownloadProvider() {
|
|||||||
|
|
||||||
BackGroundDownloader?.download({
|
BackGroundDownloader?.download({
|
||||||
id: process.id,
|
id: process.id,
|
||||||
url: getDownloadUrl(process),
|
url: `${settings?.optimizedVersionsServerUrl}download/${process.id}`,
|
||||||
destination: `${baseDirectory}/${process.item.Id}.mp4`,
|
destination: `${baseDirectory}/${process.item.Id}.mp4`,
|
||||||
metadata: process,
|
|
||||||
})
|
})
|
||||||
.begin(() => {
|
.begin(() => {
|
||||||
setProcesses((prev) =>
|
setProcesses((prev) =>
|
||||||
@@ -336,9 +256,6 @@ function useDownloadProvider() {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.progress((data) => {
|
.progress((data) => {
|
||||||
if (!usingOptimizedServer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const percent = (data.bytesDownloaded / data.bytesTotal) * 100;
|
const percent = (data.bytesDownloaded / data.bytesTotal) * 100;
|
||||||
setProcesses((prev) =>
|
setProcesses((prev) =>
|
||||||
prev.map((p) =>
|
prev.map((p) =>
|
||||||
@@ -411,12 +328,7 @@ function useDownloadProvider() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const startBackgroundDownload = useCallback(
|
const startBackgroundDownload = useCallback(
|
||||||
async (
|
async (url: string, item: BaseItemDto, mediaSource: MediaSourceInfo) => {
|
||||||
url: string,
|
|
||||||
item: BaseItemDto,
|
|
||||||
mediaSource: MediaSourceInfo,
|
|
||||||
maxBitrate?: Bitrate,
|
|
||||||
) => {
|
|
||||||
if (!api || !item.Id || !authHeader)
|
if (!api || !item.Id || !authHeader)
|
||||||
throw new Error("startBackgroundDownload ~ Missing required params");
|
throw new Error("startBackgroundDownload ~ Missing required params");
|
||||||
|
|
||||||
@@ -433,42 +345,26 @@ function useDownloadProvider() {
|
|||||||
width: 500,
|
width: 500,
|
||||||
});
|
});
|
||||||
await saveImage(item.Id, itemImage?.uri);
|
await saveImage(item.Id, itemImage?.uri);
|
||||||
if (usingOptimizedServer) {
|
|
||||||
const response = await axios.post(
|
|
||||||
`${settings?.optimizedVersionsServerUrl}optimize-version`,
|
|
||||||
{
|
|
||||||
url,
|
|
||||||
fileExtension,
|
|
||||||
deviceId,
|
|
||||||
itemId: item.Id,
|
|
||||||
item,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: authHeader,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status !== 201) {
|
const response = await axios.post(
|
||||||
throw new Error("Failed to start optimization job");
|
`${settings?.optimizedVersionsServerUrl}optimize-version`,
|
||||||
}
|
{
|
||||||
} else {
|
url,
|
||||||
const job: JobStatus = {
|
fileExtension,
|
||||||
id: item.Id!,
|
deviceId,
|
||||||
deviceId: deviceId,
|
itemId: item.Id,
|
||||||
inputUrl: url,
|
item,
|
||||||
item: item,
|
},
|
||||||
itemId: item.Id!,
|
{
|
||||||
mediaSource,
|
headers: {
|
||||||
progress: 0,
|
"Content-Type": "application/json",
|
||||||
maxBitrate,
|
Authorization: authHeader,
|
||||||
status: "downloading",
|
},
|
||||||
timestamp: new Date(),
|
},
|
||||||
};
|
);
|
||||||
setProcesses([...processes, job]);
|
|
||||||
startDownload(job);
|
if (response.status !== 201) {
|
||||||
|
throw new Error("Failed to start optimization job");
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
@@ -842,39 +738,12 @@ export function DownloadProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useDownload() {
|
export function useDownload() {
|
||||||
if (Platform.isTV) {
|
|
||||||
// Since tv doesn't do downloads, just return no-op functions for everything
|
|
||||||
return {
|
|
||||||
processes: [],
|
|
||||||
startBackgroundDownload: useCallback(
|
|
||||||
async (
|
|
||||||
_url: string,
|
|
||||||
_item: BaseItemDto,
|
|
||||||
_mediaSource: MediaSourceInfo,
|
|
||||||
_maxBitrate?: Bitrate,
|
|
||||||
) => {},
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
downloadedFiles: [],
|
|
||||||
deleteAllFiles: async (): Promise<void> => {},
|
|
||||||
deleteFile: async (id: string): Promise<void> => {},
|
|
||||||
deleteItems: async (items: BaseItemDto[]) => {},
|
|
||||||
saveDownloadedItemInfo: (item: BaseItemDto, size?: number) => {},
|
|
||||||
removeProcess: (id: string) => {},
|
|
||||||
setProcesses: () => {},
|
|
||||||
startDownload: async (_process: JobStatus): Promise<void> => {},
|
|
||||||
getDownloadedItem: (itemId: string) => {},
|
|
||||||
deleteFileByType: async (_type: BaseItemDto["Type"]) => {},
|
|
||||||
appSizeUsage: async () => 0,
|
|
||||||
getDownloadedItemSize: (itemId: string) => {},
|
|
||||||
APP_CACHE_DOWNLOAD_DIRECTORY: "",
|
|
||||||
cleanCacheDirectory: async (): Promise<void> => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = useContext(DownloadContext);
|
const context = useContext(DownloadContext);
|
||||||
if (context === null) {
|
if (context === null) {
|
||||||
throw new Error("useDownload must be used within a DownloadProvider");
|
throw new Error("useDownload must be used within a DownloadProvider");
|
||||||
}
|
}
|
||||||
|
if (Platform.isTV) {
|
||||||
|
throw new Error("useDownload is not supported on TVOS");
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|||||||
108
providers/DownloadProvider.tv.tsx
Normal file
108
providers/DownloadProvider.tv.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
|
import type { JobStatus } from "@/utils/optimize-server";
|
||||||
|
import type {
|
||||||
|
BaseItemDto,
|
||||||
|
MediaSourceInfo,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import * as Application from "expo-application";
|
||||||
|
import * as FileSystem from "expo-file-system";
|
||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import type React from "react";
|
||||||
|
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||||
|
|
||||||
|
export type DownloadedItem = {
|
||||||
|
item: Partial<BaseItemDto>;
|
||||||
|
mediaSource: MediaSourceInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const processesAtom = atom<JobStatus[]>([]);
|
||||||
|
|
||||||
|
const DownloadContext = createContext<ReturnType<
|
||||||
|
typeof useDownloadProvider
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dummy download provider for tvOS
|
||||||
|
*/
|
||||||
|
function useDownloadProvider() {
|
||||||
|
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
||||||
|
|
||||||
|
const downloadedFiles: DownloadedItem[] = [];
|
||||||
|
|
||||||
|
const removeProcess = useCallback(async (id: string) => {}, []);
|
||||||
|
|
||||||
|
const startDownload = useCallback(async (process: JobStatus) => {
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startBackgroundDownload = useCallback(
|
||||||
|
async (url: string, item: BaseItemDto, mediaSource: MediaSourceInfo) => {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteAllFiles = async (): Promise<void> => {};
|
||||||
|
|
||||||
|
const deleteFile = async (id: string): Promise<void> => {};
|
||||||
|
|
||||||
|
const deleteItems = async (items: BaseItemDto[]) => {};
|
||||||
|
|
||||||
|
const cleanCacheDirectory = async () => {};
|
||||||
|
|
||||||
|
const deleteFileByType = async (type: BaseItemDto["Type"]) => {};
|
||||||
|
|
||||||
|
const appSizeUsage = useMemo(async () => {
|
||||||
|
return 0;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function getDownloadedItem(itemId: string): DownloadedItem | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDownloadedItemInfo(item: BaseItemDto, size = 0) {}
|
||||||
|
|
||||||
|
function getDownloadedItemSize(itemId: string): number {
|
||||||
|
const size = storage.getString(`downloadedItemSize-${itemId}`);
|
||||||
|
return size ? Number.parseInt(size) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP_CACHE_DOWNLOAD_DIRECTORY = `${FileSystem.cacheDirectory}${Application.applicationId}/Downloads/`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
processes,
|
||||||
|
startBackgroundDownload,
|
||||||
|
downloadedFiles,
|
||||||
|
deleteAllFiles,
|
||||||
|
deleteFile,
|
||||||
|
deleteItems,
|
||||||
|
saveDownloadedItemInfo,
|
||||||
|
removeProcess,
|
||||||
|
setProcesses,
|
||||||
|
startDownload,
|
||||||
|
getDownloadedItem,
|
||||||
|
deleteFileByType,
|
||||||
|
appSizeUsage,
|
||||||
|
getDownloadedItemSize,
|
||||||
|
APP_CACHE_DOWNLOAD_DIRECTORY,
|
||||||
|
cleanCacheDirectory,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownloadProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const downloadProviderValue = useDownloadProvider();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DownloadContext.Provider value={downloadProviderValue}>
|
||||||
|
{children}
|
||||||
|
</DownloadContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDownload() {
|
||||||
|
const context = useContext(DownloadContext);
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error("useDownload must be used within a DownloadProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Bitrate } from "@/components/BitrateSelector";
|
import type { Bitrate } from "@/components/BitrateSelector";
|
||||||
import { settingsAtom } from "@/utils/atoms/settings";
|
import { settingsAtom } from "@/utils/atoms/settings";
|
||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
import generateDeviceProfile from "@/utils/profiles/native";
|
import native from "@/utils/profiles/native";
|
||||||
import type {
|
import type {
|
||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
@@ -84,7 +84,6 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const native = await generateDeviceProfile();
|
|
||||||
const data = await getStreamUrl({
|
const data = await getStreamUrl({
|
||||||
api,
|
api,
|
||||||
deviceProfile: native,
|
deviceProfile: native,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { apiAtom, getOrSetDeviceId } from "@/providers/JellyfinProvider";
|
import { apiAtom, getOrSetDeviceId } from "@/providers/JellyfinProvider";
|
||||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
@@ -13,12 +12,6 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { AppState, type AppStateStatus } from "react-native";
|
import { AppState, type AppStateStatus } from "react-native";
|
||||||
|
|
||||||
interface WebSocketMessage {
|
|
||||||
MessageType: string;
|
|
||||||
Data: any;
|
|
||||||
// Add other fields as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WebSocketProviderProps {
|
interface WebSocketProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
@@ -26,9 +19,6 @@ interface WebSocketProviderProps {
|
|||||||
interface WebSocketContextType {
|
interface WebSocketContextType {
|
||||||
ws: WebSocket | null;
|
ws: WebSocket | null;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
lastMessage: WebSocketMessage | null;
|
|
||||||
sendMessage: (message: any) => void;
|
|
||||||
clearLastMessage: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const WebSocketContext = createContext<WebSocketContextType | null>(null);
|
const WebSocketContext = createContext<WebSocketContextType | null>(null);
|
||||||
@@ -37,8 +27,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const [ws, setWs] = useState<WebSocket | null>(null);
|
const [ws, setWs] = useState<WebSocket | null>(null);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [lastMessage, setLastMessage] = useState<WebSocketMessage | null>(null);
|
|
||||||
const router = useRouter();
|
|
||||||
const deviceId = useMemo(() => {
|
const deviceId = useMemo(() => {
|
||||||
return getOrSetDeviceId();
|
return getOrSetDeviceId();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -59,7 +48,6 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
let keepAliveInterval: number | null = null;
|
let keepAliveInterval: number | null = null;
|
||||||
|
|
||||||
newWebSocket.onopen = () => {
|
newWebSocket.onopen = () => {
|
||||||
console.log("WebSocket connection opened");
|
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
keepAliveInterval = setInterval(() => {
|
keepAliveInterval = setInterval(() => {
|
||||||
if (newWebSocket.readyState === WebSocket.OPEN) {
|
if (newWebSocket.readyState === WebSocket.OPEN) {
|
||||||
@@ -68,23 +56,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
}, 30000);
|
}, 30000);
|
||||||
};
|
};
|
||||||
|
|
||||||
let reconnectAttempts = 0;
|
|
||||||
const maxReconnectAttempts = 5;
|
|
||||||
const reconnectDelay = 10000;
|
|
||||||
|
|
||||||
newWebSocket.onerror = (e) => {
|
newWebSocket.onerror = (e) => {
|
||||||
console.error("WebSocket error:", e);
|
console.error("WebSocket error:", e);
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
|
|
||||||
if (reconnectAttempts < maxReconnectAttempts) {
|
|
||||||
reconnectAttempts++;
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log(`WebSocket reconnect attempt ${reconnectAttempts}`);
|
|
||||||
connectWebSocket();
|
|
||||||
}, reconnectDelay);
|
|
||||||
} else {
|
|
||||||
console.warn("Max WebSocket reconnect attempts reached.");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
newWebSocket.onclose = () => {
|
newWebSocket.onclose = () => {
|
||||||
@@ -93,15 +67,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
}
|
}
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
};
|
};
|
||||||
newWebSocket.onmessage = (e) => {
|
|
||||||
try {
|
|
||||||
const message = JSON.parse(e.data);
|
|
||||||
console.log("[WS] Received message:", message);
|
|
||||||
setLastMessage(message); // Store the last message in context
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error parsing WebSocket message:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setWs(newWebSocket);
|
setWs(newWebSocket);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -112,41 +78,6 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
};
|
};
|
||||||
}, [api, deviceId]);
|
}, [api, deviceId]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!lastMessage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (lastMessage.MessageType === "Play") {
|
|
||||||
handlePlayCommand(lastMessage.Data);
|
|
||||||
}
|
|
||||||
}, [lastMessage, router]);
|
|
||||||
|
|
||||||
const handlePlayCommand = useCallback(
|
|
||||||
(data: any) => {
|
|
||||||
if (!data || !data.ItemIds || !data.ItemIds.length) {
|
|
||||||
console.warn("[WS] Received Play command with no items");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemId = data.ItemIds[0];
|
|
||||||
console.log(`[WS] Handling Play command for item: ${itemId}`);
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
pathname: "/(auth)/player/direct-player",
|
|
||||||
params: {
|
|
||||||
itemId: itemId,
|
|
||||||
playCommand: data.PlayCommand || "PlayNow",
|
|
||||||
audioIndex: data.AudioStreamIndex?.toString(),
|
|
||||||
subtitleIndex: data.SubtitleStreamIndex?.toString(),
|
|
||||||
mediaSourceId: data.MediaSourceId || "",
|
|
||||||
bitrateValue: "",
|
|
||||||
offline: "false",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[router],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = connectWebSocket();
|
const cleanup = connectWebSocket();
|
||||||
return cleanup;
|
return cleanup;
|
||||||
@@ -195,23 +126,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
ws?.close();
|
ws?.close();
|
||||||
};
|
};
|
||||||
}, [ws, connectWebSocket]);
|
}, [ws, connectWebSocket]);
|
||||||
const sendMessage = useCallback(
|
|
||||||
(message: any) => {
|
|
||||||
if (ws && isConnected) {
|
|
||||||
ws.send(JSON.stringify(message));
|
|
||||||
} else {
|
|
||||||
console.warn("Cannot send message: WebSocket is not connected");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[ws, isConnected],
|
|
||||||
);
|
|
||||||
const clearLastMessage = useCallback(() => {
|
|
||||||
setLastMessage(null);
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<WebSocketContext.Provider
|
<WebSocketContext.Provider value={{ ws, isConnected }}>
|
||||||
value={{ ws, isConnected, lastMessage, sendMessage, clearLastMessage }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</WebSocketContext.Provider>
|
</WebSocketContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"description": "Default Renovate preset for Streamyfin repositories",
|
|
||||||
"extends": [
|
|
||||||
"config:base",
|
|
||||||
":disableDependencyDashboard",
|
|
||||||
":enableVulnerabilityAlertsWithLabel(security)",
|
|
||||||
":semanticCommits",
|
|
||||||
":timezone(Etc/UTC)",
|
|
||||||
"docker:enableMajor",
|
|
||||||
"group:testNonMajor",
|
|
||||||
"group:monorepos",
|
|
||||||
"helpers:pinGitHubActionDigests"
|
|
||||||
],
|
|
||||||
"addLabels": ["dependencies"],
|
|
||||||
"rebaseWhen": "conflicted",
|
|
||||||
"ignorePaths": ["**/node_modules/**", "**/bower_components/**"],
|
|
||||||
"lockFileMaintenance": {
|
|
||||||
"enabled": true,
|
|
||||||
"groupName": "lockfiles",
|
|
||||||
"schedule": ["every month"]
|
|
||||||
},
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"description": "Add 'ci' and 'github-actions' labels to GitHub Action update PRs",
|
|
||||||
"matchManagers": ["github-actions"],
|
|
||||||
"addLabels": ["ci", "github-actions"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Group minor and patch GitHub Action updates into a single PR",
|
|
||||||
"matchManagers": ["github-actions"],
|
|
||||||
"groupName": "CI dependencies",
|
|
||||||
"groupSlug": "ci-deps",
|
|
||||||
"matchUpdateTypes": ["minor", "patch"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Group lock file maintenance updates",
|
|
||||||
"matchUpdateTypes": ["lockFileMaintenance"],
|
|
||||||
"groupName": "lockfiles",
|
|
||||||
"dependencyDashboardApproval": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Add specific labels for Expo and React Native dependencies",
|
|
||||||
"matchPackagePatterns": ["expo", "react-native"],
|
|
||||||
"addLabels": ["expo", "react-native"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -138,8 +138,7 @@
|
|||||||
"hide_libraries": "Bibliotheken ausblenden",
|
"hide_libraries": "Bibliotheken ausblenden",
|
||||||
"select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.",
|
"select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.",
|
||||||
"disable_haptic_feedback": "Haptisches Feedback deaktivieren",
|
"disable_haptic_feedback": "Haptisches Feedback deaktivieren",
|
||||||
"default_quality": "Standardqualität",
|
"default_quality": "Standardqualität"
|
||||||
"disabled": "Deaktiviert"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
@@ -371,9 +370,7 @@
|
|||||||
"audio_tracks": "Audiospuren:",
|
"audio_tracks": "Audiospuren:",
|
||||||
"playback_state": "Wiedergabestatus:",
|
"playback_state": "Wiedergabestatus:",
|
||||||
"no_data_available": "Keine Daten verfügbar",
|
"no_data_available": "Keine Daten verfügbar",
|
||||||
"index": "Index:",
|
"index": "Index:"
|
||||||
"continue_watching": "Weiterschauen",
|
|
||||||
"go_back": "Zurück"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Als Nächstes",
|
"next_up": "Als Nächstes",
|
||||||
|
|||||||
@@ -138,9 +138,7 @@
|
|||||||
"hide_libraries": "Hide Libraries",
|
"hide_libraries": "Hide Libraries",
|
||||||
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||||
"disable_haptic_feedback": "Disable Haptic Feedback",
|
"disable_haptic_feedback": "Disable Haptic Feedback",
|
||||||
"default_quality": "Default quality",
|
"default_quality": "Default quality"
|
||||||
"max_auto_play_episode_count": "Max auto play episode count",
|
|
||||||
"disabled": "Disabled"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
@@ -376,9 +374,7 @@
|
|||||||
"audio_tracks": "Audio Tracks:",
|
"audio_tracks": "Audio Tracks:",
|
||||||
"playback_state": "Playback State:",
|
"playback_state": "Playback State:",
|
||||||
"no_data_available": "No data available",
|
"no_data_available": "No data available",
|
||||||
"index": "Index:",
|
"index": "Index:"
|
||||||
"continue_watching": "Continue Watching",
|
|
||||||
"go_back": "Go back"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next up",
|
"next_up": "Next up",
|
||||||
|
|||||||
@@ -1,480 +0,0 @@
|
|||||||
{
|
|
||||||
"login": {
|
|
||||||
"username_required": "Uzantnomo estas deviga",
|
|
||||||
"error_title": "Eraro",
|
|
||||||
"login_title": "Ensaluti",
|
|
||||||
"login_to_title": "Ensaluti al",
|
|
||||||
"username_placeholder": "Uzantnomo",
|
|
||||||
"password_placeholder": "Pasvorto",
|
|
||||||
"login_button": "Ensaluti",
|
|
||||||
"quick_connect": "Rapida Konekto",
|
|
||||||
"enter_code_to_login": "Enigu kodon {{code}} por ensaluti",
|
|
||||||
"failed_to_initiate_quick_connect": "Malsukcesis iniciati Rapidan Konekton",
|
|
||||||
"got_it": "Komprenita",
|
|
||||||
"connection_failed": "Konekto malsukcesis",
|
|
||||||
"could_not_connect_to_server": "Ne povis konekti al la servilo. Bonvolu kontroli la URL-on kaj vian retan konekton.",
|
|
||||||
"an_unexpected_error_occured": "Neatendita eraro okazis",
|
|
||||||
"change_server": "Ŝanĝi servilon",
|
|
||||||
"invalid_username_or_password": "Nevalida uzantnomo aŭ pasvorto",
|
|
||||||
"user_does_not_have_permission_to_log_in": "Uzanto ne havas permeson ensaluti",
|
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Servilo respondas tro malrapide, provu denove poste",
|
|
||||||
"server_received_too_many_requests_try_again_later": "Servilo ricevis tro multajn petojn, provu denove poste.",
|
|
||||||
"there_is_a_server_error": "Estas servila eraro",
|
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Neatendita eraro okazis. Ĉu vi enigis la ĝustan servilan URL-on?"
|
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"enter_url_to_jellyfin_server": "Enigu la URL-on al via Jellyfin-servilo",
|
|
||||||
"server_url_placeholder": "http(s)://via-servilo.com",
|
|
||||||
"connect_button": "Konekti",
|
|
||||||
"previous_servers": "antaŭaj serviloj",
|
|
||||||
"clear_button": "Forviŝi",
|
|
||||||
"search_for_local_servers": "Serĉi lokajn servilojn",
|
|
||||||
"searching": "Serĉante...",
|
|
||||||
"servers": "Serviloj"
|
|
||||||
},
|
|
||||||
"home": {
|
|
||||||
"no_internet": "Neniu Interreto",
|
|
||||||
"no_items": "Neniuj eroj",
|
|
||||||
"no_internet_message": "Ne zorgu, vi ankoraŭ povas spekti\nelsŝutitan enhavon.",
|
|
||||||
"go_to_downloads": "Iri al elŝutoj",
|
|
||||||
"oops": "Ho ve!",
|
|
||||||
"error_message": "Io misfunkciis.\nBonvolu elsaluti kaj reensaluti.",
|
|
||||||
"continue_watching": "Daŭrigi Spektadon",
|
|
||||||
"next_up": "Sekva",
|
|
||||||
"recently_added_in": "Ĵus Aldonita en {{libraryName}}",
|
|
||||||
"suggested_movies": "Sugestitaj Filmoj",
|
|
||||||
"suggested_episodes": "Sugestitaj Epizodoj",
|
|
||||||
"intro": {
|
|
||||||
"welcome_to_streamyfin": "Bonvenon al Streamyfin",
|
|
||||||
"a_free_and_open_source_client_for_jellyfin": "Senpaga kaj malfermfonta kliento por Jellyfin.",
|
|
||||||
"features_title": "Trajtoj",
|
|
||||||
"features_description": "Streamyfin havas multajn trajtojn kaj integriĝas kun vasta gamo de programaroj, kiujn vi povas trovi en la agorda menuo, tiuj inkluzivas:",
|
|
||||||
"jellyseerr_feature_description": "Konekti al via Jellyseerr-instanco kaj peti filmojn rekte en la aplikaĵo.",
|
|
||||||
"downloads_feature_title": "Elŝutoj",
|
|
||||||
"downloads_feature_description": "Elŝutu filmojn kaj televidajn seriojn por vidi senkonekte. Uzu aŭ la defaŭltan metodon aŭ instalu la optimumigan servilon por elŝuti dosierojn en la fono.",
|
|
||||||
"chromecast_feature_description": "Ĵetu filmojn kaj televidajn seriojn al viaj Chromecast-aparatoj.",
|
|
||||||
"centralised_settings_plugin_title": "Centralizita Agorda Kromprogramo",
|
|
||||||
"centralised_settings_plugin_description": "Agordu agordojn de centralizita loko sur via Jellyfin-servilo. Ĉiuj klientaj agordoj por ĉiuj uzantoj estos sinkronigitaj aŭtomate.",
|
|
||||||
"done_button": "Farite",
|
|
||||||
"go_to_settings_button": "Iri al agordoj",
|
|
||||||
"read_more": "Legu pli"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"settings_title": "Agordoj",
|
|
||||||
"log_out_button": "Elsaluti",
|
|
||||||
"user_info": {
|
|
||||||
"user_info_title": "Uzantaj Informoj",
|
|
||||||
"user": "Uzanto",
|
|
||||||
"server": "Servilo",
|
|
||||||
"token": "Ĵetono",
|
|
||||||
"app_version": "Aplikaĵa Versio"
|
|
||||||
},
|
|
||||||
"quick_connect": {
|
|
||||||
"quick_connect_title": "Rapida Konekto",
|
|
||||||
"authorize_button": "Aŭtorizi Rapidan Konekton",
|
|
||||||
"enter_the_quick_connect_code": "Enigu la rapidan konektan kodon...",
|
|
||||||
"success": "Sukceso",
|
|
||||||
"quick_connect_autorized": "Rapida Konekto aŭtorizita",
|
|
||||||
"error": "Eraro",
|
|
||||||
"invalid_code": "Nevalida kodo",
|
|
||||||
"authorize": "Aŭtorizi"
|
|
||||||
},
|
|
||||||
"media_controls": {
|
|
||||||
"media_controls_title": "Mediaj Kontroloj",
|
|
||||||
"forward_skip_length": "Antaŭensalta longeco",
|
|
||||||
"rewind_length": "Rebobena longeco",
|
|
||||||
"seconds_unit": "s"
|
|
||||||
},
|
|
||||||
"audio": {
|
|
||||||
"audio_title": "Audio",
|
|
||||||
"set_audio_track": "Agordi Aŭdian Trakon De Antaŭa Ero",
|
|
||||||
"audio_language": "Aŭdia lingvo",
|
|
||||||
"audio_hint": "Elektu defaŭltan aŭdian lingvon.",
|
|
||||||
"none": "Neniu",
|
|
||||||
"language": "Lingvo"
|
|
||||||
},
|
|
||||||
"subtitles": {
|
|
||||||
"subtitle_title": "Subtekstoj",
|
|
||||||
"subtitle_language": "Subteksta lingvo",
|
|
||||||
"subtitle_mode": "Subteksta Reĝimo",
|
|
||||||
"set_subtitle_track": "Agordi Subtekstan Trakon De Antaŭa Ero",
|
|
||||||
"subtitle_size": "Subteksta Grandeco",
|
|
||||||
"subtitle_hint": "Agordu subtekstan preferon.",
|
|
||||||
"none": "Neniu",
|
|
||||||
"language": "Lingvo",
|
|
||||||
"loading": "Ŝarĝante",
|
|
||||||
"modes": {
|
|
||||||
"Default": "Defaŭlta",
|
|
||||||
"Smart": "Inteligenta",
|
|
||||||
"Always": "Ĉiam",
|
|
||||||
"None": "Neniu",
|
|
||||||
"OnlyForced": "NurDevigita"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"other": {
|
|
||||||
"other_title": "Alia",
|
|
||||||
"follow_device_orientation": "Aŭtomata rotacio",
|
|
||||||
"video_orientation": "Video-orientiĝo",
|
|
||||||
"orientation": "Orientiĝo",
|
|
||||||
"orientations": {
|
|
||||||
"DEFAULT": "Defaŭlta",
|
|
||||||
"ALL": "Ĉiuj",
|
|
||||||
"PORTRAIT": "Portreta",
|
|
||||||
"PORTRAIT_UP": "Portreta Supren",
|
|
||||||
"PORTRAIT_DOWN": "Portreta Malsupren",
|
|
||||||
"LANDSCAPE": "Pejzaĝa",
|
|
||||||
"LANDSCAPE_LEFT": "Pejzaĝa Maldekstren",
|
|
||||||
"LANDSCAPE_RIGHT": "Pejzaĝa Dekstren",
|
|
||||||
"OTHER": "Alia",
|
|
||||||
"UNKNOWN": "Nekonata"
|
|
||||||
},
|
|
||||||
"safe_area_in_controls": "Sekura areo en kontroloj",
|
|
||||||
"video_player": "Video-ludilo",
|
|
||||||
"video_players": {
|
|
||||||
"VLC_3": "VLC 3",
|
|
||||||
"VLC_4": "VLC 4 (Eksperimenta + PiP)"
|
|
||||||
},
|
|
||||||
"show_custom_menu_links": "Montri Proprajn Menuajn Ligilojn",
|
|
||||||
"hide_libraries": "Kaŝi Bibliotekojn",
|
|
||||||
"select_liraries_you_want_to_hide": "Elektu la bibliotekojn, kiujn vi volas kaŝi de la Biblioteka langeto kaj hejmpaĝaj sekcioj.",
|
|
||||||
"disable_haptic_feedback": "Malŝalti Haptan Rimarkon",
|
|
||||||
"default_quality": "Defaŭlta kvalito"
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "Elŝutoj",
|
|
||||||
"download_method": "Elŝuta metodo",
|
|
||||||
"remux_max_download": "Remux maksimuma elŝuto",
|
|
||||||
"auto_download": "Aŭtomata elŝuto",
|
|
||||||
"optimized_versions_server": "Optimumigitaj versioj servilo",
|
|
||||||
"save_button": "Konservi",
|
|
||||||
"optimized_server": "Optimumigita Servilo",
|
|
||||||
"optimized": "Optimumigita",
|
|
||||||
"default": "Defaŭlta",
|
|
||||||
"optimized_version_hint": "Enigu la URL-on por la optimumiga servilo. La URL devus inkluzivi http aŭ https kaj laŭvole la pordon.",
|
|
||||||
"read_more_about_optimized_server": "Legu pli pri la optimumiga servilo.",
|
|
||||||
"url": "URL",
|
|
||||||
"server_url_placeholder": "http(s)://domajno.org:pordo"
|
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"plugins_title": "Kromprogramoj",
|
|
||||||
"jellyseerr": {
|
|
||||||
"jellyseerr_warning": "Ĉi tiu integriĝo estas en siaj fruaj stadioj. Atendu ŝanĝojn.",
|
|
||||||
"server_url": "Servila URL",
|
|
||||||
"server_url_hint": "Ekzemplo: http(s)://via-gastiganto.url\n(aldonu pordon se necese)",
|
|
||||||
"server_url_placeholder": "Jellyseerr URL...",
|
|
||||||
"password": "Pasvorto",
|
|
||||||
"password_placeholder": "Enigu pasvorton por Jellyfin-uzanto {{username}}",
|
|
||||||
"save_button": "Konservi",
|
|
||||||
"clear_button": "Forviŝi",
|
|
||||||
"login_button": "Ensaluti",
|
|
||||||
"total_media_requests": "Totalaj mediaj petoj",
|
|
||||||
"movie_quota_limit": "Filma kvota limo",
|
|
||||||
"movie_quota_days": "Filmaj kvotaj tagoj",
|
|
||||||
"tv_quota_limit": "Televida kvota limo",
|
|
||||||
"tv_quota_days": "Televidaj kvotaj tagoj",
|
|
||||||
"reset_jellyseerr_config_button": "Restarigi Jellyseerr-agordon",
|
|
||||||
"unlimited": "Senlima",
|
|
||||||
"plus_n_more": "+{{n}} pli",
|
|
||||||
"order_by": {
|
|
||||||
"DEFAULT": "Defaŭlta",
|
|
||||||
"VOTE_COUNT_AND_AVERAGE": "Voĉdonkalkulo kaj mezumo",
|
|
||||||
"POPULARITY": "Populareco"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"marlin_search": {
|
|
||||||
"enable_marlin_search": "Ebligi Marlin Serĉon ",
|
|
||||||
"url": "URL",
|
|
||||||
"server_url_placeholder": "http(s)://domajno.org:pordo",
|
|
||||||
"marlin_search_hint": "Enigu la URL-on por la Marlin-servilo. La URL devus inkluzivi http aŭ https kaj laŭvole la pordon.",
|
|
||||||
"read_more_about_marlin": "Legu pli pri Marlin.",
|
|
||||||
"save_button": "Konservi",
|
|
||||||
"toasts": {
|
|
||||||
"saved": "Konservita"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"storage": {
|
|
||||||
"storage_title": "Stokado",
|
|
||||||
"app_usage": "Aplikaĵo {{usedSpace}}%",
|
|
||||||
"device_usage": "Aparato {{availableSpace}}%",
|
|
||||||
"size_used": "{{used}} el {{total}} uzata",
|
|
||||||
"delete_all_downloaded_files": "Forigi Ĉiujn Elŝutitajn Dosierojn"
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"show_intro": "Montri enkondukon",
|
|
||||||
"reset_intro": "Restarigi enkondukon"
|
|
||||||
},
|
|
||||||
"logs": {
|
|
||||||
"logs_title": "Protokoloj",
|
|
||||||
"export_logs": "Eksporti protokolojn",
|
|
||||||
"click_for_more_info": "Klaku por pli da informoj",
|
|
||||||
"level": "Nivelo",
|
|
||||||
"no_logs_available": "Neniuj protokoloj disponeblaj",
|
|
||||||
"delete_all_logs": "Forigi ĉiujn protokolojn"
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"title": "Lingvoj",
|
|
||||||
"app_language": "Aplikaĵa lingvo",
|
|
||||||
"app_language_description": "Elektu la lingvon por la aplikaĵo.",
|
|
||||||
"system": "Sistemo"
|
|
||||||
},
|
|
||||||
"toasts": {
|
|
||||||
"error_deleting_files": "Eraro forigante dosierojn",
|
|
||||||
"background_downloads_enabled": "Fonaj elŝutoj ebligitaj",
|
|
||||||
"background_downloads_disabled": "Fonaj elŝutoj malŝaltitaj",
|
|
||||||
"connected": "Konektita",
|
|
||||||
"could_not_connect": "Ne povis konekti",
|
|
||||||
"invalid_url": "Nevalida URL"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sessions": {
|
|
||||||
"title": "Sesioj",
|
|
||||||
"no_active_sessions": "Neniuj aktivaj sesioj"
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "Elŝutoj",
|
|
||||||
"tvseries": "Televidaj serioj",
|
|
||||||
"movies": "Filmoj",
|
|
||||||
"queue": "Vico",
|
|
||||||
"queue_hint": "Vico kaj elŝutoj perdiĝos ĉe aplikaĵa rekomenco",
|
|
||||||
"no_items_in_queue": "Neniuj eroj en vico",
|
|
||||||
"no_downloaded_items": "Neniuj elŝutitaj eroj",
|
|
||||||
"delete_all_movies_button": "Forigi ĉiujn Filmojn",
|
|
||||||
"delete_all_tvseries_button": "Forigi ĉiujn Televidajn Seriojn",
|
|
||||||
"delete_all_button": "Forigi ĉion",
|
|
||||||
"active_download": "Aktiva elŝuto",
|
|
||||||
"no_active_downloads": "Neniuj aktivaj elŝutoj",
|
|
||||||
"active_downloads": "Aktivaj elŝutoj",
|
|
||||||
"new_app_version_requires_re_download": "Nova aplikaĵa versio postulas re-elŝuton",
|
|
||||||
"new_app_version_requires_re_download_description": "La nova ĝisdatigo postulas, ke enhavo estu elŝutita denove. Bonvolu forigi ĉian elŝutitan enhavon kaj provi denove.",
|
|
||||||
"back": "Reen",
|
|
||||||
"delete": "Forigi",
|
|
||||||
"something_went_wrong": "Io misfunkciis",
|
|
||||||
"could_not_get_stream_url_from_jellyfin": "Ne povis akiri la fluan URL-on de Jellyfin",
|
|
||||||
"eta": "ETA {{eta}}",
|
|
||||||
"methods": "Metodoj",
|
|
||||||
"toasts": {
|
|
||||||
"you_are_not_allowed_to_download_files": "Vi ne rajtas elŝuti dosierojn.",
|
|
||||||
"deleted_all_movies_successfully": "Sukcese forigis ĉiujn filmojn!",
|
|
||||||
"failed_to_delete_all_movies": "Malsukcesis forigi ĉiujn filmojn",
|
|
||||||
"deleted_all_tvseries_successfully": "Sukcese forigis ĉiujn Televidajn Seriojn!",
|
|
||||||
"failed_to_delete_all_tvseries": "Malsukcesis forigi ĉiujn Televidajn Seriojn",
|
|
||||||
"download_cancelled": "Elŝuto nuligita",
|
|
||||||
"could_not_cancel_download": "Ne povis nuligi elŝuton",
|
|
||||||
"download_completed": "Elŝuto finita",
|
|
||||||
"download_started_for": "Elŝuto komenciĝis por {{item}}",
|
|
||||||
"item_is_ready_to_be_downloaded": "{{item}} estas preta por esti elŝutita",
|
|
||||||
"download_stated_for_item": "Elŝuto komenciĝis por {{item}}",
|
|
||||||
"download_failed_for_item": "Elŝuto malsukcesis por {{item}} - {{error}}",
|
|
||||||
"download_completed_for_item": "Elŝuto finita por {{item}}",
|
|
||||||
"queued_item_for_optimization": "Envicigis {{item}} por optimumigo",
|
|
||||||
"failed_to_start_download_for_item": "Malsukcesis komenci elŝutadon por {{item}}: {{message}}",
|
|
||||||
"server_responded_with_status_code": "Servilo respondis kun statuskodo {{statusCode}}",
|
|
||||||
"no_response_received_from_server": "Neniu respondo ricevita de la servilo",
|
|
||||||
"error_setting_up_the_request": "Eraro starigante la peton",
|
|
||||||
"failed_to_start_download_for_item_unexpected_error": "Malsukcesis komenci elŝutadon por {{item}}: Neatendita eraro",
|
|
||||||
"all_files_folders_and_jobs_deleted_successfully": "Ĉiuj dosieroj, dosierujoj kaj taskoj sukcese forigitaj",
|
|
||||||
"an_error_occured_while_deleting_files_and_jobs": "Eraro okazis dum forigo de dosieroj kaj taskoj",
|
|
||||||
"go_to_downloads": "Iri al elŝutoj"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"search_here": "Serĉu ĉi tie...",
|
|
||||||
"search": "Serĉi...",
|
|
||||||
"x_items": "{{count}} eroj",
|
|
||||||
"library": "Biblioteko",
|
|
||||||
"discover": "Malkovri",
|
|
||||||
"no_results": "Neniuj rezultoj",
|
|
||||||
"no_results_found_for": "Neniuj rezultoj trovitaj por",
|
|
||||||
"movies": "Filmoj",
|
|
||||||
"series": "Serioj",
|
|
||||||
"episodes": "Epizodoj",
|
|
||||||
"collections": "Kolektoj",
|
|
||||||
"actors": "Aktoroj",
|
|
||||||
"request_movies": "Peti Filmojn",
|
|
||||||
"request_series": "Peti Seriojn",
|
|
||||||
"recently_added": "Ĵus Aldonita",
|
|
||||||
"recent_requests": "Lastatempaj Petoj",
|
|
||||||
"plex_watchlist": "Plex Spektolisto",
|
|
||||||
"trending": "Tendencaj",
|
|
||||||
"popular_movies": "Popularaj Filmoj",
|
|
||||||
"movie_genres": "Filmaj Ĝenroj",
|
|
||||||
"upcoming_movies": "Venontaj Filmoj",
|
|
||||||
"studios": "Studioj",
|
|
||||||
"popular_tv": "Populara Televido",
|
|
||||||
"tv_genres": "Televidaj Ĝenroj",
|
|
||||||
"upcoming_tv": "Venonta Televido",
|
|
||||||
"networks": "Retoj",
|
|
||||||
"tmdb_movie_keyword": "TMDB Filma Ŝlosilvorto",
|
|
||||||
"tmdb_movie_genre": "TMDB Filma Ĝenro",
|
|
||||||
"tmdb_tv_keyword": "TMDB Televida Ŝlosilvorto",
|
|
||||||
"tmdb_tv_genre": "TMDB Televida Ĝenro",
|
|
||||||
"tmdb_search": "TMDB Serĉo",
|
|
||||||
"tmdb_studio": "TMDB Studio",
|
|
||||||
"tmdb_network": "TMDB Reto",
|
|
||||||
"tmdb_movie_streaming_services": "TMDB Filmaj Fluservoj",
|
|
||||||
"tmdb_tv_streaming_services": "TMDB Televidaj Fluservoj"
|
|
||||||
},
|
|
||||||
"library": {
|
|
||||||
"no_items_found": "Neniuj eroj trovitaj",
|
|
||||||
"no_results": "Neniuj rezultoj",
|
|
||||||
"no_libraries_found": "Neniuj bibliotekoj trovitaj",
|
|
||||||
"item_types": {
|
|
||||||
"movies": "filmoj",
|
|
||||||
"series": "serioj",
|
|
||||||
"boxsets": "skatolaj aroj",
|
|
||||||
"items": "eroj"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"display": "Vidigi",
|
|
||||||
"row": "Vico",
|
|
||||||
"list": "Listo",
|
|
||||||
"image_style": "Bildostilo",
|
|
||||||
"poster": "Afiŝo",
|
|
||||||
"cover": "Kovrilo",
|
|
||||||
"show_titles": "Montri titolojn",
|
|
||||||
"show_stats": "Montri statistikojn"
|
|
||||||
},
|
|
||||||
"filters": {
|
|
||||||
"genres": "Ĝenroj",
|
|
||||||
"years": "Jaroj",
|
|
||||||
"sort_by": "Ordigi laŭ",
|
|
||||||
"sort_order": "Orda ordo",
|
|
||||||
"asc": "Supreniranta",
|
|
||||||
"desc": "Malsupreniranta",
|
|
||||||
"tags": "Etikedoj"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"favorites": {
|
|
||||||
"series": "Serioj",
|
|
||||||
"movies": "Filmoj",
|
|
||||||
"episodes": "Epizodoj",
|
|
||||||
"videos": "Videoj",
|
|
||||||
"boxsets": "Skatolaj aroj",
|
|
||||||
"playlists": "Ludlistoj",
|
|
||||||
"noDataTitle": "Ankoraŭ neniuj favoratoj",
|
|
||||||
"noData": "Marku erojn kiel favoratojn por vidi ilin aperi ĉi tie por rapida aliro."
|
|
||||||
},
|
|
||||||
"custom_links": {
|
|
||||||
"no_links": "Neniuj ligiloj"
|
|
||||||
},
|
|
||||||
"player": {
|
|
||||||
"error": "Eraro",
|
|
||||||
"failed_to_get_stream_url": "Malsukcesis akiri la fluan URL-on",
|
|
||||||
"an_error_occured_while_playing_the_video": "Eraro okazis dum ludado de la video. Kontrolu protokolojn en agordoj.",
|
|
||||||
"client_error": "Klienta eraro",
|
|
||||||
"could_not_create_stream_for_chromecast": "Ne povis krei fluon por Chromecast",
|
|
||||||
"message_from_server": "Mesaĝo de servilo: {{message}}",
|
|
||||||
"video_has_finished_playing": "Video finis ludi!",
|
|
||||||
"no_video_source": "Neniu video-fonto...",
|
|
||||||
"next_episode": "Sekva Epizodo",
|
|
||||||
"refresh_tracks": "Refreŝigi Trakojn",
|
|
||||||
"subtitle_tracks": "Subtekstaj Trakoj:",
|
|
||||||
"audio_tracks": "Aŭdiaj Trakoj:",
|
|
||||||
"playback_state": "Ludada Stato:",
|
|
||||||
"no_data_available": "Neniuj datumoj disponeblaj",
|
|
||||||
"index": "Indekso:"
|
|
||||||
},
|
|
||||||
"item_card": {
|
|
||||||
"next_up": "Sekva",
|
|
||||||
"no_items_to_display": "Neniuj eroj por montri",
|
|
||||||
"cast_and_crew": "Rolantaro & Skiparo",
|
|
||||||
"series": "Serioj",
|
|
||||||
"seasons": "Sezonoj",
|
|
||||||
"season": "Sezono",
|
|
||||||
"no_episodes_for_this_season": "Neniuj epizodoj por ĉi tiu sezono",
|
|
||||||
"overview": "Superrigardo",
|
|
||||||
"more_with": "Pli kun {{name}}",
|
|
||||||
"similar_items": "Similaj eroj",
|
|
||||||
"no_similar_items_found": "Neniuj similaj eroj trovitaj",
|
|
||||||
"video": "Video",
|
|
||||||
"more_details": "Pli da detaloj",
|
|
||||||
"quality": "Kvalito",
|
|
||||||
"audio": "Audio",
|
|
||||||
"subtitles": "Subteksto",
|
|
||||||
"show_more": "Montri pli",
|
|
||||||
"show_less": "Montri malpli",
|
|
||||||
"appeared_in": "Aperis en",
|
|
||||||
"could_not_load_item": "Ne povis ŝarĝi eron",
|
|
||||||
"none": "Neniu",
|
|
||||||
"download": {
|
|
||||||
"download_season": "Elŝuti Sezonon",
|
|
||||||
"download_series": "Elŝuti Serion",
|
|
||||||
"download_episode": "Elŝuti Epizodon",
|
|
||||||
"download_movie": "Elŝuti Filmon",
|
|
||||||
"download_x_item": "Elŝuti {{item_count}} erojn",
|
|
||||||
"download_button": "Elŝuti",
|
|
||||||
"using_optimized_server": "Uzante optimumigitan servilon",
|
|
||||||
"using_default_method": "Uzante defaŭltan metodon"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"live_tv": {
|
|
||||||
"next": "Sekva",
|
|
||||||
"previous": "Antaŭa",
|
|
||||||
"live_tv": "Viva Televido",
|
|
||||||
"coming_soon": "Baldaŭ",
|
|
||||||
"on_now": "Nun",
|
|
||||||
"shows": "Spektakloj",
|
|
||||||
"movies": "Filmoj",
|
|
||||||
"sports": "Sportoj",
|
|
||||||
"for_kids": "Por Infanoj",
|
|
||||||
"news": "Novaĵoj"
|
|
||||||
},
|
|
||||||
"jellyseerr": {
|
|
||||||
"confirm": "Konfirmi",
|
|
||||||
"cancel": "Nuligi",
|
|
||||||
"yes": "Jes",
|
|
||||||
"whats_wrong": "Kio estas malĝusta?",
|
|
||||||
"issue_type": "Problema tipo",
|
|
||||||
"select_an_issue": "Elektu problemon",
|
|
||||||
"types": "Tipoj",
|
|
||||||
"describe_the_issue": "(laŭvola) Priskribu la problemon...",
|
|
||||||
"submit_button": "Sendi",
|
|
||||||
"report_issue_button": "Raporti problemon",
|
|
||||||
"request_button": "Peti",
|
|
||||||
"are_you_sure_you_want_to_request_all_seasons": "Ĉu vi certas, ke vi volas peti ĉiujn sezonojn?",
|
|
||||||
"failed_to_login": "Malsukcesis ensaluti",
|
|
||||||
"cast": "Rolantaro",
|
|
||||||
"details": "Detaloj",
|
|
||||||
"status": "Stato",
|
|
||||||
"original_title": "Originala Titolo",
|
|
||||||
"series_type": "Seria Tipo",
|
|
||||||
"release_dates": "Eldondatoj",
|
|
||||||
"first_air_date": "Unua Elsendo-dato",
|
|
||||||
"next_air_date": "Sekva Elsendo-dato",
|
|
||||||
"revenue": "Enspezo",
|
|
||||||
"budget": "Buĝeto",
|
|
||||||
"original_language": "Originala Lingvo",
|
|
||||||
"production_country": "Produktada Lando",
|
|
||||||
"studios": "Studioj",
|
|
||||||
"network": "Reto",
|
|
||||||
"currently_streaming_on": "Nuntempe Flusanta ĉe",
|
|
||||||
"advanced": "Altnivela",
|
|
||||||
"request_as": "Peti Kiel",
|
|
||||||
"tags": "Etikedoj",
|
|
||||||
"quality_profile": "Kvalita Profilo",
|
|
||||||
"root_folder": "Radika Dosierujo",
|
|
||||||
"season_all": "Sezono (ĉiuj)",
|
|
||||||
"season_number": "Sezono {{season_number}}",
|
|
||||||
"number_episodes": "{{episode_number}} Epizodoj",
|
|
||||||
"born": "Naskiĝis",
|
|
||||||
"appearances": "Aperoj",
|
|
||||||
"toasts": {
|
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr-servilo ne plenumas minimumajn versiajn postulojn! Bonvolu ĝisdatigi al almenaŭ 2.0.0",
|
|
||||||
"jellyseerr_test_failed": "Jellyseerr-testo malsukcesis. Bonvolu provi denove.",
|
|
||||||
"failed_to_test_jellyseerr_server_url": "Malsukcesis testi jellyseerr-servilan url-on",
|
|
||||||
"issue_submitted": "Problemo sendita!",
|
|
||||||
"requested_item": "Petis {{item}}!",
|
|
||||||
"you_dont_have_permission_to_request": "Vi ne havas permeson peti!",
|
|
||||||
"something_went_wrong_requesting_media": "Io misfunkciis petante medion!"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tabs": {
|
|
||||||
"home": "Hejmo",
|
|
||||||
"search": "Serĉi",
|
|
||||||
"library": "Biblioteko",
|
|
||||||
"custom_links": "Propraj Ligiloj",
|
|
||||||
"favorites": "Favoratoj"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -138,8 +138,7 @@
|
|||||||
"hide_libraries": "Ocultar bibliotecas",
|
"hide_libraries": "Ocultar bibliotecas",
|
||||||
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
|
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
|
||||||
"disable_haptic_feedback": "Desactivar feedback háptico",
|
"disable_haptic_feedback": "Desactivar feedback háptico",
|
||||||
"default_quality": "Calidad por defecto",
|
"default_quality": "Calidad por defecto"
|
||||||
"disabled": "Deshabilitado"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Descargas",
|
"downloads_title": "Descargas",
|
||||||
@@ -371,9 +370,7 @@
|
|||||||
"audio_tracks": "Pistas de audio:",
|
"audio_tracks": "Pistas de audio:",
|
||||||
"playback_state": "Estado de la reproducción:",
|
"playback_state": "Estado de la reproducción:",
|
||||||
"no_data_available": "No hay datos disponibles",
|
"no_data_available": "No hay datos disponibles",
|
||||||
"index": "Índice:",
|
"index": "Índice:"
|
||||||
"continue_watching": "Continuar viendo",
|
|
||||||
"go_back": "Volver"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "A continuación",
|
"next_up": "A continuación",
|
||||||
|
|||||||
@@ -138,8 +138,7 @@
|
|||||||
"hide_libraries": "Cacher des bibliothèques",
|
"hide_libraries": "Cacher des bibliothèques",
|
||||||
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.",
|
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.",
|
||||||
"disable_haptic_feedback": "Désactiver le retour haptique",
|
"disable_haptic_feedback": "Désactiver le retour haptique",
|
||||||
"default_quality": "Qualité par défaut",
|
"default_quality": "Qualité par défaut"
|
||||||
"disabled": "Désactivé"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Téléchargements",
|
"downloads_title": "Téléchargements",
|
||||||
@@ -371,9 +370,7 @@
|
|||||||
"audio_tracks": "Pistes audio:",
|
"audio_tracks": "Pistes audio:",
|
||||||
"playback_state": "État de lecture:",
|
"playback_state": "État de lecture:",
|
||||||
"no_data_available": "Aucune donnée disponible",
|
"no_data_available": "Aucune donnée disponible",
|
||||||
"index": "Index :",
|
"index": "Index:"
|
||||||
"continue_watching": "Continuer à regarder",
|
|
||||||
"go_back": "Retour"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "À suivre",
|
"next_up": "À suivre",
|
||||||
|
|||||||
@@ -138,8 +138,7 @@
|
|||||||
"hide_libraries": "Nascondi Librerie",
|
"hide_libraries": "Nascondi Librerie",
|
||||||
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
|
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
|
||||||
"disable_haptic_feedback": "Disabilita il feedback aptico",
|
"disable_haptic_feedback": "Disabilita il feedback aptico",
|
||||||
"default_quality": "Qualità predefinita",
|
"default_quality": "Qualità predefinita"
|
||||||
"disabled": "Disabilitato"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Scaricamento",
|
"downloads_title": "Scaricamento",
|
||||||
@@ -371,9 +370,7 @@
|
|||||||
"audio_tracks": "Tracce audio:",
|
"audio_tracks": "Tracce audio:",
|
||||||
"playback_state": "Stato della riproduzione:",
|
"playback_state": "Stato della riproduzione:",
|
||||||
"no_data_available": "Nessun dato disponibile",
|
"no_data_available": "Nessun dato disponibile",
|
||||||
"index": "Indice:",
|
"index": "Indice:"
|
||||||
"continue_watching": "Continua a guardare",
|
|
||||||
"go_back": "Indietro"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Il prossimo",
|
"next_up": "Il prossimo",
|
||||||
|
|||||||
@@ -152,9 +152,7 @@
|
|||||||
"optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。",
|
"optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。",
|
||||||
"read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。",
|
"read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"server_url_placeholder": "http(s)://domain.org:ポート",
|
"server_url_placeholder": "http(s)://domain.org:ポート"
|
||||||
"default_quality": "デフォルトの品質",
|
|
||||||
"disabled": "無効"
|
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"plugins_title": "プラグイン",
|
"plugins_title": "プラグイン",
|
||||||
@@ -371,9 +369,7 @@
|
|||||||
"audio_tracks": "音声トラック:",
|
"audio_tracks": "音声トラック:",
|
||||||
"playback_state": "再生状態:",
|
"playback_state": "再生状態:",
|
||||||
"no_data_available": "データなし",
|
"no_data_available": "データなし",
|
||||||
"index": "インデックス:",
|
"index": "インデックス:"
|
||||||
"continue_watching": "視聴を続ける",
|
|
||||||
"go_back": "戻る"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "次",
|
"next_up": "次",
|
||||||
|
|||||||
@@ -138,8 +138,7 @@
|
|||||||
"hide_libraries": "Verberg Bibliotheken",
|
"hide_libraries": "Verberg Bibliotheken",
|
||||||
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
|
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
|
||||||
"disable_haptic_feedback": "Haptische feedback uitschakelen",
|
"disable_haptic_feedback": "Haptische feedback uitschakelen",
|
||||||
"default_quality": "Standaard kwaliteit",
|
"default_quality": "Standaard kwaliteit"
|
||||||
"disabled": "Uitgeschakeld"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
@@ -371,9 +370,7 @@
|
|||||||
"audio_tracks": "Audio Tracks:",
|
"audio_tracks": "Audio Tracks:",
|
||||||
"playback_state": "Afspeelstatus:",
|
"playback_state": "Afspeelstatus:",
|
||||||
"no_data_available": "Geen data beschikbaar",
|
"no_data_available": "Geen data beschikbaar",
|
||||||
"index": "Index:",
|
"index": "Index:"
|
||||||
"continue_watching": "Verder kijken",
|
|
||||||
"go_back": "Terug"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Volgende",
|
"next_up": "Volgende",
|
||||||
|
|||||||
@@ -138,8 +138,7 @@
|
|||||||
"hide_libraries": "Ukryj biblioteki",
|
"hide_libraries": "Ukryj biblioteki",
|
||||||
"select_liraries_you_want_to_hide": "Wybierz biblioteki, które chcesz ukryć na karcie Biblioteka i w sekcjach strony głównej.",
|
"select_liraries_you_want_to_hide": "Wybierz biblioteki, które chcesz ukryć na karcie Biblioteka i w sekcjach strony głównej.",
|
||||||
"disable_haptic_feedback": "Wyłącz wibracje",
|
"disable_haptic_feedback": "Wyłącz wibracje",
|
||||||
"default_quality": "Domyślna jakość",
|
"default_quality": "Domyślna jakość"
|
||||||
"disabled": "Wyłączone"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Pobieranie",
|
"downloads_title": "Pobieranie",
|
||||||
@@ -375,9 +374,7 @@
|
|||||||
"audio_tracks": "Ścieżki audio:",
|
"audio_tracks": "Ścieżki audio:",
|
||||||
"playback_state": "Stan odtwarzania:",
|
"playback_state": "Stan odtwarzania:",
|
||||||
"no_data_available": "Brak dostępnych danych",
|
"no_data_available": "Brak dostępnych danych",
|
||||||
"index": "Indeks:",
|
"index": "Indeks:"
|
||||||
"continue_watching": "Kontynuuj oglądanie",
|
|
||||||
"go_back": "Wstecz"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Następne",
|
"next_up": "Następne",
|
||||||
|
|||||||
@@ -138,8 +138,7 @@
|
|||||||
"hide_libraries": "Ocultar bibliotecas",
|
"hide_libraries": "Ocultar bibliotecas",
|
||||||
"select_liraries_you_want_to_hide": "Selecione as bibliotecas que você deseja ocultar das abas Biblioteca e Início.",
|
"select_liraries_you_want_to_hide": "Selecione as bibliotecas que você deseja ocultar das abas Biblioteca e Início.",
|
||||||
"disable_haptic_feedback": "Desativar o feedback háptico",
|
"disable_haptic_feedback": "Desativar o feedback háptico",
|
||||||
"default_quality": "Qualidade padrão",
|
"default_quality": "Qualidade padrão"
|
||||||
"disabled": "Desativado"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
@@ -372,9 +371,7 @@
|
|||||||
"audio_tracks": "Faixas do áudio:",
|
"audio_tracks": "Faixas do áudio:",
|
||||||
"playback_state": "Playback State:",
|
"playback_state": "Playback State:",
|
||||||
"no_data_available": "Nenhum dado disponível",
|
"no_data_available": "Nenhum dado disponível",
|
||||||
"index": "Índice:",
|
"index": "Índice:"
|
||||||
"continue_watching": "Continuar assistindo",
|
|
||||||
"go_back": "Voltar"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Próximo em",
|
"next_up": "Próximo em",
|
||||||
|
|||||||
@@ -1,480 +1,478 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"username_required": "Имя пользователя обязательно",
|
"username_required": "Имя пользователя обязательно",
|
||||||
"error_title": "Ошибка",
|
"error_title": "Ошибка",
|
||||||
"login_title": "Вход",
|
"login_title": "Вход",
|
||||||
"login_to_title": "Вход в",
|
"login_to_title": "Вход в",
|
||||||
"username_placeholder": "Имя пользователя",
|
"username_placeholder": "Имя пользователя",
|
||||||
"password_placeholder": "Пароль",
|
"password_placeholder": "Пароль",
|
||||||
"login_button": "Войти",
|
"login_button": "Войти",
|
||||||
"quick_connect": "Быстрое подключение",
|
"quick_connect": "Быстрое подключение",
|
||||||
"enter_code_to_login": "Введите код {{code}} чтобы войти",
|
"enter_code_to_login": "Введите код {{code}} чтобы войти",
|
||||||
"failed_to_initiate_quick_connect": "Не удалось инициировать быстрое подключение",
|
"failed_to_initiate_quick_connect": "Не удалось инициировать быстрое подключение",
|
||||||
"got_it": "Принято",
|
"got_it": "Принято",
|
||||||
"connection_failed": "Соединение не удалось",
|
"connection_failed": "Соединение не удалось",
|
||||||
"could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста проверьте URL и ваше интернет соединение.",
|
"could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста проверьте URL и ваше интернет соединение.",
|
||||||
"an_unexpected_error_occured": "Возникла непредвиденная ошибка",
|
"an_unexpected_error_occured": "Возникла непредвиденная ошибка",
|
||||||
"change_server": "Поменять сервер",
|
"change_server": "Поменять сервер",
|
||||||
"invalid_username_or_password": "Неправильное имя пользователя или пароль",
|
"invalid_username_or_password": "Неправильное имя пользователя или пароль",
|
||||||
"user_does_not_have_permission_to_log_in": "Пользователь не имеет прав на вход",
|
"user_does_not_have_permission_to_log_in": "Пользователь не имеет прав на вход",
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Сервер долго не отвечает, попробуйте позже.",
|
"server_is_taking_too_long_to_respond_try_again_later": "Сервер долго не отвечает, попробуйте позже.",
|
||||||
"server_received_too_many_requests_try_again_later": "Сервер получил слишком много запросов, попробуйте позже.",
|
"server_received_too_many_requests_try_again_later": "Сервер получил слишком много запросов, попробуйте позже.",
|
||||||
"there_is_a_server_error": "Возникла ошибка сервера",
|
"there_is_a_server_error": "Возникла ошибка сервера",
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Возникла непредвиденная ошибка. Вы правильно ввели URL?"
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Возникла непредвиденная ошибка. Вы правильно ввели URL?"
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"enter_url_to_jellyfin_server": "Укажите URL на ваш Jellyfin сервер",
|
|
||||||
"server_url_placeholder": "http(s)://your-server.com",
|
|
||||||
"connect_button": "Подключиться",
|
|
||||||
"previous_servers": "предыдущие серверы",
|
|
||||||
"clear_button": "Очистить",
|
|
||||||
"search_for_local_servers": "Поиск локальных серверов",
|
|
||||||
"searching": "Поиск...",
|
|
||||||
"servers": "Сервера"
|
|
||||||
},
|
|
||||||
"home": {
|
|
||||||
"no_internet": "Нет интернета",
|
|
||||||
"no_items": "Нет элементов",
|
|
||||||
"no_internet_message": "Не переживайте, Вы всё ещё можете смотреть\nскачанный контент.",
|
|
||||||
"go_to_downloads": "В загрузки",
|
|
||||||
"oops": "Упс!",
|
|
||||||
"error_message": "Что-то пошло не так.\nПожалуйста выйдите и зайдите снова.",
|
|
||||||
"continue_watching": "Продолжить просмотр",
|
|
||||||
"next_up": "Следующее",
|
|
||||||
"recently_added_in": "Недавно добавлено в {{libraryName}}",
|
|
||||||
"suggested_movies": "Предложенные фильмы",
|
|
||||||
"suggested_episodes": "Предложенные серии",
|
|
||||||
"intro": {
|
|
||||||
"welcome_to_streamyfin": "Добро пожаловать в Streamyfin",
|
|
||||||
"a_free_and_open_source_client_for_jellyfin": "Бесплатный клиент для Jellyfin с открытым кодом",
|
|
||||||
"features_title": "Функции",
|
|
||||||
"features_description": "Streamyfin имеет множество функций и интегрируется с широким спектром программ, которое вы можете найти в меню настроек:",
|
|
||||||
"jellyseerr_feature_description": "Подключитесь к Jellyseerr и запрашивайте фильмы прямо в приложении.",
|
|
||||||
"downloads_feature_title": "Загрузки",
|
|
||||||
"downloads_feature_description": "Скачивайте фильмы и сериалы для просмотра без интернета. Используйте стандартный способ или установите сервер оптимизации для загрузки файлов в фоновом режиме.",
|
|
||||||
"chromecast_feature_description": "Транслируйте фильмы и сериалы на ваши устройста с поддержкой Chromecast.",
|
|
||||||
"centralised_settings_plugin_title": "Плагин для централизованной настройки",
|
|
||||||
"centralised_settings_plugin_description": "Настраивайте параметры из централизованного места на сервере Jellyfin. Все настройки клиента для всех пользователей будут синхронизированы автоматически.",
|
|
||||||
"done_button": "Готово",
|
|
||||||
"go_to_settings_button": "Перейти в настройки",
|
|
||||||
"read_more": "Узнать больше"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"server": {
|
||||||
"settings_title": "Настройки",
|
"enter_url_to_jellyfin_server": "Укажите URL на ваш Jellyfin сервер",
|
||||||
"log_out_button": "Выйти",
|
"server_url_placeholder": "http(s)://your-server.com",
|
||||||
"user_info": {
|
"connect_button": "Подключиться",
|
||||||
"user_info_title": "Информация о пользователе",
|
"previous_servers": "предыдущие серверы",
|
||||||
"user": "Пользователь",
|
"clear_button": "Очистить",
|
||||||
"server": "Сервер",
|
"search_for_local_servers": "Поиск локальных серверов",
|
||||||
"token": "Токен",
|
"searching": "Поиск...",
|
||||||
"app_version": "Версия приложения"
|
"servers": "Сервера"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"no_internet": "Нет интернета",
|
||||||
|
"no_items": "Нет элементов",
|
||||||
|
"no_internet_message": "Не переживайте, Вы всё ещё можете смотреть\nскачанный контент.",
|
||||||
|
"go_to_downloads": "В загрузки",
|
||||||
|
"oops": "Упс!",
|
||||||
|
"error_message": "Что-то пошло не так.\nПожалуйста выйдите и зайдите снова.",
|
||||||
|
"continue_watching": "Продолжить просмотр",
|
||||||
|
"next_up": "Следующее",
|
||||||
|
"recently_added_in": "Недавно добавлено в {{libraryName}}",
|
||||||
|
"suggested_movies": "Предложенные фильмы",
|
||||||
|
"suggested_episodes": "Предложенные серии",
|
||||||
|
"intro": {
|
||||||
|
"welcome_to_streamyfin": "Добро пожаловать в Streamyfin",
|
||||||
|
"a_free_and_open_source_client_for_jellyfin": "Бесплатный клиент для Jellyfin с открытым кодом",
|
||||||
|
"features_title": "Функции",
|
||||||
|
"features_description": "Streamyfin имеет множество функций и интегрируется с широким спектром программ, которое вы можете найти в меню настроек:",
|
||||||
|
"jellyseerr_feature_description": "Подключитесь к Jellyseerr и запрашивайте фильмы прямо в приложении.",
|
||||||
|
"downloads_feature_title": "Загрузки",
|
||||||
|
"downloads_feature_description": "Скачивайте фильмы и сериалы для просмотра без интернета. Используйте стандартный способ или установите сервер оптимизации для загрузки файлов в фоновом режиме.",
|
||||||
|
"chromecast_feature_description": "Транслируйте фильмы и сериалы на ваши устройста с поддержкой Chromecast.",
|
||||||
|
"centralised_settings_plugin_title": "Плагин для централизованной настройки",
|
||||||
|
"centralised_settings_plugin_description": "Настраивайте параметры из централизованного места на сервере Jellyfin. Все настройки клиента для всех пользователей будут синхронизированы автоматически.",
|
||||||
|
"done_button": "Готово",
|
||||||
|
"go_to_settings_button": "Перейти в настройки",
|
||||||
|
"read_more": "Узнать больше"
|
||||||
},
|
},
|
||||||
"quick_connect": {
|
"settings": {
|
||||||
"quick_connect_title": "Быстрое подключение",
|
"settings_title": "Настройки",
|
||||||
"authorize_button": "Авторизировать через быстрое подключение",
|
"log_out_button": "Выйти",
|
||||||
"enter_the_quick_connect_code": "Введите код для быстрого подключения...",
|
"user_info": {
|
||||||
"success": "Успех",
|
"user_info_title": "Информация о пользователе",
|
||||||
"quick_connect_autorized": "Быстрое подключение авторизовано",
|
"user": "Пользователь",
|
||||||
"error": "Ошибка",
|
"server": "Сервер",
|
||||||
"invalid_code": "Неверный код",
|
"token": "Токен",
|
||||||
"authorize": "Авторизировать"
|
"app_version": "Версия приложения"
|
||||||
},
|
},
|
||||||
"media_controls": {
|
"quick_connect": {
|
||||||
"media_controls_title": "Медиа-контроль",
|
"quick_connect_title": "Быстрое подключение",
|
||||||
"forward_skip_length": "Длина пропуска вперед",
|
"authorize_button": "Авторизировать через быстрое подключение",
|
||||||
"rewind_length": "Длина перемотки",
|
"enter_the_quick_connect_code": "Введите код для быстрого подключения...",
|
||||||
"seconds_unit": "c"
|
"success": "Успех",
|
||||||
},
|
"quick_connect_autorized": "Быстрое подключение авторизовано",
|
||||||
"audio": {
|
"error": "Ошибка",
|
||||||
"audio_title": "Аудио",
|
"invalid_code": "Неверный код",
|
||||||
"set_audio_track": "Устанавливать аудио дорожку из предыдущего элемента",
|
"authorize": "Авторизировать"
|
||||||
"audio_language": "Язык аудио",
|
},
|
||||||
"audio_hint": "Выберите стандартный язык аудио.",
|
"media_controls": {
|
||||||
"none": "Отсутствует",
|
"media_controls_title": "Медиа-контроль",
|
||||||
"language": "Язык"
|
"forward_skip_length": "Длина пропуска вперед",
|
||||||
},
|
"rewind_length": "Длина перемотки",
|
||||||
"subtitles": {
|
"seconds_unit": "c"
|
||||||
"subtitle_title": "Субтитры",
|
},
|
||||||
"subtitle_language": "Язык субтитров",
|
"audio": {
|
||||||
"subtitle_mode": "Режим субтитров",
|
"audio_title": "Аудио",
|
||||||
"set_subtitle_track": "Устанавливать субтитры из предыдущего элемента",
|
"set_audio_track": "Устанавливать аудио дорожку из предыдущего элемента",
|
||||||
"subtitle_size": "Размер субтитров",
|
"audio_language": "Язык аудио",
|
||||||
"subtitle_hint": "Настроить субтитры.",
|
"audio_hint": "Выберите стандартный язык аудио.",
|
||||||
"none": "Отсутствует",
|
"none": "Отсутствует",
|
||||||
"language": "Язык",
|
"language": "Язык"
|
||||||
"loading": "Загрузка",
|
},
|
||||||
"modes": {
|
"subtitles": {
|
||||||
"Default": "Стандартный",
|
"subtitle_title": "Субтитры",
|
||||||
"Smart": "Умный",
|
"subtitle_language": "Язык субтитров",
|
||||||
"Always": "Всегда",
|
"subtitle_mode": "Режим субтитров",
|
||||||
"None": "Отсутствует",
|
"set_subtitle_track": "Устанавливать субтитры из предыдущего элемента",
|
||||||
"OnlyForced": "Только принудительные"
|
"subtitle_size": "Размер субтитров",
|
||||||
|
"subtitle_hint": "Настроить субтитры.",
|
||||||
|
"none": "Отсутствует",
|
||||||
|
"language": "Язык",
|
||||||
|
"loading": "Загрузка",
|
||||||
|
"modes": {
|
||||||
|
"Default": "Стандартный",
|
||||||
|
"Smart": "Умный",
|
||||||
|
"Always": "Всегда",
|
||||||
|
"None": "Отсутствует",
|
||||||
|
"OnlyForced": "Только принудительные"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"other": {
|
||||||
|
"other_title": "Другое",
|
||||||
|
"follow_device_orientation": "Авто-поворот",
|
||||||
|
"video_orientation": "Ориентация видео",
|
||||||
|
"orientation": "Ориентация",
|
||||||
|
"orientations": {
|
||||||
|
"DEFAULT": "Стандартный",
|
||||||
|
"ALL": "Все",
|
||||||
|
"PORTRAIT": "Портретный",
|
||||||
|
"PORTRAIT_UP": "Портрет вверх",
|
||||||
|
"PORTRAIT_DOWN": "Портрет вниз",
|
||||||
|
"LANDSCAPE": "Ландшафтный",
|
||||||
|
"LANDSCAPE_LEFT": "Ландшафтный слева",
|
||||||
|
"LANDSCAPE_RIGHT": "Ландшафтный справа",
|
||||||
|
"OTHER": "Другое",
|
||||||
|
"UNKNOWN": "Неизвестное"
|
||||||
|
},
|
||||||
|
"safe_area_in_controls": "Безопасная зона в элементах управления",
|
||||||
|
"video_player": "Видео прейер",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Экспериментальный + PiP)"
|
||||||
|
},
|
||||||
|
"show_custom_menu_links": "Показать ссылки кастомного меню",
|
||||||
|
"hide_libraries": "Скрыть библиотеки",
|
||||||
|
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
|
||||||
|
"disable_haptic_feedback": "Отключить тактильную обратную связь",
|
||||||
|
"default_quality": "Качество по умолчанию"
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"downloads_title": "Загрузки",
|
||||||
|
"download_method": "способ загрузки",
|
||||||
|
"remux_max_download": "Remux max скачать",
|
||||||
|
"auto_download": "Авто-загрузка",
|
||||||
|
"optimized_versions_server": "Оптимизированные версии сервера",
|
||||||
|
"save_button": "Сохранить",
|
||||||
|
"optimized_server": "Оптимизированный сервер",
|
||||||
|
"optimized": "Оптимизированный",
|
||||||
|
"default": "По умолчанию",
|
||||||
|
"optimized_version_hint": "Укажите URL на оптимизированный сервер. URL должен включать http or https и опционально порт.",
|
||||||
|
"read_more_about_optimized_server": "Узнать больше про оптимизацию сервера.",
|
||||||
|
"url": "URL",
|
||||||
|
"server_url_placeholder": "http(s)://domain.org:port"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"plugins_title": "Плагины",
|
||||||
|
"jellyseerr": {
|
||||||
|
"jellyseerr_warning": "Эта интеграция находится на ранней стадии. Ожидайте изменений.",
|
||||||
|
"server_url": "URL сервера",
|
||||||
|
"server_url_hint": "Пример: http(s)://your-host.url\n(Добавьте порт если необходимо)",
|
||||||
|
"server_url_placeholder": "Jellyseerr URL...",
|
||||||
|
"password": "Пароль",
|
||||||
|
"password_placeholder": "Введите пароль для пользователя Jellyfin {{username}}",
|
||||||
|
"save_button": "Сохранить",
|
||||||
|
"clear_button": "Очистить",
|
||||||
|
"login_button": "Войти",
|
||||||
|
"total_media_requests": "Всего запросов на медиа",
|
||||||
|
"movie_quota_limit": "Ограничение квоты на фильмы",
|
||||||
|
"movie_quota_days": "Дни квоты на фильмы",
|
||||||
|
"tv_quota_limit": "Ограничение квоты на сериалы",
|
||||||
|
"tv_quota_days": "Дни квоты на сериалы",
|
||||||
|
"reset_jellyseerr_config_button": "Сбросить конфигурацию Jellyseerr",
|
||||||
|
"unlimited": "Неограниченно",
|
||||||
|
"plus_n_more": "+{{n}} больше",
|
||||||
|
"order_by": {
|
||||||
|
"DEFAULT": "По умолчанию",
|
||||||
|
"VOTE_COUNT_AND_AVERAGE": "Количеству голосов и среднему",
|
||||||
|
"POPULARITY": "Популярности"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"marlin_search": {
|
||||||
|
"enable_marlin_search": "Включить Marlin Search ",
|
||||||
|
"url": "URL",
|
||||||
|
"server_url_placeholder": "http(s)://domain.org:port",
|
||||||
|
"marlin_search_hint": "Введите URL для Marlin сервера. URL должен включать http or https и опционально порт.",
|
||||||
|
"read_more_about_marlin": "Узнать больше о Marlin.",
|
||||||
|
"save_button": "Сохранить",
|
||||||
|
"toasts": {
|
||||||
|
"saved": "Сохранено"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"storage_title": "Хранилище",
|
||||||
|
"app_usage": "Приложение {{usedSpace}}%",
|
||||||
|
"device_usage": "Устройство {{availableSpace}}%",
|
||||||
|
"size_used": "{{used}} из {{total}} использовано",
|
||||||
|
"delete_all_downloaded_files": "Удалить все загруженные файлы",
|
||||||
|
},
|
||||||
|
"intro": {
|
||||||
|
"show_intro": "Показать вступление",
|
||||||
|
"reset_intro": "Сбросить вступление"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"logs_title": "Логи",
|
||||||
|
"no_logs_available": "Логи не доступны",
|
||||||
|
"delete_all_logs": "Удалить все логи",
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"title": "Языки",
|
||||||
|
"app_language": "Язык приложения",
|
||||||
|
"app_language_description": "Выберите язык для приложения.",
|
||||||
|
"system": "Системный"
|
||||||
|
},
|
||||||
|
"toasts": {
|
||||||
|
"error_deleting_files": "Ошибка при удалении файлов",
|
||||||
|
"background_downloads_enabled": "Фоновая загрузка включена",
|
||||||
|
"background_downloads_disabled": "Фоновая загрузка отключена",
|
||||||
|
"connected": "Подключено",
|
||||||
|
"could_not_connect": "Не удалось подключиться",
|
||||||
|
"invalid_url": "Неверный URL"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"other": {
|
"sessions": {
|
||||||
"other_title": "Другое",
|
"title": "Сессии",
|
||||||
"follow_device_orientation": "Авто-поворот",
|
"no_active_sessions": "Нет активных сессий",
|
||||||
"video_orientation": "Ориентация видео",
|
|
||||||
"orientation": "Ориентация",
|
|
||||||
"orientations": {
|
|
||||||
"DEFAULT": "Стандартный",
|
|
||||||
"ALL": "Все",
|
|
||||||
"PORTRAIT": "Портретный",
|
|
||||||
"PORTRAIT_UP": "Портрет вверх",
|
|
||||||
"PORTRAIT_DOWN": "Портрет вниз",
|
|
||||||
"LANDSCAPE": "Ландшафтный",
|
|
||||||
"LANDSCAPE_LEFT": "Ландшафтный слева",
|
|
||||||
"LANDSCAPE_RIGHT": "Ландшафтный справа",
|
|
||||||
"OTHER": "Другое",
|
|
||||||
"UNKNOWN": "Неизвестное"
|
|
||||||
},
|
|
||||||
"safe_area_in_controls": "Безопасная зона в элементах управления",
|
|
||||||
"video_player": "Видео прейер",
|
|
||||||
"video_players": {
|
|
||||||
"VLC_3": "VLC 3",
|
|
||||||
"VLC_4": "VLC 4 (Экспериментальный + PiP)"
|
|
||||||
},
|
|
||||||
"show_custom_menu_links": "Показать ссылки кастомного меню",
|
|
||||||
"hide_libraries": "Скрыть библиотеки",
|
|
||||||
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
|
|
||||||
"disable_haptic_feedback": "Отключить тактильную обратную связь",
|
|
||||||
"default_quality": "Качество по умолчанию",
|
|
||||||
"disabled": "Отключено"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Загрузки",
|
"downloads_title": "Загрузки",
|
||||||
"download_method": "способ загрузки",
|
"tvseries": "Сериалы",
|
||||||
"remux_max_download": "Remux max скачать",
|
"movies": "Фильмы",
|
||||||
"auto_download": "Авто-загрузка",
|
"queue": "Очередь",
|
||||||
"optimized_versions_server": "Оптимизированные версии сервера",
|
"queue_hint": "Очередь и загрузки будут удалены при перезагрузке приложения",
|
||||||
"save_button": "Сохранить",
|
"no_items_in_queue": "Нет элементов в очереди",
|
||||||
"optimized_server": "Оптимизированный сервер",
|
"no_downloaded_items": "Нет загруженых предметов",
|
||||||
"optimized": "Оптимизированный",
|
"delete_all_movies_button": "Удалить все фильмы",
|
||||||
"default": "По умолчанию",
|
"delete_all_tvseries_button": "Удалить все сериалы",
|
||||||
"optimized_version_hint": "Укажите URL на оптимизированный сервер. URL должен включать http or https и опционально порт.",
|
"delete_all_button": "Удалить все",
|
||||||
"read_more_about_optimized_server": "Узнать больше про оптимизацию сервера.",
|
"active_download": "Активно загружается",
|
||||||
"url": "URL",
|
"no_active_downloads": "Нет активных загрузок",
|
||||||
"server_url_placeholder": "http(s)://domain.org:port"
|
"active_downloads": "Активные загрузки",
|
||||||
},
|
"new_app_version_requires_re_download": "Новая версия приложения требует повторной загрузки",
|
||||||
"plugins": {
|
"new_app_version_requires_re_download_description": "Новая версия приложения требует повторной загрузки. Пожалуйста удалите всё и попробуйте заново.",
|
||||||
"plugins_title": "Плагины",
|
"back": "Назад",
|
||||||
"jellyseerr": {
|
"delete": "Удалить",
|
||||||
"jellyseerr_warning": "Эта интеграция находится на ранней стадии. Ожидайте изменений.",
|
"something_went_wrong": "Что-то пошло не так",
|
||||||
"server_url": "URL сервера",
|
"could_not_get_stream_url_from_jellyfin": "Не удалось получить ссылку трансляции из Jellyfin",
|
||||||
"server_url_hint": "Пример: http(s)://your-host.url\n(Добавьте порт если необходимо)",
|
"eta": "ETA {{eta}}",
|
||||||
"server_url_placeholder": "Jellyseerr URL...",
|
"methods": "Методы",
|
||||||
"password": "Пароль",
|
"toasts": {
|
||||||
"password_placeholder": "Введите пароль для пользователя Jellyfin {{username}}",
|
"you_are_not_allowed_to_download_files": "Нет разрешения на скачивание файлов.",
|
||||||
"save_button": "Сохранить",
|
"deleted_all_movies_successfully": "Все фильмы были успешно удалены!",
|
||||||
"clear_button": "Очистить",
|
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
|
||||||
"login_button": "Войти",
|
"deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!",
|
||||||
"total_media_requests": "Всего запросов на медиа",
|
"failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов",
|
||||||
"movie_quota_limit": "Ограничение квоты на фильмы",
|
"download_cancelled": "Загрузка отменена",
|
||||||
"movie_quota_days": "Дни квоты на фильмы",
|
"could_not_cancel_download": "Не удалось отменить загрузку",
|
||||||
"tv_quota_limit": "Ограничение квоты на сериалы",
|
"download_completed": "Загрузка завершена",
|
||||||
"tv_quota_days": "Дни квоты на сериалы",
|
"download_started_for": "Загрузка {{item}} началась",
|
||||||
"reset_jellyseerr_config_button": "Сбросить конфигурацию Jellyseerr",
|
"item_is_ready_to_be_downloaded": "{{item}} готов к загрузке",
|
||||||
"unlimited": "Неограниченно",
|
"download_stated_for_item": "Загрузка {{item} началась",
|
||||||
"plus_n_more": "+{{n}} больше",
|
"download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}",
|
||||||
"order_by": {
|
"download_completed_for_item": "{{item}} успешно загружен",
|
||||||
"DEFAULT": "По умолчанию",
|
"queued_item_for_optimization": "{{item}} поставлен в очередь для оптимизации",
|
||||||
"VOTE_COUNT_AND_AVERAGE": "Количеству голосов и среднему",
|
"failed_to_start_download_for_item": "Не удалось начать загрузку {{item}}: {{message}}",
|
||||||
"POPULARITY": "Популярности"
|
"server_responded_with_status_code": "Сервер ответил со статусом {{statusCode}}",
|
||||||
}
|
"no_response_received_from_server": "Нет ответа от сервера",
|
||||||
},
|
"error_setting_up_the_request": "Ошибка при создании запроса",
|
||||||
"marlin_search": {
|
"failed_to_start_download_for_item_unexpected_error": "Не удалось начать загрузку {{item}}: Неожиданная ошибка",
|
||||||
"enable_marlin_search": "Включить Marlin Search ",
|
"all_files_folders_and_jobs_deleted_successfully": "Все файлы, папки, и задачи были успешно удалены",
|
||||||
"url": "URL",
|
"an_error_occured_while_deleting_files_and_jobs": "Возникла ошибка при удалении файлов и работ",
|
||||||
"server_url_placeholder": "http(s)://domain.org:port",
|
"go_to_downloads": "В загрузки"
|
||||||
"marlin_search_hint": "Введите URL для Marlin сервера. URL должен включать http or https и опционально порт.",
|
|
||||||
"read_more_about_marlin": "Узнать больше о Marlin.",
|
|
||||||
"save_button": "Сохранить",
|
|
||||||
"toasts": {
|
|
||||||
"saved": "Сохранено"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"storage": {
|
|
||||||
"storage_title": "Хранилище",
|
|
||||||
"app_usage": "Приложение {{usedSpace}}%",
|
|
||||||
"device_usage": "Устройство {{availableSpace}}%",
|
|
||||||
"size_used": "{{used}} из {{total}} использовано",
|
|
||||||
"delete_all_downloaded_files": "Удалить все загруженные файлы"
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"show_intro": "Показать вступление",
|
|
||||||
"reset_intro": "Сбросить вступление"
|
|
||||||
},
|
|
||||||
"logs": {
|
|
||||||
"logs_title": "Логи",
|
|
||||||
"no_logs_available": "Логи не доступны",
|
|
||||||
"delete_all_logs": "Удалить все логи"
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"title": "Языки",
|
|
||||||
"app_language": "Язык приложения",
|
|
||||||
"app_language_description": "Выберите язык для приложения.",
|
|
||||||
"system": "Системный"
|
|
||||||
},
|
|
||||||
"toasts": {
|
|
||||||
"error_deleting_files": "Ошибка при удалении файлов",
|
|
||||||
"background_downloads_enabled": "Фоновая загрузка включена",
|
|
||||||
"background_downloads_disabled": "Фоновая загрузка отключена",
|
|
||||||
"connected": "Подключено",
|
|
||||||
"could_not_connect": "Не удалось подключиться",
|
|
||||||
"invalid_url": "Неверный URL"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"search": {
|
||||||
"title": "Сессии",
|
"search_here": "Искать здесь...",
|
||||||
"no_active_sessions": "Нет активных сессий"
|
"search": "Поиск...",
|
||||||
},
|
"x_items": "{{count}} предметов",
|
||||||
"downloads": {
|
"library": "Библиотека",
|
||||||
"downloads_title": "Загрузки",
|
"discover": "Найти новое",
|
||||||
"tvseries": "Сериалы",
|
"no_results": "Нет результатов",
|
||||||
|
"no_results_found_for": "Не было результатов при поиске",
|
||||||
"movies": "Фильмы",
|
"movies": "Фильмы",
|
||||||
"queue": "Очередь",
|
|
||||||
"queue_hint": "Очередь и загрузки будут удалены при перезагрузке приложения",
|
|
||||||
"no_items_in_queue": "Нет элементов в очереди",
|
|
||||||
"no_downloaded_items": "Нет загруженых предметов",
|
|
||||||
"delete_all_movies_button": "Удалить все фильмы",
|
|
||||||
"delete_all_tvseries_button": "Удалить все сериалы",
|
|
||||||
"delete_all_button": "Удалить все",
|
|
||||||
"active_download": "Активно загружается",
|
|
||||||
"no_active_downloads": "Нет активных загрузок",
|
|
||||||
"active_downloads": "Активные загрузки",
|
|
||||||
"new_app_version_requires_re_download": "Новая версия приложения требует повторной загрузки",
|
|
||||||
"new_app_version_requires_re_download_description": "Новая версия приложения требует повторной загрузки. Пожалуйста удалите всё и попробуйте заново.",
|
|
||||||
"back": "Назад",
|
|
||||||
"delete": "Удалить",
|
|
||||||
"something_went_wrong": "Что-то пошло не так",
|
|
||||||
"could_not_get_stream_url_from_jellyfin": "Не удалось получить ссылку трансляции из Jellyfin",
|
|
||||||
"eta": "ETA {{eta}}",
|
|
||||||
"methods": "Методы",
|
|
||||||
"toasts": {
|
|
||||||
"you_are_not_allowed_to_download_files": "Нет разрешения на скачивание файлов.",
|
|
||||||
"deleted_all_movies_successfully": "Все фильмы были успешно удалены!",
|
|
||||||
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
|
|
||||||
"deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!",
|
|
||||||
"failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов",
|
|
||||||
"download_cancelled": "Загрузка отменена",
|
|
||||||
"could_not_cancel_download": "Не удалось отменить загрузку",
|
|
||||||
"download_completed": "Загрузка завершена",
|
|
||||||
"download_started_for": "Загрузка {{item}} началась",
|
|
||||||
"item_is_ready_to_be_downloaded": "{{item}} готов к загрузке",
|
|
||||||
"download_stated_for_item": "Загрузка {{item} началась",
|
|
||||||
"download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}",
|
|
||||||
"download_completed_for_item": "{{item}} успешно загружен",
|
|
||||||
"queued_item_for_optimization": "{{item}} поставлен в очередь для оптимизации",
|
|
||||||
"failed_to_start_download_for_item": "Не удалось начать загрузку {{item}}: {{message}}",
|
|
||||||
"server_responded_with_status_code": "Сервер ответил со статусом {{statusCode}}",
|
|
||||||
"no_response_received_from_server": "Нет ответа от сервера",
|
|
||||||
"error_setting_up_the_request": "Ошибка при создании запроса",
|
|
||||||
"failed_to_start_download_for_item_unexpected_error": "Не удалось начать загрузку {{item}}: Неожиданная ошибка",
|
|
||||||
"all_files_folders_and_jobs_deleted_successfully": "Все файлы, папки, и задачи были успешно удалены",
|
|
||||||
"an_error_occured_while_deleting_files_and_jobs": "Возникла ошибка при удалении файлов и работ",
|
|
||||||
"go_to_downloads": "В загрузки"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"search_here": "Искать здесь...",
|
|
||||||
"search": "Поиск...",
|
|
||||||
"x_items": "{{count}} предметов",
|
|
||||||
"library": "Библиотека",
|
|
||||||
"discover": "Найти новое",
|
|
||||||
"no_results": "Нет результатов",
|
|
||||||
"no_results_found_for": "Не было результатов при поиске",
|
|
||||||
"movies": "Фильмы",
|
|
||||||
"series": "Сериалы",
|
|
||||||
"episodes": "Серии",
|
|
||||||
"collections": "Коллекции",
|
|
||||||
"actors": "Актеры",
|
|
||||||
"request_movies": "Запросить фильмы",
|
|
||||||
"request_series": "Запросить сериалы",
|
|
||||||
"recently_added": "Недавно добавлено",
|
|
||||||
"recent_requests": "Недавно запрошено",
|
|
||||||
"plex_watchlist": "Список просмотра с Plex",
|
|
||||||
"trending": "В тренде",
|
|
||||||
"popular_movies": "Популярные фильмы",
|
|
||||||
"movie_genres": "Популярные жанры",
|
|
||||||
"upcoming_movies": "Предстоящие фильмы",
|
|
||||||
"studios": "Студии",
|
|
||||||
"popular_tv": "Популярные сериалы",
|
|
||||||
"tv_genres": "жанры сериалов",
|
|
||||||
"upcoming_tv": "Предстоящие сериалы",
|
|
||||||
"networks": "Сети",
|
|
||||||
"tmdb_movie_keyword": "TMDB Ключевые слова фильмов",
|
|
||||||
"tmdb_movie_genre": "TMDB Жанры фильмов",
|
|
||||||
"tmdb_tv_keyword": "TMDB Ключевые слова сериалов",
|
|
||||||
"tmdb_tv_genre": "TMDB Жанры сериалов",
|
|
||||||
"tmdb_search": "TMDB Поиск",
|
|
||||||
"tmdb_studio": "TMDB Студии",
|
|
||||||
"tmdb_network": "TMDB Сеть",
|
|
||||||
"tmdb_movie_streaming_services": "TMDB Потоковые сервисы фильмов",
|
|
||||||
"tmdb_tv_streaming_services": "TMDB Потоковые сервисы сериалов"
|
|
||||||
},
|
|
||||||
"library": {
|
|
||||||
"no_items_found": "элементы не найдены",
|
|
||||||
"no_results": "Нет результатов",
|
|
||||||
"no_libraries_found": "Библиотеки не найдены",
|
|
||||||
"item_types": {
|
|
||||||
"movies": "фильмы",
|
|
||||||
"series": "Сериалы",
|
"series": "Сериалы",
|
||||||
|
"episodes": "Серии",
|
||||||
|
"collections": "Коллекции",
|
||||||
|
"actors": "Актеры",
|
||||||
|
"request_movies": "Запросить фильмы",
|
||||||
|
"request_series": "Запросить сериалы",
|
||||||
|
"recently_added": "Недавно добавлено",
|
||||||
|
"recent_requests": "Недавно запрошено",
|
||||||
|
"plex_watchlist": "Список просмотра с Plex",
|
||||||
|
"trending": "В тренде",
|
||||||
|
"popular_movies": "Популярные фильмы",
|
||||||
|
"movie_genres": "Популярные жанры",
|
||||||
|
"upcoming_movies": "Предстоящие фильмы",
|
||||||
|
"studios": "Студии",
|
||||||
|
"popular_tv": "Популярные сериалы",
|
||||||
|
"tv_genres": "жанры сериалов",
|
||||||
|
"upcoming_tv": "Предстоящие сериалы",
|
||||||
|
"networks": "Сети",
|
||||||
|
"tmdb_movie_keyword": "TMDB Ключевые слова фильмов",
|
||||||
|
"tmdb_movie_genre": "TMDB Жанры фильмов",
|
||||||
|
"tmdb_tv_keyword": "TMDB Ключевые слова сериалов",
|
||||||
|
"tmdb_tv_genre": "TMDB Жанры сериалов",
|
||||||
|
"tmdb_search": "TMDB Поиск",
|
||||||
|
"tmdb_studio": "TMDB Студии",
|
||||||
|
"tmdb_network": "TMDB Сеть",
|
||||||
|
"tmdb_movie_streaming_services": "TMDB Потоковые сервисы фильмов",
|
||||||
|
"tmdb_tv_streaming_services": "TMDB Потоковые сервисы сериалов",
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"no_items_found": "элементы не найдены",
|
||||||
|
"no_results": "Нет результатов",
|
||||||
|
"no_libraries_found": "Библиотеки не найдены",
|
||||||
|
"item_types": {
|
||||||
|
"movies": "фильмы",
|
||||||
|
"series": "Сериалы",
|
||||||
|
"boxsets": "Коллекции",
|
||||||
|
"items": "элементы"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"display": "Отображать",
|
||||||
|
"row": "Ряд",
|
||||||
|
"list": "Список",
|
||||||
|
"image_style": "Стиль изображения",
|
||||||
|
"poster": "Постер",
|
||||||
|
"cover": "Обложка",
|
||||||
|
"show_titles": "Показывать загаловки",
|
||||||
|
"show_stats": "Показывать статистику",
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"genres": "Жанры",
|
||||||
|
"years": "Года",
|
||||||
|
"sort_by": "Сортировать по",
|
||||||
|
"sort_order": "Порядок сортировки",
|
||||||
|
"asc": "По Возрастанию",
|
||||||
|
"desc": "По убыванию",
|
||||||
|
"tags": "Тэги"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"series": "Сериалы",
|
||||||
|
"movies": "Фильмы",
|
||||||
|
"episodes": "Серии",
|
||||||
|
"videos": "Видео",
|
||||||
"boxsets": "Коллекции",
|
"boxsets": "Коллекции",
|
||||||
"items": "элементы"
|
"playlists": "Плейлисты",
|
||||||
|
"noDataTitle": "Пока нет избранных",
|
||||||
|
"noData": "Отметьте элементы как избранные, чтобы они отображались здесь для быстрого доступа."
|
||||||
},
|
},
|
||||||
"options": {
|
"custom_links": {
|
||||||
"display": "Отображать",
|
"no_links": "Нет ссылок"
|
||||||
"row": "Ряд",
|
|
||||||
"list": "Список",
|
|
||||||
"image_style": "Стиль изображения",
|
|
||||||
"poster": "Постер",
|
|
||||||
"cover": "Обложка",
|
|
||||||
"show_titles": "Показывать загаловки",
|
|
||||||
"show_stats": "Показывать статистику"
|
|
||||||
},
|
},
|
||||||
"filters": {
|
"player": {
|
||||||
"genres": "Жанры",
|
"error": "Ошибка",
|
||||||
"years": "Года",
|
"failed_to_get_stream_url": "Не удалось получить URL потока",
|
||||||
"sort_by": "Сортировать по",
|
"an_error_occured_while_playing_the_video": "Возникла Неожиданная ошибка во время воспроизведения. Проверьте логи в настройках.",
|
||||||
"sort_order": "Порядок сортировки",
|
"client_error": "Ошибка клиента",
|
||||||
"asc": "По Возрастанию",
|
"could_not_create_stream_for_chromecast": "Не удалось создать поток для Chromecast",
|
||||||
"desc": "По убыванию",
|
"message_from_server": "Сообщение от сервера: {{message}}",
|
||||||
"tags": "Тэги"
|
"video_has_finished_playing": "Видео закончило воспроизводиться!",
|
||||||
|
"no_video_source": "Нет источника видео...",
|
||||||
|
"next_episode": "Следующая серия",
|
||||||
|
"refresh_tracks": "Обновить дорожки",
|
||||||
|
"subtitle_tracks": "Субтитры:",
|
||||||
|
"audio_tracks": "Аудио дорожки:",
|
||||||
|
"playback_state": "Состояние воспроизведения:",
|
||||||
|
"no_data_available": "Данные не доступны",
|
||||||
|
"index": "Индекс:"
|
||||||
|
},
|
||||||
|
"item_card": {
|
||||||
|
"next_up": "Следующее",
|
||||||
|
"no_items_to_display": "Нет элементов для отображения",
|
||||||
|
"cast_and_crew": "Актеры и съемочная группа",
|
||||||
|
"series": "Серии",
|
||||||
|
"seasons": "Сезоны",
|
||||||
|
"season": "Сезон",
|
||||||
|
"no_episodes_for_this_season": "В этом сезоне нет серий",
|
||||||
|
"overview": "Обзор",
|
||||||
|
"more_with": "Больше с {{name}}",
|
||||||
|
"similar_items": "Похожие элементы",
|
||||||
|
"no_similar_items_found": "Похожие элементы не найдены",
|
||||||
|
"video": "Видео",
|
||||||
|
"more_details": "Больше деталей",
|
||||||
|
"quality": "Качество",
|
||||||
|
"audio": "Звук",
|
||||||
|
"subtitles": "Субтитры",
|
||||||
|
"show_more": "Показать больше",
|
||||||
|
"show_less": "Показать меньше",
|
||||||
|
"appeared_in": "Появлялся в",
|
||||||
|
"could_not_load_item": "Не удалось загрузить элемент",
|
||||||
|
"none": "Отсутствует",
|
||||||
|
"download": {
|
||||||
|
"download_season": "Загрузить сезон",
|
||||||
|
"download_series": "Загрузить сериал",
|
||||||
|
"download_episode": "Загрузить серию",
|
||||||
|
"download_movie": "Скачать фильм",
|
||||||
|
"download_x_item": "Загрузить {{item_count}} элементов",
|
||||||
|
"download_button": "Загрузить",
|
||||||
|
"using_optimized_server": "Использовать оптимизированный сервер",
|
||||||
|
"using_default_method": "Использовать стандартный метод",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"live_tv": {
|
||||||
|
"next": "Следующая",
|
||||||
|
"previous": "Предыдущая",
|
||||||
|
"live_tv": "Прямой эфир ТВ",
|
||||||
|
"coming_soon": "Скоро",
|
||||||
|
"on_now": "Сейчас в эфире",
|
||||||
|
"shows": "Сериалы",
|
||||||
|
"movies": "Фильмы",
|
||||||
|
"sports": "Спорт",
|
||||||
|
"for_kids": "Для детей",
|
||||||
|
"news": "Новости"
|
||||||
|
},
|
||||||
|
"jellyseerr": {
|
||||||
|
"confirm": "Подтвердить",
|
||||||
|
"cancel": "Отменить",
|
||||||
|
"yes": "Да",
|
||||||
|
"whats_wrong": "В чем дело?",
|
||||||
|
"issue_type": "Вид проблемы",
|
||||||
|
"select_an_issue": "Выберите проблему",
|
||||||
|
"types": "Типы",
|
||||||
|
"describe_the_issue": "(опционально) Опишите проблему...",
|
||||||
|
"submit_button": "Подать",
|
||||||
|
"report_issue_button": "Сообщить о проблеме",
|
||||||
|
"request_button": "Запросить",
|
||||||
|
"are_you_sure_you_want_to_request_all_seasons": "Вы уверены, что хотите запросить все сезоны?",
|
||||||
|
"failed_to_login": "Не удалось войти",
|
||||||
|
"cast": "Транслировать",
|
||||||
|
"details": "Детали",
|
||||||
|
"status": "Статус",
|
||||||
|
"original_title": "Оригинальное название",
|
||||||
|
"series_type": "Тип сериала",
|
||||||
|
"release_dates": "Дата релиза",
|
||||||
|
"first_air_date": "Первая дата выхода в эфир",
|
||||||
|
"next_air_date": "Следующая дата выхода в эфир",
|
||||||
|
"revenue": "Прибыль",
|
||||||
|
"budget": "Бюджет",
|
||||||
|
"original_language": "Оригинальный язык",
|
||||||
|
"production_country": "Страна производства",
|
||||||
|
"studios": "Студия",
|
||||||
|
"network": "Сеть",
|
||||||
|
"currently_streaming_on": "Сейчас доступно на",
|
||||||
|
"advanced": "Продвинутое",
|
||||||
|
"request_as": "Запросить как",
|
||||||
|
"tags": "Тэги",
|
||||||
|
"quality_profile": "Профиль качества",
|
||||||
|
"root_folder": "Корневая папка",
|
||||||
|
"season_all": "Сезон (все)",
|
||||||
|
"season_number": "Сезон {{season_number}}",
|
||||||
|
"number_episodes": "{{episode_number}} серий",
|
||||||
|
"born": "Рожден",
|
||||||
|
"appearances": "Появления",
|
||||||
|
"toasts": {
|
||||||
|
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не соответствует минимальным требованиям версии! Пожалуйста, обновите до версии не ниже 2.0.0",
|
||||||
|
"jellyseerr_test_failed": "Тест Jellyseerr не пройден. Попробуйте еще раз.",
|
||||||
|
"failed_to_test_jellyseerr_server_url": "Не удалось проверить URL-адрес сервера jellyseerr",
|
||||||
|
"issue_submitted": "Проблема отправлена!",
|
||||||
|
"requested_item": "Запрошено {{item}}!",
|
||||||
|
"you_dont_have_permission_to_request": "У вас нет разрешения на запрос!",
|
||||||
|
"something_went_wrong_requesting_media": "Что-то пошло не так при запросе медиафайлов!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"home": "Дом",
|
||||||
|
"search": "Поиск",
|
||||||
|
"library": "Библиотека",
|
||||||
|
"custom_links": "Кастомные ссылки",
|
||||||
|
"favorites": "Избранное"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"favorites": {
|
|
||||||
"series": "Сериалы",
|
|
||||||
"movies": "Фильмы",
|
|
||||||
"episodes": "Серии",
|
|
||||||
"videos": "Видео",
|
|
||||||
"boxsets": "Коллекции",
|
|
||||||
"playlists": "Плейлисты",
|
|
||||||
"noDataTitle": "Пока нет избранных",
|
|
||||||
"noData": "Отметьте элементы как избранные, чтобы они отображались здесь для быстрого доступа."
|
|
||||||
},
|
|
||||||
"custom_links": {
|
|
||||||
"no_links": "Нет ссылок"
|
|
||||||
},
|
|
||||||
"player": {
|
|
||||||
"error": "Ошибка",
|
|
||||||
"failed_to_get_stream_url": "Не удалось получить URL потока",
|
|
||||||
"an_error_occured_while_playing_the_video": "Возникла Неожиданная ошибка во время воспроизведения. Проверьте логи в настройках.",
|
|
||||||
"client_error": "Ошибка клиента",
|
|
||||||
"could_not_create_stream_for_chromecast": "Не удалось создать поток для Chromecast",
|
|
||||||
"message_from_server": "Сообщение от сервера: {{message}}",
|
|
||||||
"video_has_finished_playing": "Видео закончило воспроизводиться!",
|
|
||||||
"no_video_source": "Нет источника видео...",
|
|
||||||
"next_episode": "Следующая серия",
|
|
||||||
"refresh_tracks": "Обновить дорожки",
|
|
||||||
"subtitle_tracks": "Субтитры:",
|
|
||||||
"audio_tracks": "Аудио дорожки:",
|
|
||||||
"playback_state": "Состояние воспроизведения:",
|
|
||||||
"no_data_available": "Данные не доступны",
|
|
||||||
"index": "Индекс:",
|
|
||||||
"continue_watching": "Продолжить просмотр",
|
|
||||||
"go_back": "Назад"
|
|
||||||
},
|
|
||||||
"item_card": {
|
|
||||||
"next_up": "Следующее",
|
|
||||||
"no_items_to_display": "Нет элементов для отображения",
|
|
||||||
"cast_and_crew": "Актеры и съемочная группа",
|
|
||||||
"series": "Серии",
|
|
||||||
"seasons": "Сезоны",
|
|
||||||
"season": "Сезон",
|
|
||||||
"no_episodes_for_this_season": "В этом сезоне нет серий",
|
|
||||||
"overview": "Обзор",
|
|
||||||
"more_with": "Больше с {{name}}",
|
|
||||||
"similar_items": "Похожие элементы",
|
|
||||||
"no_similar_items_found": "Похожие элементы не найдены",
|
|
||||||
"video": "Видео",
|
|
||||||
"more_details": "Больше деталей",
|
|
||||||
"quality": "Качество",
|
|
||||||
"audio": "Звук",
|
|
||||||
"subtitles": "Субтитры",
|
|
||||||
"show_more": "Показать больше",
|
|
||||||
"show_less": "Показать меньше",
|
|
||||||
"appeared_in": "Появлялся в",
|
|
||||||
"could_not_load_item": "Не удалось загрузить элемент",
|
|
||||||
"none": "Отсутствует",
|
|
||||||
"download": {
|
|
||||||
"download_season": "Загрузить сезон",
|
|
||||||
"download_series": "Загрузить сериал",
|
|
||||||
"download_episode": "Загрузить серию",
|
|
||||||
"download_movie": "Скачать фильм",
|
|
||||||
"download_x_item": "Загрузить {{item_count}} элементов",
|
|
||||||
"download_button": "Загрузить",
|
|
||||||
"using_optimized_server": "Использовать оптимизированный сервер",
|
|
||||||
"using_default_method": "Использовать стандартный метод"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"live_tv": {
|
|
||||||
"next": "Следующая",
|
|
||||||
"previous": "Предыдущая",
|
|
||||||
"live_tv": "Прямой эфир ТВ",
|
|
||||||
"coming_soon": "Скоро",
|
|
||||||
"on_now": "Сейчас в эфире",
|
|
||||||
"shows": "Сериалы",
|
|
||||||
"movies": "Фильмы",
|
|
||||||
"sports": "Спорт",
|
|
||||||
"for_kids": "Для детей",
|
|
||||||
"news": "Новости"
|
|
||||||
},
|
|
||||||
"jellyseerr": {
|
|
||||||
"confirm": "Подтвердить",
|
|
||||||
"cancel": "Отменить",
|
|
||||||
"yes": "Да",
|
|
||||||
"whats_wrong": "В чем дело?",
|
|
||||||
"issue_type": "Вид проблемы",
|
|
||||||
"select_an_issue": "Выберите проблему",
|
|
||||||
"types": "Типы",
|
|
||||||
"describe_the_issue": "(опционально) Опишите проблему...",
|
|
||||||
"submit_button": "Подать",
|
|
||||||
"report_issue_button": "Сообщить о проблеме",
|
|
||||||
"request_button": "Запросить",
|
|
||||||
"are_you_sure_you_want_to_request_all_seasons": "Вы уверены, что хотите запросить все сезоны?",
|
|
||||||
"failed_to_login": "Не удалось войти",
|
|
||||||
"cast": "Транслировать",
|
|
||||||
"details": "Детали",
|
|
||||||
"status": "Статус",
|
|
||||||
"original_title": "Оригинальное название",
|
|
||||||
"series_type": "Тип сериала",
|
|
||||||
"release_dates": "Дата релиза",
|
|
||||||
"first_air_date": "Первая дата выхода в эфир",
|
|
||||||
"next_air_date": "Следующая дата выхода в эфир",
|
|
||||||
"revenue": "Прибыль",
|
|
||||||
"budget": "Бюджет",
|
|
||||||
"original_language": "Оригинальный язык",
|
|
||||||
"production_country": "Страна производства",
|
|
||||||
"studios": "Студия",
|
|
||||||
"network": "Сеть",
|
|
||||||
"currently_streaming_on": "Сейчас доступно на",
|
|
||||||
"advanced": "Продвинутое",
|
|
||||||
"request_as": "Запросить как",
|
|
||||||
"tags": "Тэги",
|
|
||||||
"quality_profile": "Профиль качества",
|
|
||||||
"root_folder": "Корневая папка",
|
|
||||||
"season_all": "Сезон (все)",
|
|
||||||
"season_number": "Сезон {{season_number}}",
|
|
||||||
"number_episodes": "{{episode_number}} серий",
|
|
||||||
"born": "Рожден",
|
|
||||||
"appearances": "Появления",
|
|
||||||
"toasts": {
|
|
||||||
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не соответствует минимальным требованиям версии! Пожалуйста, обновите до версии не ниже 2.0.0",
|
|
||||||
"jellyseerr_test_failed": "Тест Jellyseerr не пройден. Попробуйте еще раз.",
|
|
||||||
"failed_to_test_jellyseerr_server_url": "Не удалось проверить URL-адрес сервера jellyseerr",
|
|
||||||
"issue_submitted": "Проблема отправлена!",
|
|
||||||
"requested_item": "Запрошено {{item}}!",
|
|
||||||
"you_dont_have_permission_to_request": "У вас нет разрешения на запрос!",
|
|
||||||
"something_went_wrong_requesting_media": "Что-то пошло не так при запросе медиафайлов!"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tabs": {
|
|
||||||
"home": "Дом",
|
|
||||||
"search": "Поиск",
|
|
||||||
"library": "Библиотека",
|
|
||||||
"custom_links": "Кастомные ссылки",
|
|
||||||
"favorites": "Избранное"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -30,24 +30,5 @@
|
|||||||
"home": "Hem",
|
"home": "Hem",
|
||||||
"search": "Sök",
|
"search": "Sök",
|
||||||
"library": "Bibliotek"
|
"library": "Bibliotek"
|
||||||
},
|
|
||||||
"player": {
|
|
||||||
"error": "Fel",
|
|
||||||
"failed_to_get_stream_url": "Kunde inte hämta stream-URL",
|
|
||||||
"an_error_occured_while_playing_the_video": "Ett fel uppstod vid uppspelning av videon. Kontrollera loggarna i inställningarna.",
|
|
||||||
"client_error": "Klientfel",
|
|
||||||
"could_not_create_stream_for_chromecast": "Kunde inte skapa stream för Chromecast",
|
|
||||||
"message_from_server": "Meddelande från servern: {{message}}",
|
|
||||||
"video_has_finished_playing": "Videon har spelat klart!",
|
|
||||||
"no_video_source": "Ingen videokälla...",
|
|
||||||
"next_episode": "Nästa avsnitt",
|
|
||||||
"refresh_tracks": "Uppdatera spår",
|
|
||||||
"subtitle_tracks": "Textspår:",
|
|
||||||
"audio_tracks": "Ljudspår:",
|
|
||||||
"playback_state": "Uppspelningsstatus:",
|
|
||||||
"no_data_available": "Inga data tillgängliga",
|
|
||||||
"index": "Index:",
|
|
||||||
"continue_watching": "Fortsätt titta",
|
|
||||||
"go_back": "Tillbaka"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,480 +0,0 @@
|
|||||||
{
|
|
||||||
"login": {
|
|
||||||
"username_required": "tlhIngan DaneH",
|
|
||||||
"error_title": "ghIq",
|
|
||||||
"login_title": "lut 'el",
|
|
||||||
"login_to_title": "lut 'el",
|
|
||||||
"username_placeholder": "tlhIngan",
|
|
||||||
"password_placeholder": "ngoq De'",
|
|
||||||
"login_button": "yI'el!",
|
|
||||||
"quick_connect": "parmaq ngoQ",
|
|
||||||
"enter_code_to_login": "yI'elDI' De' {{code}} yIlaD",
|
|
||||||
"failed_to_initiate_quick_connect": "parmaq ngoQ yIchu'laHbe'",
|
|
||||||
"got_it": "jIyaj",
|
|
||||||
"connection_failed": "ngoQlaHbe'",
|
|
||||||
"could_not_connect_to_server": "SeHlaw veS Ho'Do'laHbe'. URL 'ej ret ghun mej.",
|
|
||||||
"an_unexpected_error_occured": "num ghIq Doch",
|
|
||||||
"change_server": "Ho'Do' veS yIghoS",
|
|
||||||
"invalid_username_or_password": "tlhIngan pagh ngoq De' law'be'",
|
|
||||||
"user_does_not_have_permission_to_log_in": "tlhIngan lut 'el je'laHbe'",
|
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Ho'Do' veS jachrup. pItlh yIHaD.",
|
|
||||||
"server_received_too_many_requests_try_again_later": "Ho'Do' veS lutlh ngeb petlh law'. pItlh yIHaD.",
|
|
||||||
"there_is_a_server_error": "Ho'Do' veS ghIq maS",
|
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "num ghIq Doch. URL mej Danej'a'?"
|
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"enter_url_to_jellyfin_server": "Jellyfin Ho'Do' veS URL yI'el",
|
|
||||||
"server_url_placeholder": "http(s)://HoDo-veS.com",
|
|
||||||
"connect_button": "yIngoq!",
|
|
||||||
"previous_servers": "namen Ho'Do' veS",
|
|
||||||
"clear_button": "yIQaw'",
|
|
||||||
"search_for_local_servers": "val Ho'Do' veS yISam",
|
|
||||||
"searching": "Sam...",
|
|
||||||
"servers": "Ho'Do' veS"
|
|
||||||
},
|
|
||||||
"home": {
|
|
||||||
"no_internet": "ret pagh",
|
|
||||||
"no_items": "Doch pagh",
|
|
||||||
"no_internet_message": "QublaHbe'.\nDoch Qaw'laHnIS SoH.",
|
|
||||||
"go_to_downloads": "Qaw' Doch yIghoS",
|
|
||||||
"oops": "QI'ya!",
|
|
||||||
"error_message": "Doch rurbe'.\nyIQo' 'ej yI'elqa'.",
|
|
||||||
"continue_watching": "tlhol yIHaDqa'",
|
|
||||||
"next_up": "wej",
|
|
||||||
"recently_added_in": "num tu'lu' {{libraryName}}",
|
|
||||||
"suggested_movies": "rutlh DIS",
|
|
||||||
"suggested_episodes": "rutlh Hem",
|
|
||||||
"intro": {
|
|
||||||
"welcome_to_streamyfin": "Streamyfin yI'el!",
|
|
||||||
"a_free_and_open_source_client_for_jellyfin": "Jellyfin lut 'el je'be' 'ej wang.",
|
|
||||||
"features_title": "mIw",
|
|
||||||
"features_description": "Streamyfin mIw law' tu'. men menuDaq yISam:",
|
|
||||||
"jellyseerr_feature_description": "Jellyseerr yIngoq 'ej DIS pe'vIl yISov.",
|
|
||||||
"downloads_feature_title": "Qaw' Doch",
|
|
||||||
"downloads_feature_description": "DIS 'ej Hem Qaw'laH. Qaw' mIw tu'lu'.",
|
|
||||||
"chromecast_feature_description": "DIS 'ej Hem Chromecast vI' ghoS.",
|
|
||||||
"centralised_settings_plugin_title": "wa'DIch men mIw",
|
|
||||||
"centralised_settings_plugin_description": "Jellyfin Ho'Do' veSDaq men yISeH. tlhIngan chIch.",
|
|
||||||
"done_button": "Qapla'",
|
|
||||||
"go_to_settings_button": "men yIghoS",
|
|
||||||
"read_more": "yIlaDqa'"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"settings_title": "men",
|
|
||||||
"log_out_button": "yIQo'",
|
|
||||||
"user_info": {
|
|
||||||
"user_info_title": "tlhIngan De'",
|
|
||||||
"user": "tlhIngan",
|
|
||||||
"server": "Ho'Do' veS",
|
|
||||||
"token": "per De'",
|
|
||||||
"app_version": "ghun wej"
|
|
||||||
},
|
|
||||||
"quick_connect": {
|
|
||||||
"quick_connect_title": "parmaq ngoQ",
|
|
||||||
"authorize_button": "parmaq ngoQ yIje'",
|
|
||||||
"enter_the_quick_connect_code": "parmaq ngoQ De' yI'el...",
|
|
||||||
"success": "Qapla'",
|
|
||||||
"quick_connect_autorized": "parmaq ngoQ je'laH",
|
|
||||||
"error": "ghIq",
|
|
||||||
"invalid_code": "De' law'be'",
|
|
||||||
"authorize": "yIje'"
|
|
||||||
},
|
|
||||||
"media_controls": {
|
|
||||||
"media_controls_title": "tlhol SeHlaw",
|
|
||||||
"forward_skip_length": "Du'Hom vum",
|
|
||||||
"rewind_length": "bavHom vum",
|
|
||||||
"seconds_unit": "tera' rep"
|
|
||||||
},
|
|
||||||
"audio": {
|
|
||||||
"audio_title": "QoQ",
|
|
||||||
"set_audio_track": "namen Doch QoQ ret yISeH",
|
|
||||||
"audio_language": "QoQ Hol",
|
|
||||||
"audio_hint": "QoQ Hol wa' yIwIv.",
|
|
||||||
"none": "pagh",
|
|
||||||
"language": "Hol"
|
|
||||||
},
|
|
||||||
"subtitles": {
|
|
||||||
"subtitle_title": "De' chu'",
|
|
||||||
"subtitle_language": "De' chu' Hol",
|
|
||||||
"subtitle_mode": "De' chu' mIw",
|
|
||||||
"set_subtitle_track": "namen Doch De' chu' ret yISeH",
|
|
||||||
"subtitle_size": "De' chu' qIt",
|
|
||||||
"subtitle_hint": "De' chu' wIvlaw' yISeH.",
|
|
||||||
"none": "pagh",
|
|
||||||
"language": "Hol",
|
|
||||||
"loading": "tlha'... ",
|
|
||||||
"modes": {
|
|
||||||
"Default": "wa'",
|
|
||||||
"Smart": "SonchIy",
|
|
||||||
"Always": "reH",
|
|
||||||
"None": "pagh",
|
|
||||||
"OnlyForced": "Dun je'"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"other": {
|
|
||||||
"other_title": "patlh",
|
|
||||||
"follow_device_orientation": "naDevvo' pegh",
|
|
||||||
"video_orientation": "mu'tlhegh pegh",
|
|
||||||
"orientation": "pegh",
|
|
||||||
"orientations": {
|
|
||||||
"DEFAULT": "wa'",
|
|
||||||
"ALL": "Hoch",
|
|
||||||
"PORTRAIT": "leng ret",
|
|
||||||
"PORTRAIT_UP": "leng ret Dung",
|
|
||||||
"PORTRAIT_DOWN": "leng ret nuq",
|
|
||||||
"LANDSCAPE": "leng yot",
|
|
||||||
"LANDSCAPE_LEFT": "leng yot poS",
|
|
||||||
"LANDSCAPE_RIGHT": "leng yot nIH",
|
|
||||||
"OTHER": "patlh",
|
|
||||||
"UNKNOWN": "Sovbe'"
|
|
||||||
},
|
|
||||||
"safe_area_in_controls": "SeHlawDaq yot QIH",
|
|
||||||
"video_player": "mu'tlhegh tlholwI'",
|
|
||||||
"video_players": {
|
|
||||||
"VLC_3": "VLC 3",
|
|
||||||
"VLC_4": "VLC 4 (PiP mIwHa')"
|
|
||||||
},
|
|
||||||
"show_custom_menu_links": "menuDaq ret teqlu' yInej",
|
|
||||||
"hide_libraries": "De'wI' bom yIQIj",
|
|
||||||
"select_liraries_you_want_to_hide": "De'wI' bom Danej QIj yIwIv.",
|
|
||||||
"disable_haptic_feedback": "Qub quvHa' yIQIj",
|
|
||||||
"default_quality": "wa' luj"
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "Qaw' Doch",
|
|
||||||
"download_method": "Qaw' mIw",
|
|
||||||
"remux_max_download": "Remux Qaw' Dun",
|
|
||||||
"auto_download": "chIch Qaw'",
|
|
||||||
"optimized_versions_server": "luj wej Ho'Do' veS",
|
|
||||||
"save_button": "yIqIp",
|
|
||||||
"optimized_server": "luj Ho'Do' veS",
|
|
||||||
"optimized": "luj",
|
|
||||||
"default": "wa'",
|
|
||||||
"optimized_version_hint": "luj Ho'Do' veS URL yI'el.",
|
|
||||||
"read_more_about_optimized_server": "luj Ho'Do' veS latlh yIlaD",
|
|
||||||
"url": "URL",
|
|
||||||
"server_url_placeholder": "http(s)://domajn.org:pord"
|
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"plugins_title": "mIwHom",
|
|
||||||
"jellyseerr": {
|
|
||||||
"jellyseerr_warning": "mIwHomvam chu'. ghoSlaH.",
|
|
||||||
"server_url": "Ho'Do' veS URL",
|
|
||||||
"server_url_hint": "ghu': http(s)://HoDo-veS.url\n(pord yIbel)",
|
|
||||||
"server_url_placeholder": "Jellyseerr URL...",
|
|
||||||
"password": "ngoq De'",
|
|
||||||
"password_placeholder": "tlhIngan {{username}} ngoq De' yI'el",
|
|
||||||
"save_button": "yIqIp",
|
|
||||||
"clear_button": "yIQaw'",
|
|
||||||
"login_button": "yI'el!",
|
|
||||||
"total_media_requests": "Hoch tlhol petlh",
|
|
||||||
"movie_quota_limit": "DIS petlh Dun",
|
|
||||||
"movie_quota_days": "DIS petlh jaj",
|
|
||||||
"tv_quota_limit": "TV petlh Dun",
|
|
||||||
"tv_quota_days": "TV petlh jaj",
|
|
||||||
"reset_jellyseerr_config_button": "Jellyseerr men yIQaw'qa'",
|
|
||||||
"unlimited": "Dun pagh",
|
|
||||||
"plus_n_more": "+{{n}} latlh",
|
|
||||||
"order_by": {
|
|
||||||
"DEFAULT": "wa'",
|
|
||||||
"VOTE_COUNT_AND_AVERAGE": "nem chIm 'ej mev",
|
|
||||||
"POPULARITY": "ruch"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"marlin_search": {
|
|
||||||
"enable_marlin_search": "Marlin Sam yIchu'",
|
|
||||||
"url": "URL",
|
|
||||||
"server_url_placeholder": "http(s)://domajn.org:pord",
|
|
||||||
"marlin_search_hint": "Marlin Ho'Do' veS URL yI'el.",
|
|
||||||
"read_more_about_marlin": "Marlin latlh yIlaD",
|
|
||||||
"save_button": "yIqIp",
|
|
||||||
"toasts": {
|
|
||||||
"saved": "qIp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"storage": {
|
|
||||||
"storage_title": "ram",
|
|
||||||
"app_usage": "ghun {{usedSpace}}%",
|
|
||||||
"device_usage": "naDev {{availableSpace}}%",
|
|
||||||
"size_used": "{{used}} / {{total}} ram",
|
|
||||||
"delete_all_downloaded_files": "Hoch Qaw' Doch yIQaw'"
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"show_intro": "chu' Doch yIHoch",
|
|
||||||
"reset_intro": "chu' Doch yIQaw'qa'"
|
|
||||||
},
|
|
||||||
"logs": {
|
|
||||||
"logs_title": "De' qon",
|
|
||||||
"export_logs": "De' qon yISamqa'",
|
|
||||||
"click_for_more_info": "latlh De' yIchIch",
|
|
||||||
"level": "quv",
|
|
||||||
"no_logs_available": "De' qon pagh",
|
|
||||||
"delete_all_logs": "Hoch De' qon yIQaw'"
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"title": "Holmey",
|
|
||||||
"app_language": "ghun Hol",
|
|
||||||
"app_language_description": "ghun Hol yIwIv.",
|
|
||||||
"system": "mIw'a'"
|
|
||||||
},
|
|
||||||
"toasts": {
|
|
||||||
"error_deleting_files": "Qaw' ghIq",
|
|
||||||
"background_downloads_enabled": "tlhegh Qaw' chu'",
|
|
||||||
"background_downloads_disabled": "tlhegh Qaw' QIj",
|
|
||||||
"connected": "ngoQ",
|
|
||||||
"could_not_connect": "ngoQlaHbe'",
|
|
||||||
"invalid_url": "URL law'be'"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sessions": {
|
|
||||||
"title": "tlholrap",
|
|
||||||
"no_active_sessions": "tlholrap pagh chu'"
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "Qaw' Doch",
|
|
||||||
"tvseries": "TV Hem",
|
|
||||||
"movies": "DIS",
|
|
||||||
"queue": "ghom",
|
|
||||||
"queue_hint": "ghun ghImDI' ghom Qaw'laH.",
|
|
||||||
"no_items_in_queue": "ghom Doch pagh",
|
|
||||||
"no_downloaded_items": "Qaw' Doch pagh",
|
|
||||||
"delete_all_movies_button": "Hoch DIS yIQaw'",
|
|
||||||
"delete_all_tvseries_button": "Hoch TV Hem yIQaw'",
|
|
||||||
"delete_all_button": "Hoch yIQaw'",
|
|
||||||
"active_download": "chu' Qaw'",
|
|
||||||
"no_active_downloads": "chu' Qaw' pagh",
|
|
||||||
"active_downloads": "chu' Qaw'",
|
|
||||||
"new_app_version_requires_re_download": "ghun wej chu' Qaw'qa' DaneH",
|
|
||||||
"new_app_version_requires_re_download_description": "wej chu' Doch Qaw'qa' DaneH. Hoch Qaw' Doch yIQaw' 'ej yIHaDqa'.",
|
|
||||||
"back": "yIbav",
|
|
||||||
"delete": "yIQaw'",
|
|
||||||
"something_went_wrong": "Doch rurbe'",
|
|
||||||
"could_not_get_stream_url_from_jellyfin": "Jellyfin tlhol ret URL tu'laHbe'",
|
|
||||||
"eta": "ETA {{eta}}",
|
|
||||||
"methods": "mIw",
|
|
||||||
"toasts": {
|
|
||||||
"you_are_not_allowed_to_download_files": "Doch Qaw' je'laHbe'.",
|
|
||||||
"deleted_all_movies_successfully": "Hoch DIS Qaw' Qapla'!",
|
|
||||||
"failed_to_delete_all_movies": "Hoch DIS Qaw'laHbe'",
|
|
||||||
"deleted_all_tvseries_successfully": "Hoch TV Hem Qaw' Qapla'!",
|
|
||||||
"failed_to_delete_all_tvseries": "Hoch TV Hem Qaw'laHbe'",
|
|
||||||
"download_cancelled": "Qaw' ghIm",
|
|
||||||
"could_not_cancel_download": "Qaw' ghImlaHbe'",
|
|
||||||
"download_completed": "Qaw' Qapla'",
|
|
||||||
"download_started_for": "{{item}} Qaw' vIlchu'",
|
|
||||||
"item_is_ready_to_be_downloaded": "{{item}} Qaw'laHnIS",
|
|
||||||
"download_stated_for_item": "{{item}} Qaw' vIlchu'",
|
|
||||||
"download_failed_for_item": "{{item}} Qaw'laHbe' - {{error}}",
|
|
||||||
"download_completed_for_item": "{{item}} Qaw' Qapla'",
|
|
||||||
"queued_item_for_optimization": "{{item}} luj ghom",
|
|
||||||
"failed_to_start_download_for_item": "{{item}} Qaw' vIlchu'laHbe': {{message}}",
|
|
||||||
"server_responded_with_status_code": "Ho'Do' veS jachrup {{statusCode}}",
|
|
||||||
"no_response_received_from_server": "Ho'Do' veS jachbe'",
|
|
||||||
"error_setting_up_the_request": "petlh SeH ghIq",
|
|
||||||
"failed_to_start_download_for_item_unexpected_error": "{{item}} Qaw' vIlchu'laHbe': num ghIq",
|
|
||||||
"all_files_folders_and_jobs_deleted_successfully": "Hoch De', ram 'ej vum Qaw' Qapla'",
|
|
||||||
"an_error_occured_while_deleting_files_and_jobs": "De', ram 'ej vum Qaw'DI' ghIq",
|
|
||||||
"go_to_downloads": "Qaw' Doch yIghoS"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"search_here": "DaH yISam...",
|
|
||||||
"search": "yISam...",
|
|
||||||
"x_items": "{{count}} Doch",
|
|
||||||
"library": "De'wI' bom",
|
|
||||||
"discover": "yISamqa'",
|
|
||||||
"no_results": "Doch pagh tu'",
|
|
||||||
"no_results_found_for": "Doch pagh tu' <...>",
|
|
||||||
"movies": "DIS",
|
|
||||||
"series": "Hem",
|
|
||||||
"episodes": "HemHom",
|
|
||||||
"collections": "ghom",
|
|
||||||
"actors": "tlholwI'",
|
|
||||||
"request_movies": "DIS yIpetlh",
|
|
||||||
"request_series": "Hem yIpetlh",
|
|
||||||
"recently_added": "num tu'",
|
|
||||||
"recent_requests": "num petlh",
|
|
||||||
"plex_watchlist": "Plex tlhol ghom",
|
|
||||||
"trending": "chu' ruch",
|
|
||||||
"popular_movies": "ruch DIS",
|
|
||||||
"movie_genres": "DIS qorDu'",
|
|
||||||
"upcoming_movies": "DIS wej",
|
|
||||||
"studios": "DIS qonwI'",
|
|
||||||
"popular_tv": "ruch TV",
|
|
||||||
"tv_genres": "TV qorDu'",
|
|
||||||
"upcoming_tv": "TV wej",
|
|
||||||
"networks": "ret",
|
|
||||||
"tmdb_movie_keyword": "TMDB DIS De'",
|
|
||||||
"tmdb_movie_genre": "TMDB DIS qorDu'",
|
|
||||||
"tmdb_tv_keyword": "TMDB TV De'",
|
|
||||||
"tmdb_tv_genre": "TMDB TV qorDu'",
|
|
||||||
"tmdb_search": "TMDB Sam",
|
|
||||||
"tmdb_studio": "TMDB qonwI'",
|
|
||||||
"tmdb_network": "TMDB ret",
|
|
||||||
"tmdb_movie_streaming_services": "TMDB DIS tlhol mIw",
|
|
||||||
"tmdb_tv_streaming_services": "TMDB TV tlhol mIw"
|
|
||||||
},
|
|
||||||
"library": {
|
|
||||||
"no_items_found": "Doch pagh tu'",
|
|
||||||
"no_results": "Doch pagh tu'",
|
|
||||||
"no_libraries_found": "De'wI' bom pagh tu'",
|
|
||||||
"item_types": {
|
|
||||||
"movies": "DIS",
|
|
||||||
"series": "Hem",
|
|
||||||
"boxsets": "Hem ghom",
|
|
||||||
"items": "Doch"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"display": "yIHoch",
|
|
||||||
"row": "ret",
|
|
||||||
"list": "ghom",
|
|
||||||
"image_style": "nagh bep",
|
|
||||||
"poster": "nagh",
|
|
||||||
"cover": "nagh chop",
|
|
||||||
"show_titles": "pab HoS yIHoch",
|
|
||||||
"show_stats": "chIm De' yIHoch"
|
|
||||||
},
|
|
||||||
"filters": {
|
|
||||||
"genres": "qorDu'",
|
|
||||||
"years": "DIS",
|
|
||||||
"sort_by": "yIwIv",
|
|
||||||
"sort_order": "wIv mIw",
|
|
||||||
"asc": "Dung",
|
|
||||||
"desc": "nuq",
|
|
||||||
"tags": "De'Hom"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"favorites": {
|
|
||||||
"series": "Hem",
|
|
||||||
"movies": "DIS",
|
|
||||||
"episodes": "HemHom",
|
|
||||||
"videos": "mu'tlhegh",
|
|
||||||
"boxsets": "Hem ghom",
|
|
||||||
"playlists": "bom ghom",
|
|
||||||
"noDataTitle": "wIv Doch pagh",
|
|
||||||
"noData": "Doch wIv DaneH. DaH tu'laH."
|
|
||||||
},
|
|
||||||
"custom_links": {
|
|
||||||
"no_links": "ret pagh"
|
|
||||||
},
|
|
||||||
"player": {
|
|
||||||
"error": "ghIq",
|
|
||||||
"failed_to_get_stream_url": "tlhol ret URL tu'laHbe'",
|
|
||||||
"an_error_occured_while_playing_the_video": "mu'tlhegh tlholDI' ghIq. menDaq De' qon mej.",
|
|
||||||
"client_error": "lut 'el ghIq",
|
|
||||||
"could_not_create_stream_for_chromecast": "Chromecast tlhol ret qonlaHbe'",
|
|
||||||
"message_from_server": "Ho'Do' veS jach: {{message}}",
|
|
||||||
"video_has_finished_playing": "mu'tlhegh tlhol Qapla'!",
|
|
||||||
"no_video_source": "mu'tlhegh wang pagh",
|
|
||||||
"next_episode": "wej HemHom",
|
|
||||||
"refresh_tracks": "ret yIchu'qa'",
|
|
||||||
"subtitle_tracks": "De' chu' ret:",
|
|
||||||
"audio_tracks": "QoQ ret:",
|
|
||||||
"playback_state": "tlhol mIw:",
|
|
||||||
"no_data_available": "De' pagh tu'",
|
|
||||||
"index": "nem:"
|
|
||||||
},
|
|
||||||
"item_card": {
|
|
||||||
"next_up": "wej",
|
|
||||||
"no_items_to_display": "Doch pagh HochlaH",
|
|
||||||
"cast_and_crew": "tlholwI' 'ej qonwI'",
|
|
||||||
"series": "Hem",
|
|
||||||
"seasons": "muv",
|
|
||||||
"season": "muv",
|
|
||||||
"no_episodes_for_this_season": "muvvam HemHom pagh",
|
|
||||||
"overview": "Hoch Sov",
|
|
||||||
"more_with": "{{name}} latlh",
|
|
||||||
"similar_items": "Doch rur",
|
|
||||||
"no_similar_items_found": "Doch rur pagh tu'",
|
|
||||||
"video": "mu'tlhegh",
|
|
||||||
"more_details": "latlh De'",
|
|
||||||
"quality": "luj",
|
|
||||||
"audio": "QoQ",
|
|
||||||
"subtitles": "De' chu'",
|
|
||||||
"show_more": "latlh yIHoch",
|
|
||||||
"show_less": "Hom yIHoch",
|
|
||||||
"appeared_in": "tlholvam",
|
|
||||||
"could_not_load_item": "Doch tlha'laHbe'",
|
|
||||||
"none": "pagh",
|
|
||||||
"download": {
|
|
||||||
"download_season": "muv yIQaw'",
|
|
||||||
"download_series": "Hem yIQaw'",
|
|
||||||
"download_episode": "HemHom yIQaw'",
|
|
||||||
"download_movie": "DIS yIQaw'",
|
|
||||||
"download_x_item": "{{item_count}} Doch yIQaw'",
|
|
||||||
"download_button": "yIQaw'",
|
|
||||||
"using_optimized_server": "luj Ho'Do' veS tu'lu'",
|
|
||||||
"using_default_method": "wa' mIw tu'lu'"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"live_tv": {
|
|
||||||
"next": "wej",
|
|
||||||
"previous": "namen",
|
|
||||||
"live_tv": "chu' TV",
|
|
||||||
"coming_soon": "wej lup",
|
|
||||||
"on_now": "DaH",
|
|
||||||
"shows": "tlhol",
|
|
||||||
"movies": "DIS",
|
|
||||||
"sports": "QI'",
|
|
||||||
"for_kids": "puqbeq",
|
|
||||||
"news": "De'"
|
|
||||||
},
|
|
||||||
"jellyseerr": {
|
|
||||||
"confirm": "yInej",
|
|
||||||
"cancel": "yIQo'",
|
|
||||||
"yes": "HIja'",
|
|
||||||
"whats_wrong": "Doch rurbe' 'Iv?",
|
|
||||||
"issue_type": "ghIq bep",
|
|
||||||
"select_an_issue": "ghIq yIwIv",
|
|
||||||
"types": "bep",
|
|
||||||
"describe_the_issue": "(num) ghIq yIqon...",
|
|
||||||
"submit_button": "yInejqa'",
|
|
||||||
"report_issue_button": "ghIq yIqon",
|
|
||||||
"request_button": "yIpetlh",
|
|
||||||
"are_you_sure_you_want_to_request_all_seasons": "Hoch muv Danej petlh'a'?",
|
|
||||||
"failed_to_login": "'ellaHbe'",
|
|
||||||
"cast": "tlholwI'",
|
|
||||||
"details": "De'",
|
|
||||||
"status": "mIw",
|
|
||||||
"original_title": "wa'DIch pab HoS",
|
|
||||||
"series_type": "Hem bep",
|
|
||||||
"release_dates": "Qaw' jaj",
|
|
||||||
"first_air_date": "wa'DIch tlhol jaj",
|
|
||||||
"next_air_date": "wej tlhol jaj",
|
|
||||||
"revenue": "boj De'",
|
|
||||||
"budget": "boj nem",
|
|
||||||
"original_language": "wa'DIch Hol",
|
|
||||||
"production_country": "qonwI' qo'",
|
|
||||||
"studios": "qonwI'",
|
|
||||||
"network": "ret",
|
|
||||||
"currently_streaming_on": "DaH tlhol <...>",
|
|
||||||
"advanced": "SonchIy",
|
|
||||||
"request_as": "yIpetlh <...>",
|
|
||||||
"tags": "De'Hom",
|
|
||||||
"quality_profile": "luj wIvlaw'",
|
|
||||||
"root_folder": "wa'DIch ram",
|
|
||||||
"season_all": "muv (Hoch)",
|
|
||||||
"season_number": "muv {{season_number}}",
|
|
||||||
"number_episodes": "{{episode_number}} HemHom",
|
|
||||||
"born": "poS",
|
|
||||||
"appearances": "tlholvam",
|
|
||||||
"toasts": {
|
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr Ho'Do' veS wej law'be'! 2.0.0 yIchu'!",
|
|
||||||
"jellyseerr_test_failed": "Jellyseerr nejlaHbe'. yIHaDqa'.",
|
|
||||||
"failed_to_test_jellyseerr_server_url": "Jellyseerr Ho'Do' veS URL nejlaHbe'",
|
|
||||||
"issue_submitted": "ghIq nejqa'!",
|
|
||||||
"requested_item": "{{item}} petlh!",
|
|
||||||
"you_dont_have_permission_to_request": "petlh je'laHbe'!",
|
|
||||||
"something_went_wrong_requesting_media": "tlhol petlhDI' Doch rurbe'!"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tabs": {
|
|
||||||
"home": "juH",
|
|
||||||
"search": "Sam",
|
|
||||||
"library": "De'wI' bom",
|
|
||||||
"custom_links": "teqlu' ret",
|
|
||||||
"favorites": "wIv Doch"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -137,9 +137,7 @@
|
|||||||
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
|
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
|
||||||
"hide_libraries": "Kütüphaneleri Gizle",
|
"hide_libraries": "Kütüphaneleri Gizle",
|
||||||
"select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.",
|
"select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.",
|
||||||
"disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak",
|
"disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak"
|
||||||
"default_quality": "Varsayılan kalite",
|
|
||||||
"disabled": "Devre dışı"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "İndirmeler",
|
"downloads_title": "İndirmeler",
|
||||||
@@ -371,9 +369,7 @@
|
|||||||
"audio_tracks": "Ses Parçaları:",
|
"audio_tracks": "Ses Parçaları:",
|
||||||
"playback_state": "Oynatma Durumu:",
|
"playback_state": "Oynatma Durumu:",
|
||||||
"no_data_available": "Veri bulunamadı",
|
"no_data_available": "Veri bulunamadı",
|
||||||
"index": "İndeks:",
|
"index": "İndeks:"
|
||||||
"continue_watching": "İzlemeye devam et",
|
|
||||||
"go_back": "Geri"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Sıradaki",
|
"next_up": "Sıradaki",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"invalid_username_or_password": "Неправильні імʼя користувача або пароль",
|
"invalid_username_or_password": "Неправильні імʼя користувача або пароль",
|
||||||
"user_does_not_have_permission_to_log_in": "Користувач не маю дозволу на вхід",
|
"user_does_not_have_permission_to_log_in": "Користувач не маю дозволу на вхід",
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Сервер відповідає занадто довго, будь-ласка спробуйте пізніше",
|
"server_is_taking_too_long_to_respond_try_again_later": "Сервер відповідає занадто довго, будь-ласка спробуйте пізніше",
|
||||||
"server_received_too_many_requests_try_again_later": "Сервер отримав забагато запитів, будь ласка спробуйте пізніше.",
|
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
|
||||||
"there_is_a_server_error": "Відбулася помилка на стороні сервера",
|
"there_is_a_server_error": "Відбулася помилка на стороні сервера",
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Відбулася несподівана помилка. Чи введений URL сервера правильний?"
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Відбулася несподівана помилка. Чи введений URL сервера правильний?"
|
||||||
},
|
},
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"error_message": "Щось пішло не так.\nБудь ласка вийдіть і увійдіть знов.",
|
"error_message": "Щось пішло не так.\nБудь ласка вийдіть і увійдіть знов.",
|
||||||
"continue_watching": "Продовжити перегляд",
|
"continue_watching": "Продовжити перегляд",
|
||||||
"next_up": "Далі",
|
"next_up": "Далі",
|
||||||
"recently_added_in": "Нещодавно додане до \"{{libraryName}}\"",
|
"recently_added_in": "Нещодавно додане до медіатеки {{libraryName}}",
|
||||||
"suggested_movies": "Рекомендовані Фільми",
|
"suggested_movies": "Рекомендовані Фільми",
|
||||||
"suggested_episodes": "Рекомендовані Епізоди",
|
"suggested_episodes": "Рекомендовані Епізоди",
|
||||||
"intro": {
|
"intro": {
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"jellyseerr_feature_description": "Підключіться до вашого екземпляру Jellyseerr і запитуватуйте фільми безпосередньо в застосунку.",
|
"jellyseerr_feature_description": "Підключіться до вашого екземпляру Jellyseerr і запитуватуйте фільми безпосередньо в застосунку.",
|
||||||
"downloads_feature_title": "Завантаження",
|
"downloads_feature_title": "Завантаження",
|
||||||
"downloads_feature_description": "Завантажуйте фільми і серіали для перегляду офлайн. Використовуйте або метод за замовчуванням або встановіть оптимізований сервер для завантаження файлів у фоні.",
|
"downloads_feature_description": "Завантажуйте фільми і серіали для перегляду офлайн. Використовуйте або метод за замовчуванням або встановіть оптимізований сервер для завантаження файлів у фоні.",
|
||||||
"chromecast_feature_description": "Транслюйте фільми і серіали на ваші Chromecast прилади.",
|
"chromecast_feature_description": "Транслюйте фільми і серіали но ваші Chromecast прилади.",
|
||||||
"centralised_settings_plugin_title": "Centralised Settings Plugin",
|
"centralised_settings_plugin_title": "Centralised Settings Plugin",
|
||||||
"centralised_settings_plugin_description": "Налаштуйте параметри з централізованої локації на вашому сервері Jellyfin. Всі налаштування клієнтів для всіх користувачів будуть синхронізовані автоматично.",
|
"centralised_settings_plugin_description": "Налаштуйте параметри з централізованої локації на вашому сервері Jellyfin. Всі налаштування клієнтів для всіх користувачів будуть синхронізовані автоматично.",
|
||||||
"done_button": "Готово",
|
"done_button": "Готово",
|
||||||
@@ -80,14 +80,14 @@
|
|||||||
"authorize": "Авторизувати"
|
"authorize": "Авторизувати"
|
||||||
},
|
},
|
||||||
"media_controls": {
|
"media_controls": {
|
||||||
"media_controls_title": "Керування Медіа",
|
"media_controls_title": "Керування Медія",
|
||||||
"forward_skip_length": "Довжина перемотування вперед",
|
"forward_skip_length": "Тривалість перемотування вперед",
|
||||||
"rewind_length": "Довжина перемотування назад",
|
"rewind_length": "Довжина перемотування назад",
|
||||||
"seconds_unit": "с"
|
"seconds_unit": "с"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"audio_title": "Аудіо",
|
"audio_title": "Аудіо",
|
||||||
"set_audio_track": "Аудіо доріжка як в попередньому епізоді",
|
"set_audio_track": "Виставити аудіо доріжку як в попередньому епізоду",
|
||||||
"audio_language": "Мова аудіо",
|
"audio_language": "Мова аудіо",
|
||||||
"audio_hint": "Вибрати мову аудіо за замовчуванням.",
|
"audio_hint": "Вибрати мову аудіо за замовчуванням.",
|
||||||
"none": "Ніяка",
|
"none": "Ніяка",
|
||||||
@@ -134,19 +134,18 @@
|
|||||||
"VLC_3": "VLC 3",
|
"VLC_3": "VLC 3",
|
||||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
},
|
},
|
||||||
"show_custom_menu_links": "Показати користувацькі посилання меню",
|
"show_custom_menu_links": "Показати посилання на користувацьке меню",
|
||||||
"hide_libraries": "Сховати медіатеки",
|
"hide_libraries": "Сховати медіатеки",
|
||||||
"select_liraries_you_want_to_hide": "Виберіть медіатеки, що бажаєте приховати з вкладки Медіатека і з секції на головній сторінці.",
|
"select_liraries_you_want_to_hide": "Виберіть медіатеки, що бажаєте приховати з вкладки Медіатека і з секції на головній сторінці.",
|
||||||
"disable_haptic_feedback": "Вимкнути тактильний зворотний зв'язок",
|
"disable_haptic_feedback": "Вимкнути тактильний зворотний зв'язок",
|
||||||
"default_quality": "Якість за замовченням",
|
"default_quality": "Якість за замовченням"
|
||||||
"disabled": "Вимкнено"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Завантаження",
|
"downloads_title": "Завантаження",
|
||||||
"download_method": "Метод завантаження",
|
"download_method": "Метод завантаження",
|
||||||
"remux_max_download": "Remux max download",
|
"remux_max_download": "Remux max download",
|
||||||
"auto_download": "Авто-завантаження",
|
"auto_download": "Авто-завантаження",
|
||||||
"optimized_versions_server": "Сервер оптимізованих версій",
|
"optimized_versions_server": "Optimized versions server",
|
||||||
"save_button": "Зберегти",
|
"save_button": "Зберегти",
|
||||||
"optimized_server": "Оптимізований Сервер",
|
"optimized_server": "Оптимізований Сервер",
|
||||||
"optimized": "Оптимізований",
|
"optimized": "Оптимізований",
|
||||||
@@ -353,9 +352,7 @@
|
|||||||
"episodes": "Епізоди",
|
"episodes": "Епізоди",
|
||||||
"videos": "Відео",
|
"videos": "Відео",
|
||||||
"boxsets": "Бокс-сети",
|
"boxsets": "Бокс-сети",
|
||||||
"playlists": "Плейлісти",
|
"playlists": "Плейлісти"
|
||||||
"noDataTitle": "Поки що нема обраного",
|
|
||||||
"noData": "Відмітьте як улюблене що би побачити це тут в швидкому доступі."
|
|
||||||
},
|
},
|
||||||
"custom_links": {
|
"custom_links": {
|
||||||
"no_links": "Немає посилань"
|
"no_links": "Немає посилань"
|
||||||
@@ -363,7 +360,7 @@
|
|||||||
"player": {
|
"player": {
|
||||||
"error": "Помилка",
|
"error": "Помилка",
|
||||||
"failed_to_get_stream_url": "Не вдалося отримати URL-адресу потоку",
|
"failed_to_get_stream_url": "Не вдалося отримати URL-адресу потоку",
|
||||||
"an_error_occured_while_playing_the_video": "Під час відтворення відео сталася помилка. Перевірте журнал в налаштуваннях.",
|
"an_error_occured_while_playing_the_video": "Під час відтворення відео сталася помилка. Перевірте журнали в налаштуваннях.",
|
||||||
"client_error": "Помилка клієнту",
|
"client_error": "Помилка клієнту",
|
||||||
"could_not_create_stream_for_chromecast": "Не вдалося створити потік для Chromecast",
|
"could_not_create_stream_for_chromecast": "Не вдалося створити потік для Chromecast",
|
||||||
"message_from_server": "Повідомлення від серверу: {{message}}",
|
"message_from_server": "Повідомлення від серверу: {{message}}",
|
||||||
@@ -375,9 +372,7 @@
|
|||||||
"audio_tracks": "Аудіо-доріжки:",
|
"audio_tracks": "Аудіо-доріжки:",
|
||||||
"playback_state": "Стан відтворення:",
|
"playback_state": "Стан відтворення:",
|
||||||
"no_data_available": "Дані відсутні",
|
"no_data_available": "Дані відсутні",
|
||||||
"index": "Індекс:",
|
"index": "Індекс:"
|
||||||
"continue_watching": "Продовжити перегляд",
|
|
||||||
"go_back": "Назад"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Далі",
|
"next_up": "Далі",
|
||||||
@@ -464,12 +459,12 @@
|
|||||||
"born": "Дата народження",
|
"born": "Дата народження",
|
||||||
"appearances": "Зовнішній вигляд",
|
"appearances": "Зовнішній вигляд",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Версія Jellyseerr не відповідає мінімальним вимогам! Будь ласка, оновіться принаймні до 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не відповідає мінімальним вимогам до версії! Будь ласка, оновіть версію принаймні до 2.0.0",
|
||||||
"jellyseerr_test_failed": "Тест Jellyseerr завершився невдало. Спробуйте ще раз.",
|
"jellyseerr_test_failed": "Тест Jellyseerr завершився невдало. Спробуйте ще раз.",
|
||||||
"failed_to_test_jellyseerr_server_url": "Не вдалося перевірити URL-адресу сервера jellyseerr",
|
"failed_to_test_jellyseerr_server_url": "Не вдалося перевірити URL-адресу сервера jellyseerr",
|
||||||
"issue_submitted": "Звіт про проблему відправлено",
|
"issue_submitted": "Звіт про проблему відправлено",
|
||||||
"requested_item": "Запитано {{item}}!",
|
"requested_item": "Запитано {{item}}!",
|
||||||
"you_dont_have_permission_to_request": "Ви не маєте дозволу на запит медіа!",
|
"you_dont_have_permission_to_request": "У вас нема дозволу на запит медіа!",
|
||||||
"something_went_wrong_requesting_media": "Щось пішло не так під час запиту медіа!"
|
"something_went_wrong_requesting_media": "Щось пішло не так під час запиту медіа!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -369,9 +369,7 @@
|
|||||||
"audio_tracks": "音频轨道:",
|
"audio_tracks": "音频轨道:",
|
||||||
"playback_state": "播放状态:",
|
"playback_state": "播放状态:",
|
||||||
"no_data_available": "无可用数据",
|
"no_data_available": "无可用数据",
|
||||||
"index": "索引:",
|
"index": "索引:"
|
||||||
"continue_watching": "继续观看",
|
|
||||||
"go_back": "返回"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "下一个",
|
"next_up": "下一个",
|
||||||
|
|||||||
@@ -137,9 +137,7 @@
|
|||||||
"show_custom_menu_links": "顯示自定義菜單鏈接",
|
"show_custom_menu_links": "顯示自定義菜單鏈接",
|
||||||
"hide_libraries": "隱藏媒體庫",
|
"hide_libraries": "隱藏媒體庫",
|
||||||
"select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
|
"select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
|
||||||
"disable_haptic_feedback": "禁用觸覺回饋",
|
"disable_haptic_feedback": "禁用觸覺回饋"
|
||||||
"default_quality": "預設品質",
|
|
||||||
"disabled": "已停用"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "下載",
|
"downloads_title": "下載",
|
||||||
@@ -371,9 +369,7 @@
|
|||||||
"audio_tracks": "音頻軌道:",
|
"audio_tracks": "音頻軌道:",
|
||||||
"playback_state": "播放狀態:",
|
"playback_state": "播放狀態:",
|
||||||
"no_data_available": "無可用數據",
|
"no_data_available": "無可用數據",
|
||||||
"index": "索引:",
|
"index": "索引:"
|
||||||
"continue_watching": "繼續觀看",
|
|
||||||
"go_back": "返回"
|
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "下一個",
|
"next_up": "下一個",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export enum SortByOption {
|
|||||||
CommunityRating = "CommunityRating",
|
CommunityRating = "CommunityRating",
|
||||||
CriticRating = "CriticRating",
|
CriticRating = "CriticRating",
|
||||||
DateCreated = "DateCreated",
|
DateCreated = "DateCreated",
|
||||||
DateLastContentAdded = "DateLastContentAdded",
|
|
||||||
DatePlayed = "DatePlayed",
|
DatePlayed = "DatePlayed",
|
||||||
PlayCount = "PlayCount",
|
PlayCount = "PlayCount",
|
||||||
ProductionYear = "ProductionYear",
|
ProductionYear = "ProductionYear",
|
||||||
@@ -38,7 +37,6 @@ export const sortOptions: {
|
|||||||
{ key: SortByOption.CommunityRating, value: "Community Rating" },
|
{ key: SortByOption.CommunityRating, value: "Community Rating" },
|
||||||
{ key: SortByOption.CriticRating, value: "Critics Rating" },
|
{ key: SortByOption.CriticRating, value: "Critics Rating" },
|
||||||
{ key: SortByOption.DateCreated, value: "Date Added" },
|
{ key: SortByOption.DateCreated, value: "Date Added" },
|
||||||
{ key: SortByOption.DateLastContentAdded, value: "Date Episode Added" },
|
|
||||||
{ key: SortByOption.DatePlayed, value: "Date Played" },
|
{ key: SortByOption.DatePlayed, value: "Date Played" },
|
||||||
{ key: SortByOption.PlayCount, value: "Play Count" },
|
{ key: SortByOption.PlayCount, value: "Play Count" },
|
||||||
{ key: SortByOption.ProductionYear, value: "Production Year" },
|
{ key: SortByOption.ProductionYear, value: "Production Year" },
|
||||||
|
|||||||
@@ -114,11 +114,6 @@ export type HomeSectionNextUpResolver = {
|
|||||||
enableRewatching?: boolean;
|
enableRewatching?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MaxAutoPlayEpisodeCount {
|
|
||||||
key: string;
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HomeSectionLatestResolver = {
|
export type HomeSectionLatestResolver = {
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@@ -168,8 +163,6 @@ export type Settings = {
|
|||||||
hiddenLibraries?: string[];
|
hiddenLibraries?: string[];
|
||||||
enableH265ForChromecast: boolean;
|
enableH265ForChromecast: boolean;
|
||||||
defaultPlayer: VideoPlayer;
|
defaultPlayer: VideoPlayer;
|
||||||
maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount;
|
|
||||||
autoPlayEpisodeCount: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Lockable<T> {
|
export interface Lockable<T> {
|
||||||
@@ -224,9 +217,7 @@ const defaultValues: Settings = {
|
|||||||
jellyseerrServerUrl: undefined,
|
jellyseerrServerUrl: undefined,
|
||||||
hiddenLibraries: [],
|
hiddenLibraries: [],
|
||||||
enableH265ForChromecast: false,
|
enableH265ForChromecast: false,
|
||||||
defaultPlayer: VideoPlayer.VLC_3, // ios-only setting. does not matter what this is for android
|
defaultPlayer: VideoPlayer.VLC_3, // ios only setting. does not matter what this is for android
|
||||||
maxAutoPlayEpisodeCount: { key: "3", value: 3 },
|
|
||||||
autoPlayEpisodeCount: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadSettings = (): Partial<Settings> => {
|
const loadSettings = (): Partial<Settings> => {
|
||||||
@@ -245,11 +236,11 @@ const loadSettings = (): Partial<Settings> => {
|
|||||||
const EXCLUDE_FROM_SAVE = ["home"];
|
const EXCLUDE_FROM_SAVE = ["home"];
|
||||||
|
|
||||||
const saveSettings = (settings: Settings) => {
|
const saveSettings = (settings: Settings) => {
|
||||||
for (const key of Object.keys(settings)) {
|
Object.keys(settings).forEach((key) => {
|
||||||
if (EXCLUDE_FROM_SAVE.includes(key)) {
|
if (EXCLUDE_FROM_SAVE.includes(key)) {
|
||||||
delete settings[key as keyof Settings];
|
delete settings[key as keyof Settings];
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
const jsonValue = JSON.stringify(settings);
|
const jsonValue = JSON.stringify(settings);
|
||||||
storage.set("settings", jsonValue);
|
storage.set("settings", jsonValue);
|
||||||
};
|
};
|
||||||
@@ -280,9 +271,7 @@ export const useSettings = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const refreshStreamyfinPluginSettings = useCallback(async () => {
|
const refreshStreamyfinPluginSettings = useCallback(async () => {
|
||||||
if (!api) {
|
if (!api) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const settings = await api.getStreamyfinPluginConfig().then(
|
const settings = await api.getStreamyfinPluginConfig().then(
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
writeInfoLog("Got plugin settings", data?.settings);
|
writeInfoLog("Got plugin settings", data?.settings);
|
||||||
@@ -295,9 +284,7 @@ export const useSettings = () => {
|
|||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
const updateSettings = (update: Partial<Settings>) => {
|
const updateSettings = (update: Partial<Settings>) => {
|
||||||
if (!_settings) {
|
if (!_settings) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hasChanges = Object.entries(update).some(
|
const hasChanges = Object.entries(update).some(
|
||||||
([key, value]) => _settings[key as keyof Settings] !== value,
|
([key, value]) => _settings[key as keyof Settings] !== value,
|
||||||
);
|
);
|
||||||
@@ -318,31 +305,34 @@ export const useSettings = () => {
|
|||||||
// If admin sets locked to false but provides a value,
|
// If admin sets locked to false but provides a value,
|
||||||
// use user settings first and fallback on admin setting if required.
|
// use user settings first and fallback on admin setting if required.
|
||||||
const settings: Settings = useMemo(() => {
|
const settings: Settings = useMemo(() => {
|
||||||
const unlockedPluginDefaults = {} as Settings;
|
let unlockedPluginDefaults = {} as Settings;
|
||||||
const overrideSettings = Object.entries(pluginSettings ?? {}).reduce<
|
const overrideSettings = Object.entries(pluginSettings || {}).reduce(
|
||||||
Partial<Settings>
|
(acc, [key, setting]) => {
|
||||||
>((acc, [key, setting]) => {
|
if (setting) {
|
||||||
if (setting) {
|
const { value, locked } = setting;
|
||||||
const { value, locked } = setting;
|
|
||||||
const settingsKey = key as keyof Settings;
|
|
||||||
|
|
||||||
// Make sure we override default settings with plugin settings when they are not locked.
|
// Make sure we override default settings with plugin settings when they are not locked.
|
||||||
if (
|
// Admin decided what users defaults should be and grants them the ability to change them too.
|
||||||
!locked &&
|
if (
|
||||||
value !== undefined &&
|
locked === false &&
|
||||||
_settings?.[settingsKey] !== value
|
value &&
|
||||||
) {
|
_settings?.[key as keyof Settings] !== value
|
||||||
Object.assign(unlockedPluginDefaults, {
|
) {
|
||||||
[settingsKey]: value,
|
unlockedPluginDefaults = Object.assign(unlockedPluginDefaults, {
|
||||||
|
[key as keyof Settings]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
acc = Object.assign(acc, {
|
||||||
|
[key]: locked
|
||||||
|
? value
|
||||||
|
: (_settings?.[key as keyof Settings] ?? value),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return acc;
|
||||||
Object.assign(acc, {
|
},
|
||||||
[settingsKey]: locked ? value : (_settings?.[settingsKey] ?? value),
|
{} as Settings,
|
||||||
});
|
);
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import generateDeviceProfile from "@/utils/profiles/native";
|
import native from "@/utils/profiles/native";
|
||||||
import type { Api } from "@jellyfin/sdk";
|
import type { Api } from "@jellyfin/sdk";
|
||||||
import type {
|
import type {
|
||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
@@ -14,27 +14,23 @@ export const getStreamUrl = async ({
|
|||||||
userId,
|
userId,
|
||||||
startTimeTicks = 0,
|
startTimeTicks = 0,
|
||||||
maxStreamingBitrate,
|
maxStreamingBitrate,
|
||||||
playSessionId,
|
sessionData,
|
||||||
deviceProfile = generateDeviceProfile(),
|
deviceProfile = native,
|
||||||
audioStreamIndex = 0,
|
audioStreamIndex = 0,
|
||||||
subtitleStreamIndex = undefined,
|
subtitleStreamIndex = undefined,
|
||||||
mediaSourceId,
|
mediaSourceId,
|
||||||
download = false,
|
|
||||||
deviceId,
|
|
||||||
}: {
|
}: {
|
||||||
api: Api | null | undefined;
|
api: Api | null | undefined;
|
||||||
item: BaseItemDto | null | undefined;
|
item: BaseItemDto | null | undefined;
|
||||||
userId: string | null | undefined;
|
userId: string | null | undefined;
|
||||||
startTimeTicks: number;
|
startTimeTicks: number;
|
||||||
maxStreamingBitrate?: number;
|
maxStreamingBitrate?: number;
|
||||||
playSessionId?: string | null;
|
sessionData?: PlaybackInfoResponse | null;
|
||||||
deviceProfile?: any;
|
deviceProfile?: any;
|
||||||
audioStreamIndex?: number;
|
audioStreamIndex?: number;
|
||||||
subtitleStreamIndex?: number;
|
subtitleStreamIndex?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
mediaSourceId?: string | null;
|
mediaSourceId?: string | null;
|
||||||
download?: bool;
|
|
||||||
deviceId?: string | null;
|
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
url: string | null;
|
url: string | null;
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
@@ -48,84 +44,111 @@ export const getStreamUrl = async ({
|
|||||||
let mediaSource: MediaSourceInfo | undefined;
|
let mediaSource: MediaSourceInfo | undefined;
|
||||||
let sessionId: string | null | undefined;
|
let sessionId: string | null | undefined;
|
||||||
|
|
||||||
const res = await getMediaInfoApi(api).getPlaybackInfo(
|
if (item.Type === "Program") {
|
||||||
|
console.log("Item is of type program...");
|
||||||
|
const res0 = await getMediaInfoApi(api).getPlaybackInfo(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
itemId: item.ChannelId!,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
params: {
|
||||||
|
startTimeTicks: 0,
|
||||||
|
isPlayback: true,
|
||||||
|
autoOpenLiveStream: true,
|
||||||
|
maxStreamingBitrate,
|
||||||
|
audioStreamIndex,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
deviceProfile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const transcodeUrl = res0.data.MediaSources?.[0].TranscodingUrl;
|
||||||
|
sessionId = res0.data.PlaySessionId || null;
|
||||||
|
|
||||||
|
if (transcodeUrl) {
|
||||||
|
return {
|
||||||
|
url: `${api.basePath}${transcodeUrl}`,
|
||||||
|
sessionId,
|
||||||
|
mediaSource: res0.data.MediaSources?.[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemId = item.Id;
|
||||||
|
|
||||||
|
const res2 = await getMediaInfoApi(api).getPlaybackInfo(
|
||||||
{
|
{
|
||||||
itemId: item.Id!,
|
itemId: item.Id!,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: {
|
data: {
|
||||||
userId,
|
|
||||||
deviceProfile,
|
deviceProfile,
|
||||||
subtitleStreamIndex,
|
userId,
|
||||||
startTimeTicks,
|
|
||||||
isPlayback: true,
|
|
||||||
autoOpenLiveStream: true,
|
|
||||||
maxStreamingBitrate,
|
maxStreamingBitrate,
|
||||||
audioStreamIndex,
|
startTimeTicks,
|
||||||
|
autoOpenLiveStream: true,
|
||||||
mediaSourceId,
|
mediaSourceId,
|
||||||
|
audioStreamIndex,
|
||||||
|
subtitleStreamIndex,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res2.status !== 200) {
|
||||||
console.error("Error getting playback info:", res.status, res.statusText);
|
console.error("Error getting playback info:", res2.status, res2.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionId = res.data.PlaySessionId || null;
|
sessionId = res2.data.PlaySessionId || null;
|
||||||
mediaSource = res.data.MediaSources[0];
|
|
||||||
let transcodeUrl = mediaSource.TranscodingUrl;
|
|
||||||
|
|
||||||
if (transcodeUrl) {
|
mediaSource = res2.data.MediaSources?.find(
|
||||||
if (download) {
|
(source: MediaSourceInfo) => source.Id === mediaSourceId,
|
||||||
transcodeUrl = transcodeUrl.replace("master.m3u8", "stream");
|
);
|
||||||
|
|
||||||
|
if (item.MediaType === "Video") {
|
||||||
|
if (mediaSource?.TranscodingUrl) {
|
||||||
|
const urlObj = new URL(api.basePath + mediaSource?.TranscodingUrl); // Create a URL object
|
||||||
|
|
||||||
|
// Get the updated URL
|
||||||
|
const transcodeUrl = urlObj.toString();
|
||||||
|
|
||||||
|
console.log("Video has transcoding URL:", `${transcodeUrl}`);
|
||||||
|
return {
|
||||||
|
url: transcodeUrl,
|
||||||
|
sessionId: sessionId,
|
||||||
|
mediaSource,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
console.log("Video is being transcoded:", transcodeUrl);
|
const searchParams = new URLSearchParams({
|
||||||
|
playSessionId: sessionData?.PlaySessionId || "",
|
||||||
|
mediaSourceId: mediaSource?.Id || "",
|
||||||
|
static: "true",
|
||||||
|
subtitleStreamIndex: subtitleStreamIndex?.toString() || "",
|
||||||
|
audioStreamIndex: audioStreamIndex?.toString() || "",
|
||||||
|
deviceId: api.deviceInfo.id,
|
||||||
|
api_key: api.accessToken,
|
||||||
|
startTimeTicks: startTimeTicks.toString(),
|
||||||
|
maxStreamingBitrate: maxStreamingBitrate?.toString() || "",
|
||||||
|
userId: userId || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const directPlayUrl = `${
|
||||||
|
api.basePath
|
||||||
|
}/Videos/${itemId}/stream.mp4?${searchParams.toString()}`;
|
||||||
|
|
||||||
|
console.log("Video is being direct played:", directPlayUrl);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: `${api.basePath}${transcodeUrl}`,
|
url: directPlayUrl,
|
||||||
sessionId,
|
sessionId: sessionId,
|
||||||
mediaSource,
|
mediaSource,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let downloadParams = {};
|
Alert.alert("Error", "Could not play this item");
|
||||||
|
|
||||||
if (download) {
|
return null;
|
||||||
// We need to disable static so we can have a remux with subtitle.
|
|
||||||
downloadParams = {
|
|
||||||
subtitleMethod: "Embed",
|
|
||||||
enableSubtitlesInManifest: true,
|
|
||||||
static: "false",
|
|
||||||
allowVideoStreamCopy: true,
|
|
||||||
allowAudioStreamCopy: true,
|
|
||||||
playSessionId: sessionId || "",
|
|
||||||
container: "ts",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const streamParams = new URLSearchParams({
|
|
||||||
static: "true",
|
|
||||||
container: "mp4",
|
|
||||||
mediaSourceId: mediaSource?.Id || "",
|
|
||||||
subtitleStreamIndex: subtitleStreamIndex?.toString() || "",
|
|
||||||
audioStreamIndex: audioStreamIndex?.toString() || "",
|
|
||||||
deviceId: deviceId || api.deviceInfo.id,
|
|
||||||
api_key: api.accessToken,
|
|
||||||
startTimeTicks: startTimeTicks.toString(),
|
|
||||||
maxStreamingBitrate: maxStreamingBitrate?.toString() || "",
|
|
||||||
userId: userId || "",
|
|
||||||
...downloadParams,
|
|
||||||
});
|
|
||||||
|
|
||||||
const directPlayUrl = `${
|
|
||||||
api.basePath
|
|
||||||
}/Videos/${item.Id}/stream?${streamParams.toString()}`;
|
|
||||||
|
|
||||||
console.log("Video is being direct played:", directPlayUrl);
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: directPlayUrl,
|
|
||||||
sessionId: sessionId || playSessionId,
|
|
||||||
mediaSource,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { getOrSetDeviceId } from "@/providers/JellyfinProvider";
|
import { getOrSetDeviceId } from "@/providers/JellyfinProvider";
|
||||||
import type { Settings } from "@/utils/atoms/settings";
|
import type { Settings } from "@/utils/atoms/settings";
|
||||||
|
import ios from "@/utils/profiles/ios";
|
||||||
|
import native from "@/utils/profiles/native";
|
||||||
import old from "@/utils/profiles/old";
|
import old from "@/utils/profiles/old";
|
||||||
import type { Api } from "@jellyfin/sdk";
|
import type { Api } from "@jellyfin/sdk";
|
||||||
import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client";
|
import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Settings } from "@/utils/atoms/settings";
|
import type { Settings } from "@/utils/atoms/settings";
|
||||||
import generateDeviceProfile from "@/utils/profiles/native";
|
import native from "@/utils/profiles/native";
|
||||||
import type { Api } from "@jellyfin/sdk";
|
import type { Api } from "@jellyfin/sdk";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { getAuthHeaders } from "../jellyfin";
|
import { getAuthHeaders } from "../jellyfin";
|
||||||
@@ -43,7 +43,7 @@ export const postCapabilities = async ({
|
|||||||
],
|
],
|
||||||
supportsMediaControl: true,
|
supportsMediaControl: true,
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
DeviceProfile: generateDeviceProfile(),
|
DeviceProfile: native,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: getAuthHeaders(api),
|
headers: getAuthHeaders(api),
|
||||||
|
|||||||
143
utils/profiles/android.js
Normal file
143
utils/profiles/android.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
import MediaTypes from "../../constants/MediaTypes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device profile for Native video player
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
Name: "1. Native iOS Video Profile",
|
||||||
|
MaxStaticBitrate: 100000000,
|
||||||
|
MaxStreamingBitrate: 120000000,
|
||||||
|
MusicStreamingTranscodingBitrate: 384000,
|
||||||
|
CodecProfiles: [
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
Codec: "h264,h265,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
Codec: "aac,mp3,flac,alac,opus,vorbis,pcm,wma",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
DirectPlayProfiles: [
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp",
|
||||||
|
VideoCodec: "h264,h265,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
|
||||||
|
AudioCodec: "aac,mp3,flac,alac,opus,vorbis,wma",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
Container: "mp3,aac,flac,alac,wav,ogg,wma",
|
||||||
|
AudioCodec: "mp3,aac,flac,alac,opus,vorbis,wma,pcm",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
TranscodingProfiles: [
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
Context: "Streaming",
|
||||||
|
Protocol: "hls",
|
||||||
|
Container: "ts",
|
||||||
|
VideoCodec: "h264",
|
||||||
|
AudioCodec: "aac,mp3,ac3",
|
||||||
|
MaxAudioChannels: "8",
|
||||||
|
MinSegments: "2",
|
||||||
|
BreakOnNonKeyFrames: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
Context: "Streaming",
|
||||||
|
Protocol: "http",
|
||||||
|
Container: "mp3",
|
||||||
|
AudioCodec: "mp3",
|
||||||
|
MaxAudioChannels: "2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ResponseProfiles: [
|
||||||
|
{
|
||||||
|
Container: "mkv",
|
||||||
|
MimeType: "video/x-matroska",
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "mp4",
|
||||||
|
MimeType: "video/mp4",
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SubtitleProfiles: [
|
||||||
|
{ Format: "srt", Method: "Embed" },
|
||||||
|
{ Format: "srt", Method: "External" },
|
||||||
|
{ Format: "srt", Method: "Encode" },
|
||||||
|
{ Format: "ass", Method: "Embed" },
|
||||||
|
{ Format: "ass", Method: "External" },
|
||||||
|
{ Format: "ass", Method: "Encode" },
|
||||||
|
{ Format: "ssa", Method: "Embed" },
|
||||||
|
{ Format: "ssa", Method: "External" },
|
||||||
|
{ Format: "ssa", Method: "Encode" },
|
||||||
|
{ Format: "sub", Method: "Embed" },
|
||||||
|
{ Format: "sub", Method: "External" },
|
||||||
|
{ Format: "sub", Method: "Encode" },
|
||||||
|
{ Format: "vtt", Method: "Embed" },
|
||||||
|
{ Format: "vtt", Method: "External" },
|
||||||
|
{ Format: "vtt", Method: "Encode" },
|
||||||
|
{ Format: "ttml", Method: "Embed" },
|
||||||
|
{ Format: "ttml", Method: "External" },
|
||||||
|
{ Format: "ttml", Method: "Encode" },
|
||||||
|
{ Format: "pgs", Method: "Embed" },
|
||||||
|
{ Format: "pgs", Method: "External" },
|
||||||
|
{ Format: "pgs", Method: "Encode" },
|
||||||
|
{ Format: "dvdsub", Method: "Embed" },
|
||||||
|
{ Format: "dvdsub", Method: "External" },
|
||||||
|
{ Format: "dvdsub", Method: "Encode" },
|
||||||
|
{ Format: "dvbsub", Method: "Embed" },
|
||||||
|
{ Format: "dvbsub", Method: "External" },
|
||||||
|
{ Format: "dvbsub", Method: "Encode" },
|
||||||
|
{ Format: "xsub", Method: "Embed" },
|
||||||
|
{ Format: "xsub", Method: "External" },
|
||||||
|
{ Format: "xsub", Method: "Encode" },
|
||||||
|
{ Format: "mov_text", Method: "Embed" },
|
||||||
|
{ Format: "mov_text", Method: "External" },
|
||||||
|
{ Format: "mov_text", Method: "Encode" },
|
||||||
|
{ Format: "scc", Method: "Embed" },
|
||||||
|
{ Format: "scc", Method: "External" },
|
||||||
|
{ Format: "scc", Method: "Encode" },
|
||||||
|
{ Format: "smi", Method: "Embed" },
|
||||||
|
{ Format: "smi", Method: "External" },
|
||||||
|
{ Format: "smi", Method: "Encode" },
|
||||||
|
{ Format: "teletext", Method: "Embed" },
|
||||||
|
{ Format: "teletext", Method: "External" },
|
||||||
|
{ Format: "teletext", Method: "Encode" },
|
||||||
|
{ Format: "microdvd", Method: "Embed" },
|
||||||
|
{ Format: "microdvd", Method: "External" },
|
||||||
|
{ Format: "microdvd", Method: "Encode" },
|
||||||
|
{ Format: "mpl2", Method: "Embed" },
|
||||||
|
{ Format: "mpl2", Method: "External" },
|
||||||
|
{ Format: "mpl2", Method: "Encode" },
|
||||||
|
{ Format: "pjs", Method: "Embed" },
|
||||||
|
{ Format: "pjs", Method: "External" },
|
||||||
|
{ Format: "pjs", Method: "Encode" },
|
||||||
|
{ Format: "realtext", Method: "Embed" },
|
||||||
|
{ Format: "realtext", Method: "External" },
|
||||||
|
{ Format: "realtext", Method: "Encode" },
|
||||||
|
{ Format: "stl", Method: "Embed" },
|
||||||
|
{ Format: "stl", Method: "External" },
|
||||||
|
{ Format: "stl", Method: "Encode" },
|
||||||
|
{ Format: "subrip", Method: "Embed" },
|
||||||
|
{ Format: "subrip", Method: "External" },
|
||||||
|
{ Format: "subrip", Method: "Encode" },
|
||||||
|
{ Format: "subviewer", Method: "Embed" },
|
||||||
|
{ Format: "subviewer", Method: "External" },
|
||||||
|
{ Format: "subviewer", Method: "Encode" },
|
||||||
|
{ Format: "text", Method: "Embed" },
|
||||||
|
{ Format: "text", Method: "External" },
|
||||||
|
{ Format: "text", Method: "Encode" },
|
||||||
|
{ Format: "vplayer", Method: "Embed" },
|
||||||
|
{ Format: "vplayer", Method: "External" },
|
||||||
|
{ Format: "vplayer", Method: "Encode" },
|
||||||
|
],
|
||||||
|
};
|
||||||
86
utils/profiles/base.js
Normal file
86
utils/profiles/base.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import MediaTypes from "../../constants/MediaTypes";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Name: "Expo Base Video Profile",
|
||||||
|
MaxStaticBitrate: 100000000,
|
||||||
|
MaxStreamingBitrate: 120000000,
|
||||||
|
MusicStreamingTranscodingBitrate: 384000,
|
||||||
|
CodecProfiles: [
|
||||||
|
{
|
||||||
|
Codec: "h264",
|
||||||
|
Conditions: [
|
||||||
|
{
|
||||||
|
Condition: "NotEquals",
|
||||||
|
IsRequired: false,
|
||||||
|
Property: "IsAnamorphic",
|
||||||
|
Value: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "EqualsAny",
|
||||||
|
IsRequired: false,
|
||||||
|
Property: "VideoProfile",
|
||||||
|
Value: "high|main|baseline|constrained baseline",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "LessThanEqual",
|
||||||
|
IsRequired: false,
|
||||||
|
Property: "VideoLevel",
|
||||||
|
Value: "51",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "NotEquals",
|
||||||
|
IsRequired: false,
|
||||||
|
Property: "IsInterlaced",
|
||||||
|
Value: "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Codec: "hevc",
|
||||||
|
Conditions: [
|
||||||
|
{
|
||||||
|
Condition: "NotEquals",
|
||||||
|
IsRequired: false,
|
||||||
|
Property: "IsAnamorphic",
|
||||||
|
Value: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "EqualsAny",
|
||||||
|
IsRequired: false,
|
||||||
|
Property: "VideoProfile",
|
||||||
|
Value: "main|main 10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "LessThanEqual",
|
||||||
|
IsRequired: false,
|
||||||
|
Property: "VideoLevel",
|
||||||
|
Value: "183",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "NotEquals",
|
||||||
|
IsRequired: false,
|
||||||
|
Property: "IsInterlaced",
|
||||||
|
Value: "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ContainerProfiles: [],
|
||||||
|
DirectPlayProfiles: [],
|
||||||
|
ResponseProfiles: [
|
||||||
|
{
|
||||||
|
Container: "m4v",
|
||||||
|
MimeType: "video/mp4",
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SubtitleProfiles: [
|
||||||
|
{
|
||||||
|
Format: "vtt",
|
||||||
|
Method: "Hls",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
TranscodingProfiles: [],
|
||||||
|
};
|
||||||
149
utils/profiles/ios.js
Normal file
149
utils/profiles/ios.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
import MediaTypes from "../../constants/MediaTypes";
|
||||||
|
|
||||||
|
import BaseProfile from "./base";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device profile for Expo Video player on iOS 13+
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
...BaseProfile,
|
||||||
|
Name: "Expo iOS Video Profile",
|
||||||
|
DirectPlayProfiles: [
|
||||||
|
{
|
||||||
|
AudioCodec: "aac,mp3,ac3,eac3,flac,alac",
|
||||||
|
Container: "mp4,m4v",
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
VideoCodec: "hevc,h264",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "aac,mp3,ac3,eac3,flac,alac",
|
||||||
|
Container: "mov",
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
VideoCodec: "hevc,h264",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "mp3",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "aac",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "aac",
|
||||||
|
Container: "m4a",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "aac",
|
||||||
|
Container: "m4b",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "flac",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "alac",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "alac",
|
||||||
|
Container: "m4a",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "alac",
|
||||||
|
Container: "m4b",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Container: "wav",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
TranscodingProfiles: [
|
||||||
|
{
|
||||||
|
AudioCodec: "aac",
|
||||||
|
BreakOnNonKeyFrames: true,
|
||||||
|
Container: "aac",
|
||||||
|
Context: "Streaming",
|
||||||
|
MaxAudioChannels: "6",
|
||||||
|
MinSegments: "2",
|
||||||
|
Protocol: "hls",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "aac",
|
||||||
|
Container: "aac",
|
||||||
|
Context: "Streaming",
|
||||||
|
MaxAudioChannels: "6",
|
||||||
|
Protocol: "http",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "mp3",
|
||||||
|
Container: "mp3",
|
||||||
|
Context: "Streaming",
|
||||||
|
MaxAudioChannels: "6",
|
||||||
|
Protocol: "http",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "wav",
|
||||||
|
Container: "wav",
|
||||||
|
Context: "Streaming",
|
||||||
|
MaxAudioChannels: "6",
|
||||||
|
Protocol: "http",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "mp3",
|
||||||
|
Container: "mp3",
|
||||||
|
Context: "Static",
|
||||||
|
MaxAudioChannels: "6",
|
||||||
|
Protocol: "http",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "aac",
|
||||||
|
Container: "aac",
|
||||||
|
Context: "Static",
|
||||||
|
MaxAudioChannels: "6",
|
||||||
|
Protocol: "http",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "wav",
|
||||||
|
Container: "wav",
|
||||||
|
Context: "Static",
|
||||||
|
MaxAudioChannels: "6",
|
||||||
|
Protocol: "http",
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "aac,mp3",
|
||||||
|
BreakOnNonKeyFrames: true,
|
||||||
|
Container: "ts",
|
||||||
|
Context: "Streaming",
|
||||||
|
MaxAudioChannels: "6",
|
||||||
|
MinSegments: "2",
|
||||||
|
Protocol: "hls",
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
VideoCodec: "h264",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AudioCodec: "aac,mp3,ac3,eac3,flac,alac",
|
||||||
|
Container: "mp4",
|
||||||
|
Context: "Static",
|
||||||
|
Protocol: "http",
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
VideoCodec: "h264",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Platform } from "react-native";
|
|
||||||
import DeviceInfo from "react-native-device-info";
|
|
||||||
/**
|
/**
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
@@ -7,190 +5,132 @@ import DeviceInfo from "react-native-device-info";
|
|||||||
*/
|
*/
|
||||||
import MediaTypes from "../../constants/MediaTypes";
|
import MediaTypes from "../../constants/MediaTypes";
|
||||||
|
|
||||||
// Helper function to detect Dolby Vision support
|
/**
|
||||||
const supportsDolbyVision = async () => {
|
* Device profile for Native video player
|
||||||
if (Platform.OS === "ios") {
|
*/
|
||||||
const deviceModel = await DeviceInfo.getModel();
|
export default {
|
||||||
// iPhone 12 and newer generally support Dolby Vision
|
Name: "1. Vlc Player",
|
||||||
const modelNumber = Number.parseInt(deviceModel.replace(/iPhone/, ""), 10);
|
MaxStaticBitrate: 999_999_999,
|
||||||
return !Number.isNaN(modelNumber) && modelNumber >= 12;
|
MaxStreamingBitrate: 999_999_999,
|
||||||
}
|
CodecProfiles: [
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
Codec: "h264,h265,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
Codec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,pcm,wma",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
DirectPlayProfiles: [
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
|
||||||
|
VideoCodec:
|
||||||
|
"h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video",
|
||||||
|
AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma,dts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
Container: "mp3,aac,flac,alac,wav,ogg,wma",
|
||||||
|
AudioCodec:
|
||||||
|
"mp3,aac,flac,alac,opus,vorbis,wma,pcm,mpa,wav,ogg,oga,webma,ape",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
TranscodingProfiles: [
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Video,
|
||||||
|
Context: "Streaming",
|
||||||
|
Protocol: "hls",
|
||||||
|
Container: "fmp4",
|
||||||
|
VideoCodec: "h264, hevc",
|
||||||
|
AudioCodec: "aac,mp3,ac3,dts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: MediaTypes.Audio,
|
||||||
|
Context: "Streaming",
|
||||||
|
Protocol: "http",
|
||||||
|
Container: "mp3",
|
||||||
|
AudioCodec: "mp3",
|
||||||
|
MaxAudioChannels: "2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SubtitleProfiles: [
|
||||||
|
// Official formats
|
||||||
|
{ Format: "vtt", Method: "Embed" },
|
||||||
|
{ Format: "vtt", Method: "External" },
|
||||||
|
|
||||||
if (Platform.OS === "android") {
|
{ Format: "webvtt", Method: "Embed" },
|
||||||
const apiLevel = await DeviceInfo.getApiLevel();
|
{ Format: "webvtt", Method: "External" },
|
||||||
const isHighEndDevice =
|
|
||||||
(await DeviceInfo.getTotalMemory()) > 4 * 1024 * 1024 * 1024; // >4GB RAM
|
|
||||||
// Very rough approximation - Android 10+ on higher-end devices may support it
|
|
||||||
return apiLevel >= 29 && isHighEndDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
{ Format: "srt", Method: "Embed" },
|
||||||
};
|
{ Format: "srt", Method: "External" },
|
||||||
|
|
||||||
export const generateDeviceProfile = async () => {
|
{ Format: "subrip", Method: "Embed" },
|
||||||
const dolbyVisionSupported = await supportsDolbyVision();
|
{ Format: "subrip", Method: "External" },
|
||||||
/**
|
|
||||||
* Device profile for Native video player
|
{ Format: "ttml", Method: "Embed" },
|
||||||
*/
|
{ Format: "ttml", Method: "External" },
|
||||||
const profile = {
|
|
||||||
Name: "1. Vlc Player",
|
{ Format: "dvbsub", Method: "Embed" },
|
||||||
MaxStaticBitrate: 999_999_999,
|
{ Format: "dvdsub", Method: "Encode" },
|
||||||
MaxStreamingBitrate: 999_999_999,
|
|
||||||
CodecProfiles: [
|
{ Format: "ass", Method: "Embed" },
|
||||||
{
|
{ Format: "ass", Method: "External" },
|
||||||
Type: MediaTypes.Video,
|
|
||||||
Codec: "h264,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
|
{ Format: "idx", Method: "Embed" },
|
||||||
},
|
{ Format: "idx", Method: "Encode" },
|
||||||
{
|
|
||||||
Type: MediaTypes.Video,
|
{ Format: "pgs", Method: "Embed" },
|
||||||
Codec: "hevc,h265",
|
{ Format: "pgs", Method: "Encode" },
|
||||||
Conditions: [
|
|
||||||
{
|
{ Format: "pgssub", Method: "Embed" },
|
||||||
Condition: "LessThanEqual",
|
{ Format: "pgssub", Method: "Encode" },
|
||||||
Property: "VideoLevel",
|
|
||||||
Value: "153",
|
{ Format: "ssa", Method: "Embed" },
|
||||||
IsRequired: false,
|
{ Format: "ssa", Method: "External" },
|
||||||
},
|
|
||||||
// We'll add Dolby Vision condition below if not supported
|
// Other formats
|
||||||
],
|
{ Format: "microdvd", Method: "Embed" },
|
||||||
},
|
{ Format: "microdvd", Method: "External" },
|
||||||
{
|
|
||||||
Type: MediaTypes.Audio,
|
{ Format: "mov_text", Method: "Embed" },
|
||||||
Codec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,pcm,wma",
|
{ Format: "mov_text", Method: "External" },
|
||||||
},
|
|
||||||
],
|
{ Format: "mpl2", Method: "Embed" },
|
||||||
DirectPlayProfiles: [
|
{ Format: "mpl2", Method: "External" },
|
||||||
{
|
|
||||||
Type: MediaTypes.Video,
|
{ Format: "pjs", Method: "Embed" },
|
||||||
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
|
{ Format: "pjs", Method: "External" },
|
||||||
VideoCodec:
|
|
||||||
"h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video",
|
{ Format: "realtext", Method: "Embed" },
|
||||||
AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma,dts",
|
{ Format: "realtext", Method: "External" },
|
||||||
},
|
|
||||||
{
|
{ Format: "scc", Method: "Embed" },
|
||||||
Type: MediaTypes.Audio,
|
{ Format: "scc", Method: "External" },
|
||||||
Container: "mp3,aac,flac,alac,wav,ogg,wma",
|
|
||||||
AudioCodec:
|
{ Format: "smi", Method: "Embed" },
|
||||||
"mp3,aac,flac,alac,opus,vorbis,wma,pcm,mpa,wav,ogg,oga,webma,ape",
|
{ Format: "smi", Method: "External" },
|
||||||
},
|
|
||||||
],
|
{ Format: "stl", Method: "Embed" },
|
||||||
TranscodingProfiles: [
|
{ Format: "stl", Method: "External" },
|
||||||
{
|
|
||||||
Type: MediaTypes.Video,
|
{ Format: "sub", Method: "Embed" },
|
||||||
Context: "Streaming",
|
{ Format: "sub", Method: "External" },
|
||||||
Protocol: "hls",
|
|
||||||
Container: "mp4",
|
{ Format: "subviewer", Method: "Embed" },
|
||||||
VideoCodec: "h264, hevc",
|
{ Format: "subviewer", Method: "External" },
|
||||||
AudioCodec: "aac,mp3,ac3,dts",
|
|
||||||
},
|
{ Format: "teletext", Method: "Embed" },
|
||||||
{
|
{ Format: "teletext", Method: "Encode" },
|
||||||
Type: MediaTypes.Audio,
|
|
||||||
Context: "Streaming",
|
{ Format: "text", Method: "Embed" },
|
||||||
Protocol: "http",
|
{ Format: "text", Method: "External" },
|
||||||
Container: "mp3",
|
|
||||||
AudioCodec: "mp3",
|
{ Format: "vplayer", Method: "Embed" },
|
||||||
MaxAudioChannels: "2",
|
{ Format: "vplayer", Method: "External" },
|
||||||
},
|
|
||||||
],
|
{ Format: "xsub", Method: "Embed" },
|
||||||
SubtitleProfiles: [
|
{ Format: "xsub", Method: "External" },
|
||||||
// Official formats
|
],
|
||||||
{ Format: "vtt", Method: "Embed" },
|
|
||||||
{ Format: "vtt", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "webvtt", Method: "Embed" },
|
|
||||||
{ Format: "webvtt", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "srt", Method: "Embed" },
|
|
||||||
{ Format: "srt", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "subrip", Method: "Embed" },
|
|
||||||
{ Format: "subrip", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "ttml", Method: "Embed" },
|
|
||||||
{ Format: "ttml", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "dvbsub", Method: "Embed" },
|
|
||||||
{ Format: "dvdsub", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "ass", Method: "Embed" },
|
|
||||||
{ Format: "ass", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "idx", Method: "Embed" },
|
|
||||||
{ Format: "idx", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "pgs", Method: "Embed" },
|
|
||||||
{ Format: "pgs", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "pgssub", Method: "Embed" },
|
|
||||||
{ Format: "pgssub", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "ssa", Method: "Embed" },
|
|
||||||
{ Format: "ssa", Method: "External" },
|
|
||||||
|
|
||||||
// Other formats
|
|
||||||
{ Format: "microdvd", Method: "Embed" },
|
|
||||||
{ Format: "microdvd", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "mov_text", Method: "Embed" },
|
|
||||||
{ Format: "mov_text", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "mpl2", Method: "Embed" },
|
|
||||||
{ Format: "mpl2", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "pjs", Method: "Embed" },
|
|
||||||
{ Format: "pjs", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "realtext", Method: "Embed" },
|
|
||||||
{ Format: "realtext", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "scc", Method: "Embed" },
|
|
||||||
{ Format: "scc", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "smi", Method: "Embed" },
|
|
||||||
{ Format: "smi", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "stl", Method: "Embed" },
|
|
||||||
{ Format: "stl", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "sub", Method: "Embed" },
|
|
||||||
{ Format: "sub", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "subviewer", Method: "Embed" },
|
|
||||||
{ Format: "subviewer", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "teletext", Method: "Embed" },
|
|
||||||
{ Format: "teletext", Method: "Encode" },
|
|
||||||
|
|
||||||
{ Format: "text", Method: "Embed" },
|
|
||||||
{ Format: "text", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "vplayer", Method: "Embed" },
|
|
||||||
{ Format: "vplayer", Method: "External" },
|
|
||||||
|
|
||||||
{ Format: "xsub", Method: "Embed" },
|
|
||||||
{ Format: "xsub", Method: "External" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add Dolby Vision restriction if not supported
|
|
||||||
if (!dolbyVisionSupported) {
|
|
||||||
const hevcProfile = profile.CodecProfiles.find(
|
|
||||||
(p) => p.Type === MediaTypes.Video && p.Codec.includes("hevc"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hevcProfile) {
|
|
||||||
hevcProfile.Conditions.push({
|
|
||||||
Condition: "NotEquals",
|
|
||||||
Property: "VideoRangeType",
|
|
||||||
Value: "DOVI", //no dolby vision at all
|
|
||||||
IsRequired: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return profile;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async () => {
|
|
||||||
return await generateDeviceProfile();
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user