Compare commits

..

3 Commits

Author SHA1 Message Date
Alex Kim
ccdd7770c9 More fixes 2026-02-19 18:50:36 +11:00
Alex Kim
24cb679c0b Fix some formatting 2026-02-19 18:39:00 +11:00
Alex Kim
af50b023ef Sync subtitle and audio indexes between server and offline 2026-02-19 18:23:45 +11:00
36 changed files with 3036 additions and 2690 deletions

View File

@@ -26,7 +26,7 @@ jobs:
steps: steps:
- name: 🔍 Get PR and Artifacts - name: 🔍 Get PR and Artifacts
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with: with:
script: | script: |
// Check if we're running from a fork (more precise detection) // Check if we're running from a fork (more precise detection)
@@ -188,17 +188,6 @@ jobs:
if (latestAppsRun) { if (latestAppsRun) {
console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`); console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`);
// Map job names to our build targets. Declared outside the try so
// the catch fallback can reuse the same keys.
const jobMappings = {
'Android Phone': ['🤖 Build Android APK (Phone)', 'build-android-phone'],
'Android TV': ['🤖 Build Android APK (TV)', 'build-android-tv'],
'iOS': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone'],
'iOS Unsigned': ['🍎 Build iOS IPA (Phone - Unsigned)', 'build-ios-phone-unsigned'],
'tvOS': ['🍎 Build tvOS IPA', 'build-ios-tv'],
'tvOS Unsigned': ['🍎 Build tvOS IPA (Unsigned)', 'build-ios-tv-unsigned']
};
try { try {
// Get all jobs for this workflow run // Get all jobs for this workflow run
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
@@ -227,6 +216,13 @@ jobs:
return; // Exit early return; // Exit early
} }
// Map job names to our build targets
const jobMappings = {
'Android Phone': ['🤖 Build Android APK (Phone)', 'build-android-phone'],
'Android TV': ['🤖 Build Android APK (TV)', 'build-android-tv'],
'iOS Phone': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone']
};
// Create individual status for each job // Create individual status for each job
for (const [platform, jobNames] of Object.entries(jobMappings)) { for (const [platform, jobNames] of Object.entries(jobMappings)) {
const job = jobs.jobs.find(j => const job = jobs.jobs.find(j =>
@@ -240,9 +236,7 @@ jobs:
conclusion: job.conclusion, conclusion: job.conclusion,
url: job.html_url, url: job.html_url,
runId: latestAppsRun.id, runId: latestAppsRun.id,
created_at: job.started_at || latestAppsRun.created_at, created_at: job.started_at || latestAppsRun.created_at
started_at: job.started_at,
completed_at: job.completed_at
}; };
console.log(`Mapped ${platform} to job: ${job.name} (${job.status}/${job.conclusion || 'none'})`); console.log(`Mapped ${platform} to job: ${job.name} (${job.status}/${job.conclusion || 'none'})`);
} else { } else {
@@ -253,30 +247,22 @@ jobs:
conclusion: latestAppsRun.conclusion, conclusion: latestAppsRun.conclusion,
url: latestAppsRun.html_url, url: latestAppsRun.html_url,
runId: latestAppsRun.id, runId: latestAppsRun.id,
created_at: latestAppsRun.created_at, created_at: latestAppsRun.created_at
started_at: latestAppsRun.run_started_at,
completed_at: latestAppsRun.updated_at
}; };
} }
} }
} catch (error) { } catch (error) {
console.log(`Failed to get jobs for run ${latestAppsRun.id}:`, error.message); console.log(`Failed to get jobs for run ${latestAppsRun.id}:`, error.message);
// Fallback to workflow-level status for every build target. // Fallback to workflow-level status
// Keys must match jobMappings / buildTargets statusKey values. buildStatuses['Android Phone'] = buildStatuses['Android TV'] = buildStatuses['iOS Phone'] = {
const fallbackStatus = {
name: latestAppsRun.name, name: latestAppsRun.name,
status: latestAppsRun.status, status: latestAppsRun.status,
conclusion: latestAppsRun.conclusion, conclusion: latestAppsRun.conclusion,
url: latestAppsRun.html_url, url: latestAppsRun.html_url,
runId: latestAppsRun.id, runId: latestAppsRun.id,
created_at: latestAppsRun.created_at, created_at: latestAppsRun.created_at
started_at: latestAppsRun.run_started_at,
completed_at: latestAppsRun.updated_at
}; };
for (const platform of Object.keys(jobMappings)) {
buildStatuses[platform] = fallbackStatus;
}
} }
// Collect artifacts if any job has completed successfully // Collect artifacts if any job has completed successfully
@@ -367,12 +353,10 @@ jobs:
// Process each expected build target individually // Process each expected build target individually
const buildTargets = [ const buildTargets = [
{ name: 'Android Phone', platform: '🤖', device: '📱 Phone', statusKey: 'Android Phone', artifactPattern: /android.*phone/i }, { name: 'Android Phone', platform: '🤖', device: '📱', statusKey: 'Android Phone', artifactPattern: /android.*phone/i },
{ name: 'Android TV', platform: '🤖', device: '📺 TV', statusKey: 'Android TV', artifactPattern: /android.*tv/i }, { name: 'Android TV', platform: '🤖', device: '📺', statusKey: 'Android TV', artifactPattern: /android.*tv/i },
{ name: 'iOS', platform: '🍎', device: '📱 Phone', statusKey: 'iOS', artifactPattern: /ios.*phone.*ipa(?!.*unsigned)/i }, { name: 'iOS Phone', platform: '🍎', device: '📱', statusKey: 'iOS Phone', artifactPattern: /ios.*phone/i },
{ name: 'iOS Unsigned', platform: '🍎', device: '📱 Phone Unsigned', statusKey: 'iOS Unsigned', artifactPattern: /ios.*phone.*unsigned/i }, { name: 'iOS TV', platform: '🍎', device: '📺', statusKey: 'iOS TV', artifactPattern: /ios.*tv/i }
{ name: 'tvOS', platform: '🍎', device: '📺 TV', statusKey: 'tvOS', artifactPattern: /ios.*tv.*ipa(?!.*unsigned)/i },
{ name: 'tvOS Unsigned', platform: '🍎', device: '📺 TV Unsigned', statusKey: 'tvOS Unsigned', artifactPattern: /ios.*tv.*unsigned/i }
]; ];
for (const target of buildTargets) { for (const target of buildTargets) {
@@ -387,31 +371,16 @@ jobs:
let status = '⏳ Pending'; let status = '⏳ Pending';
let downloadLink = '*Waiting for build...*'; let downloadLink = '*Waiting for build...*';
// tvOS builds are temporarily disabled until feat/tv-interface // Special case for iOS TV - show as disabled
// is merged - show them as disabled instead of stuck pending. if (target.name === 'iOS TV') {
if (target.name === 'tvOS' || target.name === 'tvOS Unsigned') {
status = '💤 Disabled'; status = '💤 Disabled';
downloadLink = '*Disabled until feat/tv-interface is merged*'; downloadLink = '*Disabled for now*';
} else if (matchingStatus) { } else if (matchingStatus) {
if (matchingStatus.conclusion === 'success' && matchingArtifact) { if (matchingStatus.conclusion === 'success' && matchingArtifact) {
status = '✅ Complete'; status = '✅ Complete';
const directLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/artifacts/${matchingArtifact.id}`; const directLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/artifacts/${matchingArtifact.id}`;
const fileType = target.name.includes('Android') ? 'APK' : 'IPA'; const fileType = target.name.includes('Android') ? 'APK' : 'IPA';
downloadLink = `[📥 Download ${fileType}](${directLink})`;
// Format file size
const sizeInMB = (matchingArtifact.size_in_bytes / (1024 * 1024)).toFixed(1);
const sizeInfo = `(${sizeInMB} MB)`;
// Calculate build duration
let durationInfo = '';
if (matchingStatus.started_at && matchingStatus.completed_at) {
const durationMs = new Date(matchingStatus.completed_at) - new Date(matchingStatus.started_at);
const durationMin = Math.floor(durationMs / 60000);
const durationSec = Math.floor((durationMs % 60000) / 1000);
durationInfo = ` - ${durationMin}m ${durationSec}s`;
}
downloadLink = `[📥 Download ${fileType}](${directLink}) ${sizeInfo}${durationInfo}`;
} else if (matchingStatus.conclusion === 'failure') { } else if (matchingStatus.conclusion === 'failure') {
status = `❌ [Failed](${matchingStatus.url})`; status = `❌ [Failed](${matchingStatus.url})`;
downloadLink = '*Build failed*'; downloadLink = '*Build failed*';
@@ -439,7 +408,7 @@ jobs:
} }
} }
commentBody += `| ${target.platform} ${target.name} | ${target.device} | ${status} | ${downloadLink} |\n`; commentBody += `| ${target.platform} ${target.name.split(' ')[0]} | ${target.device} ${target.name.split(' ')[1]} | ${status} | ${downloadLink} |\n`;
} }
commentBody += `\n`; commentBody += `\n`;

View File

@@ -41,12 +41,12 @@ jobs:
show-progress: false show-progress: false
- name: 🍞 Setup Bun - name: 🍞 Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
with: with:
bun-version: latest bun-version: latest
- name: 💾 Cache Bun dependencies - name: 💾 Cache Bun dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.bun/install/cache path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }} key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
@@ -60,7 +60,7 @@ jobs:
bun run submodule-reload bun run submodule-reload
- name: 💾 Cache Gradle global - name: 💾 Cache Gradle global
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches
@@ -73,7 +73,7 @@ jobs:
run: bun run prebuild run: bun run prebuild
- name: 💾 Cache project Gradle (.gradle) - name: 💾 Cache project Gradle (.gradle)
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: android/.gradle path: android/.gradle
key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}
@@ -88,7 +88,7 @@ jobs:
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
- name: 📤 Upload APK artifact - name: 📤 Upload APK artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: streamyfin-android-phone-apk-${{ env.DATE_TAG }} name: streamyfin-android-phone-apk-${{ env.DATE_TAG }}
path: | path: |
@@ -124,12 +124,12 @@ jobs:
show-progress: false show-progress: false
- name: 🍞 Setup Bun - name: 🍞 Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
with: with:
bun-version: latest bun-version: latest
- name: 💾 Cache Bun dependencies - name: 💾 Cache Bun dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.bun/install/cache path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }} key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
@@ -143,7 +143,7 @@ jobs:
bun run submodule-reload bun run submodule-reload
- name: 💾 Cache Gradle global - name: 💾 Cache Gradle global
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches
@@ -156,7 +156,7 @@ jobs:
run: bun run prebuild:tv run: bun run prebuild:tv
- name: 💾 Cache project Gradle (.gradle) - name: 💾 Cache project Gradle (.gradle)
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: android/.gradle path: android/.gradle
key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}
@@ -171,7 +171,7 @@ jobs:
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
- name: 📤 Upload APK artifact - name: 📤 Upload APK artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: streamyfin-android-tv-apk-${{ env.DATE_TAG }} name: streamyfin-android-tv-apk-${{ env.DATE_TAG }}
path: | path: |
@@ -195,12 +195,12 @@ jobs:
show-progress: false show-progress: false
- name: 🍞 Setup Bun - name: 🍞 Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
with: with:
bun-version: latest bun-version: latest
- name: 💾 Cache Bun dependencies - name: 💾 Cache Bun dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.bun/install/cache path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
@@ -216,12 +216,12 @@ jobs:
run: bun run prebuild run: bun run prebuild
- name: 🔧 Setup Xcode - name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
with: with:
xcode-version: "26.2" xcode-version: "26.2"
- name: 🏗️ Setup EAS - name: 🏗️ Setup EAS
uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main uses: expo/expo-github-action@main
with: with:
eas-version: latest eas-version: latest
token: ${{ secrets.EXPO_TOKEN }} token: ${{ secrets.EXPO_TOKEN }}
@@ -236,7 +236,7 @@ jobs:
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
- name: 📤 Upload IPA artifact - name: 📤 Upload IPA artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: streamyfin-ios-phone-ipa-${{ env.DATE_TAG }} name: streamyfin-ios-phone-ipa-${{ env.DATE_TAG }}
path: build-*.ipa path: build-*.ipa
@@ -259,12 +259,12 @@ jobs:
show-progress: false show-progress: false
- name: 🍞 Setup Bun - name: 🍞 Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
with: with:
bun-version: latest bun-version: latest
- name: 💾 Cache Bun dependencies - name: 💾 Cache Bun dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.bun/install/cache path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
@@ -280,7 +280,7 @@ jobs:
run: bun run prebuild run: bun run prebuild
- name: 🔧 Setup Xcode - name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
with: with:
xcode-version: "26.2" xcode-version: "26.2"
@@ -293,133 +293,73 @@ jobs:
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
- name: 📤 Upload IPA artifact - name: 📤 Upload IPA artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: streamyfin-ios-phone-unsigned-ipa-${{ env.DATE_TAG }} name: streamyfin-ios-phone-unsigned-ipa-${{ env.DATE_TAG }}
path: build/*.ipa path: build/*.ipa
retention-days: 7 retention-days: 7
build-ios-tv: # Disabled for now - uncomment when ready to build iOS TV
# Temporarily disabled until feat/tv-interface is merged (TV UI not ready). # build-ios-tv:
# Re-enable by removing the `false &&` prefix below. # if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
if: false && (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin')) # runs-on: macos-26
runs-on: macos-26 # name: 🍎 Build iOS IPA (TV)
name: 🍎 Build tvOS IPA # permissions:
permissions: # contents: read
contents: read #
# steps:
steps: # - name: 📥 Checkout code
- name: 📥 Checkout code # uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # with:
with: # ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }} # fetch-depth: 0
fetch-depth: 0 # submodules: recursive
submodules: recursive # show-progress: false
show-progress: false #
# - name: 🍞 Setup Bun
- name: 🍞 Setup Bun # uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 # with:
with: # bun-version: latest
bun-version: latest #
# - name: 💾 Cache Bun dependencies
- name: 💾 Cache Bun dependencies # uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 # with:
with: # path: ~/.bun/install/cache
path: ~/.bun/install/cache # key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} # restore-keys: |
restore-keys: | # ${{ runner.os }}-bun-cache
${{ runner.os }}-bun-cache #
# - name: 📦 Install dependencies and reload submodules
- name: 📦 Install dependencies and reload submodules # run: |
run: | # bun install --frozen-lockfile
bun install --frozen-lockfile # bun run submodule-reload
bun run submodule-reload #
# - name: 🛠️ Generate project files
- name: 🛠️ Generate project files # run: bun run prebuild:tv
run: bun run prebuild:tv #
# - name: 🔧 Setup Xcode
- name: 🔧 Setup Xcode # uses: maxim-lobanov/setup-xcode@v1
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 # with:
with: # xcode-version: '26.0.1'
xcode-version: "26.2" #
# - name: 🏗️ Setup EAS
- name: 🏗️ Setup EAS # uses: expo/expo-github-action@main
uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main # with:
with: # eas-version: latest
eas-version: latest # token: ${{ secrets.EXPO_TOKEN }}
token: ${{ secrets.EXPO_TOKEN }} # eas-cache: true
eas-cache: true #
# - name: 🚀 Build iOS app
- name: 🚀 Build iOS app # env:
env: # EXPO_TV: 1
EXPO_TV: 1 # run: eas build -p ios --local --non-interactive
run: eas build -p ios --local --non-interactive #
# - name: 📅 Set date tag
- name: 📅 Set date tag # run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV #
# - name: 📤 Upload IPA artifact
- name: 📤 Upload IPA artifact # uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 # with:
with: # name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }}
name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }} # path: build-*.ipa
path: build-*.ipa # retention-days: 7
retention-days: 7
build-ios-tv-unsigned:
# Temporarily disabled until feat/tv-interface is merged (TV UI not ready).
# Re-enable by removing the `false &&` prefix below.
if: false && (!contains(github.event.head_commit.message, '[skip ci]'))
runs-on: macos-26
name: 🍎 Build tvOS IPA (Unsigned)
permissions:
contents: read
steps:
- name: 📥 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
submodules: recursive
show-progress: false
- name: 🍞 Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
- name: 💾 Cache Bun dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-cache
- name: 📦 Install dependencies and reload submodules
run: |
bun install --frozen-lockfile
bun run submodule-reload
- name: 🛠️ Generate project files
run: bun run prebuild:tv
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
xcode-version: "26.2"
- name: 🚀 Build iOS app
env:
EXPO_TV: 1
run: bun run ios:unsigned-build:tv ${{ github.event_name == 'pull_request' && '-- --verbose' || '' }}
- 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: streamyfin-ios-tv-unsigned-ipa-${{ env.DATE_TAG }}
path: build/*.ipa
retention-days: 7

View File

@@ -27,12 +27,12 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: 🍞 Setup Bun - name: 🍞 Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
with: with:
bun-version: latest bun-version: latest
- name: 💾 Cache Bun dependencies - name: 💾 Cache Bun dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: | path: |
~/.bun/install/cache ~/.bun/install/cache

View File

@@ -27,13 +27,13 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 🏁 Initialize CodeQL - name: 🏁 Initialize CodeQL
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended,security-and-quality queries: +security-extended,security-and-quality
- name: 🛠️ Autobuild - name: 🛠️ Autobuild
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
- name: 🧪 Perform CodeQL Analysis - name: 🧪 Perform CodeQL Analysis
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0

View File

@@ -28,7 +28,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: 🌐 Sync Translations with Crowdin - name: 🌐 Sync Translations with Crowdin
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0
with: with:
upload_sources: true upload_sources: true
upload_translations: true upload_translations: true

View File

@@ -25,7 +25,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
if: always() && (steps.lint_pr_title.outputs.error_message != null) if: always() && (steps.lint_pr_title.outputs.error_message != null)
with: with:
header: pr-title-lint-error header: pr-title-lint-error
@@ -39,7 +39,7 @@ jobs:
``` ```
- if: ${{ steps.lint_pr_title.outputs.error_message == null }} - if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
with: with:
header: pr-title-lint-error header: pr-title-lint-error
delete: true delete: true
@@ -57,7 +57,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Dependency Review - name: Dependency Review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
with: with:
fail-on-severity: high fail-on-severity: high
base-ref: ${{ github.event.pull_request.base.sha || 'develop' }} base-ref: ${{ github.event.pull_request.base.sha || 'develop' }}
@@ -76,7 +76,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: 🍞 Setup Bun - name: 🍞 Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
with: with:
bun-version: latest bun-version: latest
@@ -107,12 +107,12 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: "🟢 Setup Node.js" - name: "🟢 Setup Node.js"
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with: with:
node-version: '24.x' node-version: '24.x'
- name: "🍞 Setup Bun" - name: "🍞 Setup Bun"
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
with: with:
bun-version: latest bun-version: latest

View File

@@ -21,14 +21,14 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: "🟢 Setup Node.js" - name: "🟢 Setup Node.js"
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with: with:
node-version: '24.x' node-version: '24.x'
cache: 'npm' cache: 'npm'
- name: 🔍 Extract minor version from app.json - name: 🔍 Extract minor version from app.json
id: minor id: minor
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # main uses: actions/github-script@main
with: with:
result-encoding: string result-encoding: string
script: | script: |
@@ -54,7 +54,7 @@ jobs:
dry_run: no-push dry_run: no-push
- name: 📬 Commit and create pull request - name: 📬 Commit and create pull request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with: with:
add-paths: .github/ISSUE_TEMPLATE/bug_report.yml add-paths: .github/ISSUE_TEMPLATE/bug_report.yml
branch: ci-update-bug-report branch: ci-update-bug-report

View File

@@ -61,10 +61,7 @@ export default function Page() {
setLoading(true); setLoading(true);
try { try {
logsFile.write(JSON.stringify(filteredLogs)); logsFile.write(JSON.stringify(filteredLogs));
await Sharing.shareAsync(logsFile.uri, { await Sharing.shareAsync(logsFile.uri, { mimeType: "txt", UTI: "txt" });
mimeType: "text/plain",
UTI: "public.plain-text",
});
} catch (e: any) { } catch (e: any) {
writeErrorLog("Something went wrong attempting to export", e); writeErrorLog("Something went wrong attempting to export", e);
} finally { } finally {

View File

@@ -1,101 +0,0 @@
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "expo-router";
import { TFunction } from "i18next";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import { PlatformDropdown } from "@/components/PlatformDropdown";
import { SegmentSkipMode, useSettings } from "@/utils/atoms/settings";
type SkipSettingKey =
| "skipIntro"
| "skipOutro"
| "skipRecap"
| "skipCommercial"
| "skipPreview";
const SEGMENTS: ReadonlyArray<{ key: SkipSettingKey; labelKey: string }> = [
{ key: "skipIntro", labelKey: "skip_intro" },
{ key: "skipOutro", labelKey: "skip_outro" },
{ key: "skipRecap", labelKey: "skip_recap" },
{ key: "skipCommercial", labelKey: "skip_commercial" },
{ key: "skipPreview", labelKey: "skip_preview" },
];
const SEGMENT_SKIP_OPTIONS = (
t: TFunction<"translation", undefined>,
): Array<{ label: string; value: SegmentSkipMode }> => [
{ label: t("home.settings.other.segment_skip_auto"), value: "auto" },
{ label: t("home.settings.other.segment_skip_ask"), value: "ask" },
{ label: t("home.settings.other.segment_skip_none"), value: "none" },
];
export default function SegmentSkipPage() {
const { settings, updateSettings, pluginSettings } = useSettings();
const { t } = useTranslation();
const navigation = useNavigation();
useEffect(() => {
navigation.setOptions({
title: t("home.settings.other.segment_skip_settings"),
});
}, [navigation, t]);
const options = useMemo(() => SEGMENT_SKIP_OPTIONS(t), [t]);
if (!settings) return null;
return (
<View className='px-4'>
<ListGroup>
{SEGMENTS.map(({ key, labelKey }) => {
const current = settings[key];
const locked = pluginSettings?.[key]?.locked ?? false;
const groups = [
{
options: options.map((o) => ({
type: "radio" as const,
label: o.label,
value: o.value,
selected: o.value === current,
disabled: locked,
onPress: () => {
if (locked) return;
updateSettings({ [key]: o.value });
},
})),
},
];
return (
<ListItem
key={key}
title={t(`home.settings.other.${labelKey}`)}
subtitle={t(`home.settings.other.${labelKey}_description`)}
disabled={locked}
>
<PlatformDropdown
groups={groups}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{t(`home.settings.other.segment_skip_${current}`)}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t(`home.settings.other.${labelKey}`)}
/>
</ListItem>
);
})}
</ListGroup>
</View>
);
}

View File

@@ -134,7 +134,7 @@ export default function page() {
const audioIndexFromUrl = audioIndexStr const audioIndexFromUrl = audioIndexStr
? Number.parseInt(audioIndexStr, 10) ? Number.parseInt(audioIndexStr, 10)
: undefined; : undefined;
const subtitleIndex = subtitleIndexStr const subtitleIndexFromUrl = subtitleIndexStr
? Number.parseInt(subtitleIndexStr, 10) ? Number.parseInt(subtitleIndexStr, 10)
: -1; : -1;
const bitrateValue = bitrateValueStr const bitrateValue = bitrateValueStr
@@ -161,6 +161,24 @@ export default function page() {
return undefined; return undefined;
}, [audioIndexFromUrl, offline, downloadedItem?.userData?.audioStreamIndex]); }, [audioIndexFromUrl, offline, downloadedItem?.userData?.audioStreamIndex]);
// Resolve subtitle index: use URL param if provided, otherwise use stored index for offline playback
const subtitleIndex = useMemo(() => {
if (subtitleIndexFromUrl !== undefined) {
return subtitleIndexFromUrl;
}
if (
offline &&
downloadedItem?.userData?.subtitleStreamIndex !== undefined
) {
return downloadedItem.userData.subtitleStreamIndex;
}
return -1;
}, [
subtitleIndexFromUrl,
offline,
downloadedItem?.userData?.subtitleStreamIndex,
]);
// Get the playback speed for this item based on settings // Get the playback speed for this item based on settings
const { playbackSpeed: initialPlaybackSpeed } = usePlaybackSpeed( const { playbackSpeed: initialPlaybackSpeed } = usePlaybackSpeed(
item, item,
@@ -406,8 +424,8 @@ export default function page() {
return { return {
ItemId: item.Id, ItemId: item.Id,
AudioStreamIndex: audioIndex ? audioIndex : undefined, AudioStreamIndex: audioIndex,
SubtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, SubtitleStreamIndex: subtitleIndex,
MediaSourceId: mediaSourceId, MediaSourceId: mediaSourceId,
PositionTicks: msToTicks(progress.get()), PositionTicks: msToTicks(progress.get()),
IsPaused: !isPlaying, IsPaused: !isPlaying,

1426
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@ import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColorsReturn } from "@/hooks/useImageColorsReturn"; import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
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 { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useOfflineMode } from "@/providers/OfflineModeProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
@@ -53,6 +54,9 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
({ item, itemWithSources }) => { ({ item, itemWithSources }) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const isOffline = useOfflineMode(); const isOffline = useOfflineMode();
const { getDownloadedItemById } = useDownload();
const downloadedItem =
isOffline && item.Id ? getDownloadedItemById(item.Id) : null;
const { settings } = useSettings(); const { settings } = useSettings();
const { orientation } = useOrientation(); const { orientation } = useOrientation();
const navigation = useNavigation(); const navigation = useNavigation();
@@ -91,17 +95,29 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
// Needs to automatically change the selected to the default values for default indexes. // Needs to automatically change the selected to the default values for default indexes.
useEffect(() => { useEffect(() => {
// When offline, use the indices stored in userData (the last-used tracks for this file)
// rather than the server's defaults, so MediaSourceButton reflects what will actually play.
const offlineUserData = downloadedItem?.userData;
setSelectedOptions(() => ({ setSelectedOptions(() => ({
bitrate: defaultBitrate, bitrate: defaultBitrate,
mediaSource: defaultMediaSource ?? undefined, mediaSource: defaultMediaSource ?? undefined,
subtitleIndex: defaultSubtitleIndex ?? -1, subtitleIndex:
audioIndex: defaultAudioIndex, offlineUserData && !offlineUserData.isTranscoded
? offlineUserData.subtitleStreamIndex
: (defaultSubtitleIndex ?? -1),
audioIndex:
offlineUserData && !offlineUserData.isTranscoded
? offlineUserData.audioStreamIndex
: defaultAudioIndex,
})); }));
}, [ }, [
defaultAudioIndex, defaultAudioIndex,
defaultBitrate, defaultBitrate,
defaultSubtitleIndex, defaultSubtitleIndex,
defaultMediaSource, defaultMediaSource,
downloadedItem?.userData?.audioStreamIndex,
downloadedItem?.userData?.subtitleStreamIndex,
]); ]);
useEffect(() => { useEffect(() => {
@@ -232,14 +248,12 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
colors={itemColors} colors={itemColors}
/> />
<View className='w-1' /> <View className='w-1' />
{!isOffline && ( <MediaSourceButton
<MediaSourceButton selectedOptions={selectedOptions}
selectedOptions={selectedOptions} setSelectedOptions={setSelectedOptions}
setSelectedOptions={setSelectedOptions} item={itemWithSources}
item={itemWithSources} colors={itemColors}
colors={itemColors} />
/>
)}
</View> </View>
</View> </View>
{item.Type === "Episode" && ( {item.Type === "Episode" && (

View File

@@ -7,6 +7,8 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import type { ThemeColors } from "@/hooks/useImageColorsReturn"; import type { ThemeColors } from "@/hooks/useImageColorsReturn";
import { useDownload } from "@/providers/DownloadProvider";
import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { BITRATES } from "./BitRateSheet"; import { BITRATES } from "./BitRateSheet";
import type { SelectedOptions } from "./ItemContent"; import type { SelectedOptions } from "./ItemContent";
import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown"; import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
@@ -28,6 +30,14 @@ export const MediaSourceButton: React.FC<Props> = ({
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const isOffline = useOfflineMode();
const { getDownloadedItemById } = useDownload();
// For transcoded downloads there's only one burned-in track — nothing to pick
const isTranscodedDownload = useMemo(() => {
if (!isOffline || !item?.Id) return false;
return getDownloadedItemById(item.Id)?.userData?.isTranscoded === true;
}, [isOffline, item?.Id, getDownloadedItemById]);
const effectiveColors = colors || { const effectiveColors = colors || {
primary: "#7c3aed", primary: "#7c3aed",
@@ -72,34 +82,36 @@ export const MediaSourceButton: React.FC<Props> = ({
const optionGroups: OptionGroup[] = useMemo(() => { const optionGroups: OptionGroup[] = useMemo(() => {
const groups: OptionGroup[] = []; const groups: OptionGroup[] = [];
// Bitrate group if (!isOffline) {
groups.push({ // Bitrate group
title: t("item_card.quality"),
options: BITRATES.map((bitrate) => ({
type: "radio" as const,
label: bitrate.key,
value: bitrate,
selected: bitrate.value === selectedOptions.bitrate?.value,
onPress: () =>
setSelectedOptions((prev) => prev && { ...prev, bitrate }),
})),
});
// Media Source group (only if multiple sources)
if (item?.MediaSources && item.MediaSources.length > 1) {
groups.push({ groups.push({
title: t("item_card.video"), title: t("item_card.quality"),
options: item.MediaSources.map((source) => ({ options: BITRATES.map((bitrate) => ({
type: "radio" as const, type: "radio" as const,
label: getMediaSourceDisplayName(source), label: bitrate.key,
value: source, value: bitrate,
selected: source.Id === selectedOptions.mediaSource?.Id, selected: bitrate.value === selectedOptions.bitrate?.value,
onPress: () => onPress: () =>
setSelectedOptions( setSelectedOptions((prev) => prev && { ...prev, bitrate }),
(prev) => prev && { ...prev, mediaSource: source },
),
})), })),
}); });
// Media Source group (only if multiple sources)
if (item?.MediaSources && item.MediaSources.length > 1) {
groups.push({
title: t("item_card.video"),
options: item.MediaSources.map((source) => ({
type: "radio" as const,
label: getMediaSourceDisplayName(source),
value: source,
selected: source.Id === selectedOptions.mediaSource?.Id,
onPress: () =>
setSelectedOptions(
(prev) => prev && { ...prev, mediaSource: source },
),
})),
});
}
} }
// Audio track group // Audio track group
@@ -150,6 +162,7 @@ export const MediaSourceButton: React.FC<Props> = ({
return groups; return groups;
}, [ }, [
item, item,
isOffline,
selectedOptions, selectedOptions,
audioStreams, audioStreams,
subtitleStreams, subtitleStreams,
@@ -178,6 +191,8 @@ export const MediaSourceButton: React.FC<Props> = ({
</TouchableOpacity> </TouchableOpacity>
); );
if (isTranscodedDownload) return null;
return ( return (
<PlatformDropdown <PlatformDropdown
groups={optionGroups} groups={optionGroups}

View File

@@ -96,14 +96,23 @@ export const PlayButton: React.FC<Props> = ({
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
itemId: item.Id!, itemId: item.Id!,
audioIndex: selectedOptions.audioIndex?.toString() ?? "",
subtitleIndex: selectedOptions.subtitleIndex?.toString() ?? "",
mediaSourceId: selectedOptions.mediaSource?.Id ?? "", mediaSourceId: selectedOptions.mediaSource?.Id ?? "",
bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "", bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "",
playbackPosition: item.UserData?.PlaybackPositionTicks?.toString() ?? "0", playbackPosition: item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
offline: isOffline ? "true" : "false", offline: isOffline ? "true" : "false",
}); });
if (selectedOptions.audioIndex !== undefined) {
queryParams.set("audioIndex", selectedOptions.audioIndex.toString());
}
if (selectedOptions.subtitleIndex !== undefined) {
queryParams.set(
"subtitleIndex",
selectedOptions.subtitleIndex.toString(),
);
}
const queryString = queryParams.toString(); const queryString = queryParams.toString();
if (!client) { if (!client) {
@@ -292,6 +301,29 @@ export const PlayButton: React.FC<Props> = ({
t, t,
]); ]);
const buildOfflineQueryParams = useCallback(
(downloadedItem: NonNullable<ReturnType<typeof getDownloadedItemById>>) => {
const isTranscoded = downloadedItem.userData?.isTranscoded === true;
const audioIdx = isTranscoded
? downloadedItem.userData?.audioStreamIndex
: selectedOptions.audioIndex;
const subtitleIdx = isTranscoded
? downloadedItem.userData?.subtitleStreamIndex
: selectedOptions.subtitleIndex;
const params = new URLSearchParams({
itemId: item.Id!,
offline: "true",
playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
});
if (audioIdx !== undefined) params.set("audioIndex", audioIdx.toString());
if (subtitleIdx !== undefined)
params.set("subtitleIndex", subtitleIdx.toString());
return params;
},
[item, selectedOptions],
);
const onPress = useCallback(async () => { const onPress = useCallback(async () => {
if (!item) return; if (!item) return;
@@ -302,13 +334,7 @@ export const PlayButton: React.FC<Props> = ({
// If already in offline mode, play downloaded file directly // If already in offline mode, play downloaded file directly
if (isOffline && downloadedItem) { if (isOffline && downloadedItem) {
const queryParams = new URLSearchParams({ goToPlayer(buildOfflineQueryParams(downloadedItem).toString());
itemId: item.Id!,
offline: "true",
playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
});
goToPlayer(queryParams.toString());
return; return;
} }
@@ -331,13 +357,9 @@ export const PlayButton: React.FC<Props> = ({
<Button <Button
onPress={() => { onPress={() => {
hideModal(); hideModal();
const queryParams = new URLSearchParams({ goToPlayer(
itemId: item.Id!, buildOfflineQueryParams(downloadedItem).toString(),
offline: "true", );
playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
});
goToPlayer(queryParams.toString());
}} }}
color='purple' color='purple'
> >
@@ -374,13 +396,7 @@ export const PlayButton: React.FC<Props> = ({
{ {
text: t("player.downloaded_file_yes"), text: t("player.downloaded_file_yes"),
onPress: () => { onPress: () => {
const queryParams = new URLSearchParams({ goToPlayer(buildOfflineQueryParams(downloadedItem).toString());
itemId: item.Id!,
offline: "true",
playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
});
goToPlayer(queryParams.toString());
}, },
isPreferred: true, isPreferred: true,
}, },

View File

@@ -8,7 +8,6 @@ import { BITRATES } from "@/components/BitrateSelector";
import { PlatformDropdown } from "@/components/PlatformDropdown"; import { PlatformDropdown } from "@/components/PlatformDropdown";
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector"; import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
import DisabledSetting from "@/components/settings/DisabledSetting"; import DisabledSetting from "@/components/settings/DisabledSetting";
import useRouter from "@/hooks/useAppRouter";
import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings"; import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
@@ -16,7 +15,6 @@ import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
export const PlaybackControlsSettings: React.FC = () => { export const PlaybackControlsSettings: React.FC = () => {
const router = useRouter();
const { settings, updateSettings, pluginSettings } = useSettings(); const { settings, updateSettings, pluginSettings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -250,15 +248,6 @@ export const PlaybackControlsSettings: React.FC = () => {
title={t("home.settings.other.max_auto_play_episode_count")} title={t("home.settings.other.max_auto_play_episode_count")}
/> />
</ListItem> </ListItem>
{/* Media Segment Skip Settings */}
<ListItem
title={t("home.settings.other.segment_skip_settings")}
subtitle={t("home.settings.other.segment_skip_settings_description")}
onPress={() => router.push("/settings/segment-skip/page")}
>
<Ionicons name='chevron-forward' size={20} color='#8E8D91' />
</ListItem>
</ListGroup> </ListGroup>
</DisabledSetting> </DisabledSetting>
); );

View File

@@ -18,13 +18,11 @@ interface BottomControlsProps {
showRemoteBubble: boolean; showRemoteBubble: boolean;
currentTime: number; currentTime: number;
remainingTime: number; remainingTime: number;
showSkipSegmentButton: boolean; showSkipButton: boolean;
skipSegmentButtonText: string; showSkipCreditButton: boolean;
showSkipOutroButton: boolean;
skipOutroButtonText: string;
hasContentAfterCredits: boolean; hasContentAfterCredits: boolean;
onSkipSegment: () => void; skipIntro: () => void;
onSkipOutro: () => void; skipCredit: () => void;
nextItem?: BaseItemDto | null; nextItem?: BaseItemDto | null;
handleNextEpisodeAutoPlay: () => void; handleNextEpisodeAutoPlay: () => void;
handleNextEpisodeManual: () => void; handleNextEpisodeManual: () => void;
@@ -68,13 +66,11 @@ export const BottomControls: FC<BottomControlsProps> = ({
showRemoteBubble, showRemoteBubble,
currentTime, currentTime,
remainingTime, remainingTime,
showSkipSegmentButton, showSkipButton,
skipSegmentButtonText, showSkipCreditButton,
showSkipOutroButton,
skipOutroButtonText,
hasContentAfterCredits, hasContentAfterCredits,
onSkipSegment, skipIntro,
onSkipOutro, skipCredit,
nextItem, nextItem,
handleNextEpisodeAutoPlay, handleNextEpisodeAutoPlay,
handleNextEpisodeManual, handleNextEpisodeManual,
@@ -138,18 +134,19 @@ export const BottomControls: FC<BottomControlsProps> = ({
</View> </View>
<View className='flex flex-row space-x-2 shrink-0'> <View className='flex flex-row space-x-2 shrink-0'>
<SkipButton <SkipButton
showButton={showSkipSegmentButton} showButton={showSkipButton}
onPress={onSkipSegment} onPress={skipIntro}
buttonText={skipSegmentButtonText} buttonText='Skip Intro'
/> />
{/* Outro button defers to "Next Episode" when credits run to the {/* Smart Skip Credits behavior:
video end and a next episode exists. */} - Show "Skip Credits" if there's content after credits OR no next episode
- Show "Next Episode" if credits extend to video end AND next episode exists */}
<SkipButton <SkipButton
showButton={ showButton={
showSkipOutroButton && (hasContentAfterCredits || !nextItem) showSkipCreditButton && (hasContentAfterCredits || !nextItem)
} }
onPress={onSkipOutro} onPress={skipCredit}
buttonText={skipOutroButtonText} buttonText='Skip Credits'
/> />
{settings.autoPlayNextEpisode !== false && {settings.autoPlayNextEpisode !== false &&
(settings.maxAutoPlayEpisodeCount.value === -1 || (settings.maxAutoPlayEpisodeCount.value === -1 ||
@@ -160,7 +157,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
!nextItem !nextItem
? false ? false
: // Show during credits if no content after, OR near end of video : // Show during credits if no content after, OR near end of video
(showSkipOutroButton && !hasContentAfterCredits) || (showSkipCreditButton && !hasContentAfterCredits) ||
remainingTime < 10000 remainingTime < 10000
} }
onFinish={handleNextEpisodeAutoPlay} onFinish={handleNextEpisodeAutoPlay}

View File

@@ -4,15 +4,7 @@ import type {
MediaSourceInfo, MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { import { type FC, useCallback, useEffect, useState } from "react";
type FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, useWindowDimensions, View } from "react-native"; import { StyleSheet, useWindowDimensions, View } from "react-native";
import Animated, { import Animated, {
Easing, Easing,
@@ -24,17 +16,17 @@ import Animated, {
} from "react-native-reanimated"; } from "react-native-reanimated";
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay"; import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
import { useHaptic } from "@/hooks/useHaptic"; import { useHaptic } from "@/hooks/useHaptic";
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
import { usePlaybackManager } from "@/hooks/usePlaybackManager"; import { usePlaybackManager } from "@/hooks/usePlaybackManager";
import { useSegmentSkipper } from "@/hooks/useSegmentSkipper";
import { useTrickplay } from "@/hooks/useTrickplay"; import { useTrickplay } from "@/hooks/useTrickplay";
import type { TechnicalInfo } from "@/modules/mpv-player"; import type { TechnicalInfo } from "@/modules/mpv-player";
import { DownloadedItem } from "@/providers/Downloads/types"; import { DownloadedItem } from "@/providers/Downloads/types";
import { useOfflineMode } from "@/providers/OfflineModeProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { useSegments } from "@/utils/segments"; import { ticksToMs } from "@/utils/time";
import { msToSeconds, ticksToMs } from "@/utils/time";
import { BottomControls } from "./BottomControls"; import { BottomControls } from "./BottomControls";
import { CenterControls } from "./CenterControls"; import { CenterControls } from "./CenterControls";
import { CONTROLS_CONSTANTS } from "./constants"; import { CONTROLS_CONSTANTS } from "./constants";
@@ -50,9 +42,6 @@ import { useControlsTimeout } from "./useControlsTimeout";
import { PlaybackSpeedScope } from "./utils/playback-speed-settings"; import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
import { type AspectRatio } from "./VideoScalingModeSelector"; import { type AspectRatio } from "./VideoScalingModeSelector";
// No-op function to avoid creating new references on every render
const noop = () => {};
interface Props { interface Props {
item: BaseItemDto; item: BaseItemDto;
isPlaying: boolean; isPlaying: boolean;
@@ -121,24 +110,6 @@ export const Controls: FC<Props> = ({
const [episodeView, setEpisodeView] = useState(false); const [episodeView, setEpisodeView] = useState(false);
const [showAudioSlider, setShowAudioSlider] = useState(false); const [showAudioSlider, setShowAudioSlider] = useState(false);
// Ref to track pending play timeout for cleanup and cancellation
const playTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Mutable ref tracking isPlaying to avoid stale closures in seekMs timeout
const playingRef = useRef(isPlaying);
useEffect(() => {
playingRef.current = isPlaying;
}, [isPlaying]);
// Clean up timeout on unmount
useEffect(() => {
return () => {
if (playTimeoutRef.current) {
clearTimeout(playTimeoutRef.current);
}
};
}, []);
const { height: screenHeight, width: screenWidth } = useWindowDimensions(); const { height: screenHeight, width: screenWidth } = useWindowDimensions();
const { previousItem, nextItem } = usePlaybackManager({ const { previousItem, nextItem } = usePlaybackManager({
item, item,
@@ -329,140 +300,27 @@ export const Controls: FC<Props> = ({
subtitleIndex: string; subtitleIndex: string;
}>(); }>();
// Fetch all segments for the current item const { showSkipButton, skipIntro } = useIntroSkipper(
const { data: segments } = useSegments( item.Id!,
item.Id ?? "", currentTime,
seek,
play,
offline, offline,
downloadedFiles,
api, api,
downloadedFiles,
); );
const currentTimeSeconds = msToSeconds(currentTime); const { showSkipCreditButton, skipCredit, hasContentAfterCredits } =
const maxSeconds = maxMs ? msToSeconds(maxMs) : undefined; useCreditSkipper(
item.Id!,
// Segment hook deals in seconds; player API in ms. The 200ms delayed play() currentTime,
// is a workaround: some seeks otherwise resume from the pre-seek position. seek,
const seekMs = useCallback( play,
(timeInSeconds: number) => { offline,
if (playTimeoutRef.current) { api,
clearTimeout(playTimeoutRef.current); downloadedFiles,
} maxMs,
seek(timeInSeconds * 1000); );
playTimeoutRef.current = setTimeout(() => {
// playingRef avoids a stale closure: re-check current isPlaying.
if (playingRef.current) {
play();
}
playTimeoutRef.current = null;
}, 200);
},
[seek, play],
);
const introSkipper = useSegmentSkipper({
segments: segments?.introSegments || [],
segmentType: "Intro",
currentTime: currentTimeSeconds,
seek: seekMs,
isPaused: !isPlaying,
});
const outroSkipper = useSegmentSkipper({
segments: segments?.creditSegments || [],
segmentType: "Outro",
currentTime: currentTimeSeconds,
totalDuration: maxSeconds,
seek: seekMs,
isPaused: !isPlaying,
});
const recapSkipper = useSegmentSkipper({
segments: segments?.recapSegments || [],
segmentType: "Recap",
currentTime: currentTimeSeconds,
seek: seekMs,
isPaused: !isPlaying,
});
const commercialSkipper = useSegmentSkipper({
segments: segments?.commercialSegments || [],
segmentType: "Commercial",
currentTime: currentTimeSeconds,
seek: seekMs,
isPaused: !isPlaying,
});
const previewSkipper = useSegmentSkipper({
segments: segments?.previewSegments || [],
segmentType: "Preview",
currentTime: currentTimeSeconds,
seek: seekMs,
isPaused: !isPlaying,
});
// Priority when multiple segments overlap: Commercial > Recap > Intro > Preview > Outro.
const activeSegment = useMemo(() => {
if (commercialSkipper.currentSegment)
return {
type: "Commercial" as const,
currentSegment: commercialSkipper.currentSegment,
skipSegment: commercialSkipper.skipSegment,
};
if (recapSkipper.currentSegment)
return {
type: "Recap" as const,
currentSegment: recapSkipper.currentSegment,
skipSegment: recapSkipper.skipSegment,
};
if (introSkipper.currentSegment)
return {
type: "Intro" as const,
currentSegment: introSkipper.currentSegment,
skipSegment: introSkipper.skipSegment,
};
if (previewSkipper.currentSegment)
return {
type: "Preview" as const,
currentSegment: previewSkipper.currentSegment,
skipSegment: previewSkipper.skipSegment,
};
if (outroSkipper.currentSegment)
return {
type: "Outro" as const,
currentSegment: outroSkipper.currentSegment,
skipSegment: outroSkipper.skipSegment,
};
return null;
}, [
commercialSkipper.currentSegment,
commercialSkipper.skipSegment,
recapSkipper.currentSegment,
recapSkipper.skipSegment,
introSkipper.currentSegment,
introSkipper.skipSegment,
previewSkipper.currentSegment,
previewSkipper.skipSegment,
outroSkipper.currentSegment,
outroSkipper.skipSegment,
]);
// Outro gets a dedicated button (so it can compose with Next Episode logic);
// every other segment type shares the generic skip button.
const showSkipSegmentButton =
!!activeSegment && activeSegment.type !== "Outro";
const onSkipSegment = activeSegment?.skipSegment ?? noop;
const showSkipOutroButton = activeSegment?.type === "Outro";
const onSkipOutro = outroSkipper.skipSegment;
const hasContentAfterCredits =
outroSkipper.currentSegment && maxSeconds
? outroSkipper.currentSegment.endTime < maxSeconds
: false;
const { t } = useTranslation();
const skipSegmentButtonText = activeSegment
? t(`player.skip_${activeSegment.type.toLowerCase()}`)
: t("player.skip_intro");
const skipOutroButtonText = t("player.skip_outro");
const goToItemCommon = useCallback( const goToItemCommon = useCallback(
(item: BaseItemDto) => { (item: BaseItemDto) => {
@@ -675,13 +533,11 @@ export const Controls: FC<Props> = ({
showRemoteBubble={showRemoteBubble} showRemoteBubble={showRemoteBubble}
currentTime={currentTime} currentTime={currentTime}
remainingTime={remainingTime} remainingTime={remainingTime}
showSkipSegmentButton={showSkipSegmentButton} showSkipButton={showSkipButton}
skipSegmentButtonText={skipSegmentButtonText} showSkipCreditButton={showSkipCreditButton}
showSkipOutroButton={showSkipOutroButton}
skipOutroButtonText={skipOutroButtonText}
hasContentAfterCredits={hasContentAfterCredits} hasContentAfterCredits={hasContentAfterCredits}
onSkipSegment={onSkipSegment} skipIntro={skipIntro}
onSkipOutro={onSkipOutro} skipCredit={skipCredit}
nextItem={nextItem} nextItem={nextItem}
handleNextEpisodeAutoPlay={handleNextEpisodeAutoPlay} handleNextEpisodeAutoPlay={handleNextEpisodeAutoPlay}
handleNextEpisodeManual={handleNextEpisodeManual} handleNextEpisodeManual={handleNextEpisodeManual}

109
hooks/useCreditSkipper.ts Normal file
View File

@@ -0,0 +1,109 @@
import { Api } from "@jellyfin/sdk";
import { useCallback, useEffect, useState } from "react";
import { DownloadedItem } from "@/providers/Downloads/types";
import { useSegments } from "@/utils/segments";
import { msToSeconds, secondsToMs } from "@/utils/time";
import { useHaptic } from "./useHaptic";
/**
* Custom hook to handle skipping credits in a media player.
* The player reports time values in milliseconds.
*/
export const useCreditSkipper = (
itemId: string,
currentTime: number,
seek: (ms: number) => void,
play: () => void,
isOffline = false,
api: Api | null = null,
downloadedFiles: DownloadedItem[] | undefined = undefined,
totalDuration?: number,
) => {
const [showSkipCreditButton, setShowSkipCreditButton] = useState(false);
const lightHapticFeedback = useHaptic("light");
// Convert ms to seconds for comparison with timestamps
const currentTimeSeconds = msToSeconds(currentTime);
const totalDurationInSeconds =
totalDuration != null ? msToSeconds(totalDuration) : undefined;
// Regular function (not useCallback) to match useIntroSkipper pattern
const wrappedSeek = (seconds: number) => {
seek(secondsToMs(seconds));
};
const { data: segments } = useSegments(
itemId,
isOffline,
downloadedFiles,
api,
);
const creditTimestamps = segments?.creditSegments?.[0];
// Determine if there's content after credits (credits don't extend to video end)
// Use a 5-second buffer to account for timing discrepancies
const hasContentAfterCredits = (() => {
if (
!creditTimestamps ||
totalDurationInSeconds == null ||
!Number.isFinite(totalDurationInSeconds)
) {
return false;
}
const creditsEndToVideoEnd =
totalDurationInSeconds - creditTimestamps.endTime;
// If credits end more than 5 seconds before video ends, there's content after
return creditsEndToVideoEnd > 5;
})();
useEffect(() => {
if (creditTimestamps) {
const shouldShow =
currentTimeSeconds > creditTimestamps.startTime &&
currentTimeSeconds < creditTimestamps.endTime;
setShowSkipCreditButton(shouldShow);
} else {
// Reset button state when no credit timestamps exist
if (showSkipCreditButton) {
setShowSkipCreditButton(false);
}
}
}, [creditTimestamps, currentTimeSeconds, showSkipCreditButton]);
const skipCredit = useCallback(() => {
if (!creditTimestamps) return;
try {
lightHapticFeedback();
// Calculate the target seek position
let seekTarget = creditTimestamps.endTime;
// If we have total duration, ensure we don't seek past the end of the video.
// Some media sources report credit end times that exceed the actual video duration,
// which causes the player to pause/stop when seeking past the end.
// Leave a small buffer (2 seconds) to trigger the natural end-of-video flow
// (next episode countdown, etc.) instead of an abrupt pause.
if (totalDurationInSeconds && seekTarget >= totalDurationInSeconds) {
seekTarget = Math.max(0, totalDurationInSeconds - 2);
}
wrappedSeek(seekTarget);
setTimeout(() => {
play();
}, 200);
} catch (error) {
console.error("[CREDIT_SKIPPER] Error skipping credit", error);
}
}, [
creditTimestamps,
lightHapticFeedback,
wrappedSeek,
play,
totalDurationInSeconds,
]);
return { showSkipCreditButton, skipCredit, hasContentAfterCredits };
};

View File

@@ -1,6 +1,7 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useCallback } from "react"; import { useCallback } from "react";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { getDownloadedItemById } from "@/providers/Downloads/database";
import { usePlaySettings } from "@/providers/PlaySettingsProvider"; import { usePlaySettings } from "@/providers/PlaySettingsProvider";
import { writeToLog } from "@/utils/log"; import { writeToLog } from "@/utils/log";
@@ -15,12 +16,27 @@ export const useDownloadedFileOpener = () => {
console.error("Attempted to open a file without an ID."); console.error("Attempted to open a file without an ID.");
return; return;
} }
const downloadedItem = getDownloadedItemById(item.Id);
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
itemId: item.Id, itemId: item.Id,
offline: "true", offline: "true",
playbackPosition: playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "0", item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
}); });
if (downloadedItem?.userData?.audioStreamIndex !== undefined) {
queryParams.set(
"audioIndex",
downloadedItem.userData.audioStreamIndex.toString(),
);
}
if (downloadedItem?.userData?.subtitleStreamIndex !== undefined) {
queryParams.set(
"subtitleIndex",
downloadedItem.userData.subtitleStreamIndex.toString(),
);
}
try { try {
router.push(`/player/direct-player?${queryParams.toString()}`); router.push(`/player/direct-player?${queryParams.toString()}`);
} catch (error) { } catch (error) {

68
hooks/useIntroSkipper.ts Normal file
View File

@@ -0,0 +1,68 @@
import { Api } from "@jellyfin/sdk";
import { useCallback, useEffect, useState } from "react";
import { DownloadedItem } from "@/providers/Downloads/types";
import { useSegments } from "@/utils/segments";
import { msToSeconds, secondsToMs } from "@/utils/time";
import { useHaptic } from "./useHaptic";
/**
* Custom hook to handle skipping intros in a media player.
* MPV player uses milliseconds for time.
*
* @param {number} currentTime - The current playback time in milliseconds.
*/
export const useIntroSkipper = (
itemId: string,
currentTime: number,
seek: (ms: number) => void,
play: () => void,
isOffline = false,
api: Api | null = null,
downloadedFiles: DownloadedItem[] | undefined = undefined,
) => {
const [showSkipButton, setShowSkipButton] = useState(false);
// Convert ms to seconds for comparison with timestamps
const currentTimeSeconds = msToSeconds(currentTime);
const lightHapticFeedback = useHaptic("light");
const wrappedSeek = (seconds: number) => {
seek(secondsToMs(seconds));
};
const { data: segments } = useSegments(
itemId,
isOffline,
downloadedFiles,
api,
);
const introTimestamps = segments?.introSegments?.[0];
useEffect(() => {
if (introTimestamps) {
const shouldShow =
currentTimeSeconds > introTimestamps.startTime &&
currentTimeSeconds < introTimestamps.endTime;
setShowSkipButton(shouldShow);
} else {
if (showSkipButton) {
setShowSkipButton(false);
}
}
}, [introTimestamps, currentTimeSeconds, showSkipButton]);
const skipIntro = useCallback(() => {
if (!introTimestamps) return;
try {
lightHapticFeedback();
wrappedSeek(introTimestamps.endTime);
setTimeout(() => {
play();
}, 200);
} catch (error) {
console.error("[INTRO_SKIPPER] Error skipping intro", error);
}
}, [introTimestamps, lightHapticFeedback, wrappedSeek, play]);
return { showSkipButton, skipIntro };
};

View File

@@ -186,6 +186,20 @@ export const usePlaybackManager = ({
: playedPercentage, : playedPercentage,
}, },
}, },
// Sync selected audio/subtitle tracks so next playback resumes with
// the same tracks the user had active — but only for non-transcoded
// downloads where the user can freely switch tracks.
userData: localItem.userData.isTranscoded
? localItem.userData
: {
...localItem.userData,
audioStreamIndex:
playbackProgressInfo.AudioStreamIndex ??
localItem.userData.audioStreamIndex,
subtitleStreamIndex:
playbackProgressInfo.SubtitleStreamIndex ??
localItem.userData.subtitleStreamIndex,
},
}); });
// Force invalidate queries so they refetch from updated local database // Force invalidate queries so they refetch from updated local database
queryClient.invalidateQueries({ queryKey: ["item", itemId] }); queryClient.invalidateQueries({ queryKey: ["item", itemId] });

View File

@@ -1,109 +0,0 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import { MediaTimeSegment } from "@/providers/Downloads/types";
import { SegmentSkipMode, useSettings } from "@/utils/atoms/settings";
import { useHaptic } from "./useHaptic";
type SegmentType = "Intro" | "Outro" | "Recap" | "Commercial" | "Preview";
const SEGMENT_TO_SETTING: Record<
SegmentType,
"skipIntro" | "skipOutro" | "skipRecap" | "skipCommercial" | "skipPreview"
> = {
Intro: "skipIntro",
Outro: "skipOutro",
Recap: "skipRecap",
Commercial: "skipCommercial",
Preview: "skipPreview",
};
interface UseSegmentSkipperProps {
segments: MediaTimeSegment[];
segmentType: SegmentType;
currentTime: number;
totalDuration?: number;
seek: (time: number) => void;
isPaused: boolean;
}
interface UseSegmentSkipperReturn {
currentSegment: MediaTimeSegment | null;
skipSegment: (useHaptics?: boolean) => void;
}
/**
* Generic hook to handle all media segment types (intro, outro, recap, commercial, preview)
* Supports three modes: 'none' (disabled), 'ask' (show button), 'auto' (auto-skip)
*/
export const useSegmentSkipper = ({
segments,
segmentType,
currentTime,
totalDuration,
seek,
isPaused,
}: UseSegmentSkipperProps): UseSegmentSkipperReturn => {
const { settings } = useSettings();
const haptic = useHaptic();
const autoSkipTriggeredRef = useRef<string | null>(null);
const skipMode: SegmentSkipMode =
settings?.[SEGMENT_TO_SETTING[segmentType]] ?? "none";
const currentSegment = useMemo(
() =>
segments.find(
(s) => currentTime >= s.startTime && currentTime < s.endTime,
) ?? null,
[segments, currentTime],
);
// Refs let the auto-skip effect avoid re-running when skipSegment/haptic
// identities change (haptic is unstable when disabled).
const seekRef = useRef(seek);
const hapticRef = useRef(haptic);
useEffect(() => {
seekRef.current = seek;
hapticRef.current = haptic;
});
const skipSegment = useCallback(
(useHaptics = true) => {
if (!currentSegment || skipMode === "none") return;
// Outro endTime sometimes exceeds the actual file duration. Keep a 2s
// buffer so the player's natural end-of-video flow (next-episode
// countdown, etc.) still fires instead of stalling at the exact end.
let target = currentSegment.endTime;
if (
segmentType === "Outro" &&
totalDuration != null &&
Number.isFinite(totalDuration) &&
target >= totalDuration
) {
target = Math.max(0, totalDuration - 2);
}
seekRef.current(target);
if (useHaptics) hapticRef.current();
},
[currentSegment, segmentType, totalDuration, skipMode],
);
useEffect(() => {
if (skipMode !== "auto" || isPaused || !currentSegment) {
if (!currentSegment) autoSkipTriggeredRef.current = null;
return;
}
const segmentId = `${currentSegment.startTime}-${currentSegment.endTime}`;
if (autoSkipTriggeredRef.current === segmentId) return;
autoSkipTriggeredRef.current = segmentId;
skipSegment(false);
}, [currentSegment, skipMode, isPaused, skipSegment]);
return {
currentSegment: skipMode === "none" ? null : currentSegment,
skipSegment,
};
};

View File

@@ -171,11 +171,7 @@ final class MPVLayerRenderer {
// Enable composite OSD mode - renders subtitles directly onto video frames using GPU // Enable composite OSD mode - renders subtitles directly onto video frames using GPU
// This is better for PiP as subtitles are baked into the video // This is better for PiP as subtitles are baked into the video
// NOTE: Must be set BEFORE the #if targetEnvironment check or tvOS will freeze on player exit // NOTE: Must be set BEFORE the #if targetEnvironment check or tvOS will freeze on player exit
#if targetEnvironment(simulator)
checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "no"))
#else
checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "yes")) checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "yes"))
#endif
// Hardware decoding with VideoToolbox // Hardware decoding with VideoToolbox
// On simulator, use software decoding since VideoToolbox is not available // On simulator, use software decoding since VideoToolbox is not available

View File

@@ -15,7 +15,6 @@
"android:tv": "cross-env EXPO_TV=1 expo run:android", "android:tv": "cross-env EXPO_TV=1 expo run:android",
"build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease", "build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease",
"ios:unsigned-build": "cross-env EXPO_TV=0 bun scripts/ios/build-ios.ts --production", "ios:unsigned-build": "cross-env EXPO_TV=0 bun scripts/ios/build-ios.ts --production",
"ios:unsigned-build:tv": "cross-env EXPO_TV=1 bun scripts/ios/build-ios.ts --production",
"prepare": "husky", "prepare": "husky",
"typecheck": "node scripts/typecheck.js", "typecheck": "node scripts/typecheck.js",
"check": "biome check . --max-diagnostics 1000", "check": "biome check . --max-diagnostics 1000",
@@ -34,7 +33,7 @@
"@expo/vector-icons": "^15.0.3", "@expo/vector-icons": "^15.0.3",
"@gorhom/bottom-sheet": "5.2.8", "@gorhom/bottom-sheet": "5.2.8",
"@jellyfin/sdk": "^0.13.0", "@jellyfin/sdk": "^0.13.0",
"@react-native-community/netinfo": "^12.0.0", "@react-native-community/netinfo": "^11.4.1",
"@react-navigation/material-top-tabs": "7.4.9", "@react-navigation/material-top-tabs": "7.4.9",
"@react-navigation/native": "^7.0.14", "@react-navigation/native": "^7.0.14",
"@shopify/flash-list": "2.0.2", "@shopify/flash-list": "2.0.2",
@@ -71,14 +70,14 @@
"expo-system-ui": "~6.0.9", "expo-system-ui": "~6.0.9",
"expo-task-manager": "14.0.9", "expo-task-manager": "14.0.9",
"expo-web-browser": "~15.0.10", "expo-web-browser": "~15.0.10",
"i18next": "^26.0.0", "i18next": "^25.0.0",
"jotai": "2.16.2", "jotai": "2.16.2",
"lodash": "4.17.23", "lodash": "4.17.23",
"nativewind": "^2.0.11", "nativewind": "^2.0.11",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-i18next": "17.0.8", "react-i18next": "16.5.4",
"react-native": "0.81.5", "react-native": "0.81.5",
"react-native-awesome-slider": "^2.9.0", "react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "1.1.0", "react-native-bottom-tabs": "1.1.0",
@@ -118,16 +117,16 @@
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.6", "@babel/core": "7.28.6",
"@biomejs/biome": "2.3.11", "@biomejs/biome": "2.3.11",
"@react-native-community/cli": "20.1.3", "@react-native-community/cli": "20.1.1",
"@react-native-tvos/config-tv": "0.1.6", "@react-native-tvos/config-tv": "0.1.4",
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/lodash": "4.17.23", "@types/lodash": "4.17.23",
"@types/react": "19.1.17", "@types/react": "19.1.17",
"@types/react-test-renderer": "19.1.0", "@types/react-test-renderer": "19.1.0",
"cross-env": "10.1.0", "cross-env": "10.1.0",
"expo-doctor": "1.19.7", "expo-doctor": "1.17.14",
"husky": "9.1.7", "husky": "9.1.7",
"lint-staged": "17.0.5", "lint-staged": "16.2.7",
"react-test-renderer": "19.2.3", "react-test-renderer": "19.2.3",
"typescript": "5.9.3" "typescript": "5.9.3"
}, },

View File

@@ -32,6 +32,12 @@ export interface MediaTimeSegment {
text: string; text: string;
} }
export interface Segment {
startTime: number;
endTime: number;
text: string;
}
/** Represents a single downloaded media item with all necessary metadata for offline playback. */ /** Represents a single downloaded media item with all necessary metadata for offline playback. */
export interface DownloadedItem { export interface DownloadedItem {
/** The Jellyfin item DTO. */ /** The Jellyfin item DTO. */
@@ -50,12 +56,6 @@ export interface DownloadedItem {
introSegments?: MediaTimeSegment[]; introSegments?: MediaTimeSegment[];
/** The credit segments for the item. */ /** The credit segments for the item. */
creditSegments?: MediaTimeSegment[]; creditSegments?: MediaTimeSegment[];
/** The recap segments for the item. */
recapSegments?: MediaTimeSegment[];
/** The commercial segments for the item. */
commercialSegments?: MediaTimeSegment[];
/** The preview segments for the item. */
previewSegments?: MediaTimeSegment[];
/** The user data for the item. */ /** The user data for the item. */
userData: UserData; userData: UserData;
} }
@@ -144,12 +144,6 @@ export type JobStatus = {
introSegments?: MediaTimeSegment[]; introSegments?: MediaTimeSegment[];
/** Pre-downloaded credit segments (optional) - downloaded before video starts */ /** Pre-downloaded credit segments (optional) - downloaded before video starts */
creditSegments?: MediaTimeSegment[]; creditSegments?: MediaTimeSegment[];
/** Pre-downloaded recap segments (optional) - downloaded before video starts */
recapSegments?: MediaTimeSegment[];
/** Pre-downloaded commercial segments (optional) - downloaded before video starts */
commercialSegments?: MediaTimeSegment[];
/** Pre-downloaded preview segments (optional) - downloaded before video starts */
previewSegments?: MediaTimeSegment[];
/** The audio stream index selected for this download */ /** The audio stream index selected for this download */
audioStreamIndex?: number; audioStreamIndex?: number;
/** The subtitle stream index selected for this download */ /** The subtitle stream index selected for this download */

View File

@@ -5,13 +5,10 @@ import {
type ReactNode, type ReactNode,
useCallback, useCallback,
useContext, useContext,
useEffect,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { BackHandler, Platform } from "react-native";
interface ModalOptions { interface ModalOptions {
enableDynamicSizing?: boolean; enableDynamicSizing?: boolean;
snapPoints?: (string | number)[]; snapPoints?: (string | number)[];
@@ -76,25 +73,6 @@ export const GlobalModalProvider: React.FC<GlobalModalProviderProps> = ({
}); });
}, []); }, []);
useEffect(() => {
if (Platform.OS !== "android") return;
const onBackPress = () => {
if (isVisible) {
hideModal();
return true;
}
return false;
};
const subscription = BackHandler.addEventListener(
"hardwareBackPress",
onBackPress,
);
return () => subscription.remove();
}, [isVisible, hideModal]);
const value = { const value = {
showModal, showModal,
hideModal, hideModal,

View File

@@ -7,7 +7,7 @@
"username_placeholder": "Benutzername", "username_placeholder": "Benutzername",
"password_placeholder": "Passwort", "password_placeholder": "Passwort",
"login_button": "Anmelden", "login_button": "Anmelden",
"quick_connect": "Quick Connect", "quick_connect": "Schnellverbindung",
"enter_code_to_login": "Gib den Code {{code}} ein, um dich anzumelden", "enter_code_to_login": "Gib den Code {{code}} ein, um dich anzumelden",
"failed_to_initiate_quick_connect": "Fehler beim Initiieren der Schnellverbindung", "failed_to_initiate_quick_connect": "Fehler beim Initiieren der Schnellverbindung",
"got_it": "Verstanden", "got_it": "Verstanden",
@@ -30,48 +30,48 @@
"connect_button": "Verbinden", "connect_button": "Verbinden",
"previous_servers": "Vorherige Server", "previous_servers": "Vorherige Server",
"clear_button": "Löschen", "clear_button": "Löschen",
"swipe_to_remove": "Wischen, um zu entfernen", "swipe_to_remove": "Swipe to remove",
"search_for_local_servers": "Nach lokalen Servern suchen", "search_for_local_servers": "Nach lokalen Servern suchen",
"searching": "Suche...", "searching": "Suche...",
"servers": "Server", "servers": "Server",
"saved": "Gespeichert", "saved": "Saved",
"session_expired": "Sitzung abgelaufen", "session_expired": "Session Expired",
"please_login_again": "Ihre Sitzung ist abgelaufen. Bitte erneut anmelden.", "please_login_again": "Your saved session has expired. Please log in again.",
"remove_saved_login": "Gespeicherte Zugangsdaten entfernen", "remove_saved_login": "Remove Saved Login",
"remove_saved_login_description": "Hiermit werden ihre gespeicherten Zugangsdaten für diesen Server entfernt. Sie müssen sich dann erneut anmelden.", "remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
"accounts_count": "{{count}} Konten", "accounts_count": "{{count}} accounts",
"select_account": "Konto auswählen", "select_account": "Select Account",
"add_account": "Konto hinzufügen", "add_account": "Add Account",
"remove_account_description": "Hiermit werden die gespeicherten Zugangsdaten für {{username}} entfernt." "remove_account_description": "This will remove the saved credentials for {{username}}."
}, },
"save_account": { "save_account": {
"title": "Konto speichern", "title": "Save Account",
"save_for_later": "Dieses Konto speichern", "save_for_later": "Save this account",
"security_option": "Sicherheitseinstellung", "security_option": "Security Option",
"no_protection": "Keine", "no_protection": "No protection",
"no_protection_desc": "Schnellanmeldung ohne Authentifizierung", "no_protection_desc": "Quick login without authentication",
"pin_code": "PIN", "pin_code": "PIN code",
"pin_code_desc": "4-stellige PIN bei Konto-Wechsel erforderlich", "pin_code_desc": "4-digit PIN required when switching",
"password": "Passwort wiederholen", "password": "Re-enter password",
"password_desc": "Passwort bei Konto-Wechsel erforderlich", "password_desc": "Password required when switching",
"save_button": "Speichern", "save_button": "Save",
"cancel_button": "Abbrechen" "cancel_button": "Cancel"
}, },
"pin": { "pin": {
"enter_pin": "PIN eingeben", "enter_pin": "Enter PIN",
"enter_pin_for": "PIN für {{username}} eingeben", "enter_pin_for": "Enter PIN for {{username}}",
"enter_4_digits": "4 Ziffern eingeben", "enter_4_digits": "Enter 4 digits",
"invalid_pin": "Ungültige PIN", "invalid_pin": "Invalid PIN",
"setup_pin": "PIN festlegen", "setup_pin": "Set Up PIN",
"confirm_pin": "PIN bestätigen", "confirm_pin": "Confirm PIN",
"pins_dont_match": "PIN stimmt nicht überein", "pins_dont_match": "PINs don't match",
"forgot_pin": "PIN vergessen?", "forgot_pin": "Forgot PIN?",
"forgot_pin_desc": "Ihre gespeicherten Zugangsdaten werden entfernt" "forgot_pin_desc": "Your saved credentials will be removed"
}, },
"password": { "password": {
"enter_password": "Passwort eingeben", "enter_password": "Enter Password",
"enter_password_for": "Passwort für {{username}} eingeben", "enter_password_for": "Enter password for {{username}}",
"invalid_password": "Ungültiges Passwort" "invalid_password": "Invalid password"
}, },
"home": { "home": {
"checking_server_connection": "Überprüfe Serververbindung...", "checking_server_connection": "Überprüfe Serververbindung...",
@@ -87,7 +87,7 @@
"error_message": "Etwas ist schiefgelaufen.\nBitte melde dich ab und wieder an.", "error_message": "Etwas ist schiefgelaufen.\nBitte melde dich ab und wieder an.",
"continue_watching": "Weiterschauen", "continue_watching": "Weiterschauen",
"next_up": "Als nächstes", "next_up": "Als nächstes",
"continue_and_next_up": "\"Weiterschauen\" und \"Als Nächstes\"", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}", "recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}",
"suggested_movies": "Empfohlene Filme", "suggested_movies": "Empfohlene Filme",
"suggested_episodes": "Empfohlene Episoden", "suggested_episodes": "Empfohlene Episoden",
@@ -120,36 +120,36 @@
}, },
"appearance": { "appearance": {
"title": "Aussehen", "title": "Aussehen",
"merge_next_up_continue_watching": "\"Weiterschauen\" und \"Als Nächstes\" kombinieren", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Button für Remote-Sitzung ausblenden" "hide_remote_session_button": "Hide Remote Session Button"
}, },
"network": { "network": {
"title": "Netzwerk", "title": "Network",
"local_network": "Lokales Netzwerk", "local_network": "Local Network",
"auto_switch_enabled": "Zuhause automatisch wechseln", "auto_switch_enabled": "Auto-switch when at home",
"auto_switch_description": "Im WLAN Zuhause automatisch zu lokaler URL wechseln", "auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
"local_url": "Lokale URL", "local_url": "Local URL",
"local_url_hint": "Lokale Server-URL eingeben (zB. http://192.168.1.100:8096)", "local_url_hint": "Enter your local server address (e.g., http://192.168.1.100:8096)",
"local_url_placeholder": "http://192.168.1.100:8096", "local_url_placeholder": "http://192.168.1.100:8096",
"home_wifi_networks": "Private WLAN-Netze", "home_wifi_networks": "Home WiFi Networks",
"add_current_network": "{{ssid}} hinzufügen", "add_current_network": "Add \"{{ssid}}\"",
"not_connected_to_wifi": "Nicht mit WLAN verbunden", "not_connected_to_wifi": "Not connected to WiFi",
"no_networks_configured": "Keine Netzwerke konfiguriert", "no_networks_configured": "No networks configured",
"add_network_hint": "Füge dein privates WLAN-Netz hinzu um automatischen Wechsel zu aktivieren", "add_network_hint": "Add your home WiFi network to enable auto-switching",
"current_wifi": "Aktuelles WLAN-Netz", "current_wifi": "Current WiFi",
"using_url": "Verwendet", "using_url": "Using",
"local": "Lokale URL", "local": "Local URL",
"remote": "Remote URL", "remote": "Remote URL",
"not_connected": "Nicht verbunden", "not_connected": "Not connected",
"current_server": "Aktueller Server", "current_server": "Current Server",
"remote_url": "Remote URL", "remote_url": "Remote URL",
"active_url": "Aktive URL", "active_url": "Active URL",
"not_configured": "Nicht konfiguriert", "not_configured": "Not configured",
"network_added": "Netzwerk hinzugefügt", "network_added": "Network added",
"network_already_added": "Netzwerk bereits hinzugefügt", "network_already_added": "Network already added",
"no_wifi_connected": "Nicht mit WLAN verbunden", "no_wifi_connected": "Not connected to WiFi",
"permission_denied": "Standortberechtigung nicht verfügbar", "permission_denied": "Location permission denied",
"permission_denied_explanation": "Standortberechtigung ist nötig um WLAN-Netze für den automatischen Wechsel zu erkennen. Bitte in den Einstellungen aktivieren." "permission_denied_explanation": "Location permission is required to detect WiFi network for auto-switching. Please enable it in Settings."
}, },
"user_info": { "user_info": {
"user_info_title": "Benutzerinformationen", "user_info_title": "Benutzerinformationen",
@@ -159,82 +159,82 @@
"app_version": "App-Version" "app_version": "App-Version"
}, },
"quick_connect": { "quick_connect": {
"quick_connect_title": "Quick Connect", "quick_connect_title": "Schnellverbindung",
"authorize_button": "Quick Connect autorisieren", "authorize_button": "Schnellverbindung autorisieren",
"enter_the_quick_connect_code": "Quick Connect-Code eingeben...", "enter_the_quick_connect_code": "Gib den Schnellverbindungscode ein...",
"success": "Erfolgreich verbunden", "success": "Erfolg",
"quick_connect_autorized": "Quick Connect autorisiert", "quick_connect_autorized": "Schnellverbindung autorisiert",
"error": "Fehler", "error": "Fehler",
"invalid_code": "Ungültiger Code", "invalid_code": "Ungültiger Code",
"authorize": "Autorisieren" "authorize": "Autorisieren"
}, },
"media_controls": { "media_controls": {
"media_controls_title": "Mediensteuerung", "media_controls_title": "Mediensteuerung",
"forward_skip_length": "Vorspullänge", "forward_skip_length": "Vorspulzeit",
"rewind_length": "Rückspullänge", "rewind_length": "Rückspulzeit",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Gestensteuerung", "gesture_controls_title": "Gestensteuerung",
"horizontal_swipe_skip": "Horizontal Wischen zum Überspringen", "horizontal_swipe_skip": "Horizontales Wischen zum Überspringen",
"horizontal_swipe_skip_description": "Wische links/rechts, wenn Steuerelemente ausgeblendet sind um zu überspringen", "horizontal_swipe_skip_description": "Wische links/rechts, wenn Steuerelemente ausgeblendet werden um zu überspringen",
"left_side_brightness": "Helligkeitsregler Links", "left_side_brightness": "Helligkeitskontrolle der linken Seite",
"left_side_brightness_description": "Links nach oben/unten wischen um Helligkeit anzupassen", "left_side_brightness_description": "Wischen Sie auf der linken Seite nach oben/runter, um die Helligkeit anzupassen",
"right_side_volume": "Lautstärkeregler Rechts", "right_side_volume": "Lautstärkeregelung der rechten Seite",
"right_side_volume_description": "Rechts nach oben/unten wischen um Lautstärke anzupassen", "right_side_volume_description": "Auf der rechten Seite nach oben/unten wischen, um Lautstärke anzupassen",
"hide_volume_slider": "Lautstärkeregler ausblenden", "hide_volume_slider": "Hide Volume Slider",
"hide_volume_slider_description": "Lautstärkeregler im Videoplayer ausblenden", "hide_volume_slider_description": "Hide the volume slider in the video player",
"hide_brightness_slider": "Helligkeitsregler ausblenden", "hide_brightness_slider": "Hide Brightness Slider",
"hide_brightness_slider_description": "Helligkeitsregler im Videoplayer ausblenden" "hide_brightness_slider_description": "Hide the brightness slider in the video player"
}, },
"audio": { "audio": {
"audio_title": "Audio", "audio_title": "Audio",
"set_audio_track": "Audiospur aus dem vorherigen Element übernehmen", "set_audio_track": "Audiospur aus dem vorherigen Element festlegen",
"audio_language": "Audio-Sprache", "audio_language": "Audio-Sprache",
"audio_hint": "Standardsprache für Audio auswählen.", "audio_hint": "Wähl die Standardsprache für Audio aus.",
"none": "Keine", "none": "Keine",
"language": "Sprache", "language": "Sprache",
"transcode_mode": { "transcode_mode": {
"title": "Audio-Transcoding", "title": "Audio Transcoding",
"description": "Legt fest, wie Surround-Audio (7.1, TrueHD, DTS-HD) behandelt wird", "description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
"auto": "Auto", "auto": "Auto",
"stereo": "Stereo erzwingen", "stereo": "Force Stereo",
"5_1": "5.1 erlauben", "5_1": "Allow 5.1",
"passthrough": "Passthrough" "passthrough": "Passthrough"
} }
}, },
"subtitles": { "subtitles": {
"subtitle_title": "Untertitel", "subtitle_title": "Untertitel",
"subtitle_hint": "Untertitel-Erscheinungsbild und Verhalten konfigurieren.", "subtitle_hint": "Konfigurier die Untertitel-Präferenzen.",
"subtitle_language": "Untertitel-Sprache", "subtitle_language": "Untertitel-Sprache",
"subtitle_mode": "Untertitel-Modus", "subtitle_mode": "Untertitel-Modus",
"set_subtitle_track": "Untertitel-Spur aus dem vorherigen Element übernehmen", "set_subtitle_track": "Untertitel-Spur aus dem vorherigen Element festlegen",
"subtitle_size": "Untertitel-Größe", "subtitle_size": "Untertitel-Größe",
"none": "Keine", "none": "Keine",
"language": "Sprache", "language": "Sprache",
"loading": "Lädt", "loading": "Lädt",
"modes": { "modes": {
"Default": "Standard", "Default": "Standard",
"Smart": "Smart", "Smart": "Intelligent",
"Always": "Immer", "Always": "Immer",
"None": "Keine", "None": "Keine",
"OnlyForced": "Nur erzwungene" "OnlyForced": "Nur erzwungen"
}, },
"text_color": "Textfarbe", "text_color": "Textfarbe",
"background_color": "Hintergrundfarbe", "background_color": "Hintergrundfarbe",
"outline_color": "Konturfarbe", "outline_color": "Konturfarbe",
"outline_thickness": "Konturdicke", "outline_thickness": "Umriss Dicke",
"background_opacity": "Hintergrundtransparenz", "background_opacity": "Hintergrundtransparenz",
"outline_opacity": "Konturtransparenz", "outline_opacity": "Kontur-Deckkraft",
"bold_text": "Fettgedruckter Text", "bold_text": "Bold Text",
"colors": { "colors": {
"Black": "Schwarz", "Black": "Schwarz",
"Gray": "Grau", "Gray": "Grau",
"Silver": "Silber", "Silver": "Silber",
"White": "Weiß", "White": "Weiß",
"Maroon": "Rotbraun", "Maroon": "Marotte",
"Red": "Rot", "Red": "Rot",
"Fuchsia": "Magenta", "Fuchsia": "Fuchsia",
"Yellow": "Gelb", "Yellow": "Gelb",
"Olive": "Olivgrün", "Olive": "Olivgrün",
"Green": "Grün", "Green": "Grün",
@@ -251,29 +251,29 @@
"Normal": "Normal", "Normal": "Normal",
"Thick": "Dick" "Thick": "Dick"
}, },
"subtitle_color": "Untertitelfarbe", "subtitle_color": "Subtitle Color",
"subtitle_background_color": "Hintergrundfarbe", "subtitle_background_color": "Background Color",
"subtitle_font": "Untertitel-Schriftart", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Einstellungen", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Hardwarebeschleunigung für Video Decoding verwenden. Deaktivieren wenn Wiedergabeprobleme auftreten." "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Untertitel-Einstellungen", "title": "VLC Subtitle Settings",
"hint": "Anpassen des Untertitel-Erscheinungsbildes für VLC. Änderungen werden bei der nächsten Wiedergabe übernommen.", "hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Schriftfarbe", "text_color": "Text Color",
"background_color": "Hintergrundfarbe", "background_color": "Background Color",
"background_opacity": "Hintergrundtransparenz", "background_opacity": "Background Opacity",
"outline_color": "Konturfarbe", "outline_color": "Outline Color",
"outline_opacity": "Konturtransparenz", "outline_opacity": "Outline Opacity",
"outline_thickness": "Konturdicke", "outline_thickness": "Outline Thickness",
"bold": "Fettgedruckter Text", "bold": "Bold Text",
"margin": "Unterer Abstand" "margin": "Bottom Margin"
}, },
"video_player": { "video_player": {
"title": "Videoplayer", "title": "Video Player",
"video_player": "Videoplayer", "video_player": "Video Player",
"video_player_description": "Videoplayer auf iOS auswählen.", "video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer", "ksplayer": "KSPlayer",
"vlc": "VLC" "vlc": "VLC"
}, },
@@ -282,7 +282,7 @@
"video_orientation": "Videoausrichtung", "video_orientation": "Videoausrichtung",
"orientation": "Ausrichtung", "orientation": "Ausrichtung",
"orientations": { "orientations": {
"DEFAULT": "Geräteausrichtung folgen", "DEFAULT": "Standard",
"ALL": "Alle", "ALL": "Alle",
"PORTRAIT": "Hochformat", "PORTRAIT": "Hochformat",
"PORTRAIT_UP": "Hochformat oben", "PORTRAIT_UP": "Hochformat oben",
@@ -294,54 +294,54 @@
"UNKNOWN": "Unbekannt" "UNKNOWN": "Unbekannt"
}, },
"safe_area_in_controls": "Sicherer Bereich in den Steuerungen", "safe_area_in_controls": "Sicherer Bereich in den Steuerungen",
"video_player": "Videoplayer", "video_player": "Video player",
"video_players": { "video_players": {
"VLC_3": "VLC 3", "VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimentell + PiP)" "VLC_4": "VLC 4 (Experimentell + PiP)"
}, },
"show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen", "show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen",
"show_large_home_carousel": "Zeige große Startseiten-Übersicht (Beta)", "show_large_home_carousel": "Zeige Großes Heimkarussell (Beta)",
"hide_libraries": "Bibliotheken ausblenden", "hide_libraries": "Bibliotheken ausblenden",
"select_liraries_you_want_to_hide": "Bibliotheken auswählen die aus dem Bibliothekstab und der Startseite ausgeblendet werden sollen.", "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",
"default_playback_speed": "Standard-Wiedergabegeschwindigkeit", "default_playback_speed": "Default Playback Speed",
"auto_play_next_episode": "Automatisch nächste Episode abspielen", "auto_play_next_episode": "Auto-play Next Episode",
"max_auto_play_episode_count": "Maximale automatisch abzuspielende Episodenanzahl", "max_auto_play_episode_count": "Max. automatische Wiedergabe Episodenanzahl",
"disabled": "Deaktiviert" "disabled": "Deaktiviert"
}, },
"downloads": { "downloads": {
"downloads_title": "Downloads" "downloads_title": "Downloads"
}, },
"music": { "music": {
"title": "Musik", "title": "Music",
"playback_title": "Wiedergabe", "playback_title": "Playback",
"playback_description": "Konfigurieren, wie Musik abgespielt wird.", "playback_description": "Configure how music is played.",
"prefer_downloaded": "Bevorzuge heruntergeladene Titel", "prefer_downloaded": "Prefer Downloaded Songs",
"caching_title": "Caching", "caching_title": "Caching",
"caching_description": "Automatisches Caching anstehender Titel für bessere Wiedergabe.", "caching_description": "Automatically cache upcoming tracks for smoother playback.",
"lookahead_enabled": "Look-Ahead Caching aktivieren", "lookahead_enabled": "Enable Look-Ahead Caching",
"lookahead_count": "Titel vorher in den Cache laden", "lookahead_count": "Tracks to Pre-cache",
"max_cache_size": "Maximale Cache-Größe" "max_cache_size": "Max Cache Size"
}, },
"plugins": { "plugins": {
"plugins_title": "Plugins", "plugins_title": "Erweiterungen",
"jellyseerr": { "jellyseerr": {
"jellyseerr_warning": "Diese Integration ist in einer frühen Entwicklungsphase und kann jederzeit geändert werden.", "jellyseerr_warning": "Diese integration ist in einer frühen Entwicklungsphase. Erwarte Veränderungen.",
"server_url": "Server URL", "server_url": "Server Adresse",
"server_url_hint": "Beispiel: http(s)://your-host.url\n(Port hinzufügen, falls erforderlich)", "server_url_hint": "Beispiel: http(s)://your-host.url\n(Portnummer hinzufügen, falls erforderlich)",
"server_url_placeholder": "Seerr URL", "server_url_placeholder": "Jellyseerr URL...",
"password": "Passwort", "password": "Passwort",
"password_placeholder": "Passwort für Jellyfin Benutzer {{username}} eingeben", "password_placeholder": "Passwort für Jellyfin Benutzer {{username}} eingeben",
"login_button": "Anmelden", "login_button": "Anmelden",
"total_media_requests": "Gesamtanfragen", "total_media_requests": "Gesamtanfragen",
"movie_quota_limit": "Film-Anfragelimit", "movie_quota_limit": "Film-Anfragelimit",
"movie_quota_days": "Film-Anfragetagelimit", "movie_quota_days": "Film-Anfragetage",
"tv_quota_limit": "Serien-Anfragelimit", "tv_quota_limit": "TV-Anfragelimit",
"tv_quota_days": "Serien-Anfragetagelimit", "tv_quota_days": "TV-Anfragetage",
"reset_jellyseerr_config_button": "Seerr-Konfiguration zurücksetzen", "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück",
"unlimited": "Unlimitiert", "unlimited": "Unlimitiert",
"plus_n_more": "+{{n}} weitere", "plus_n_more": "+{{n}} more",
"order_by": { "order_by": {
"DEFAULT": "Standard", "DEFAULT": "Standard",
"VOTE_COUNT_AND_AVERAGE": "Stimmenanzahl und Durchschnitt", "VOTE_COUNT_AND_AVERAGE": "Stimmenanzahl und Durchschnitt",
@@ -352,71 +352,71 @@
"enable_marlin_search": "Aktiviere Marlin Search", "enable_marlin_search": "Aktiviere Marlin Search",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://domain.org:port", "server_url_placeholder": "http(s)://domain.org:port",
"marlin_search_hint": "URL für den Marlin Server eingeben. Die URL sollte http oder https enthalten und optional den Port.", "marlin_search_hint": "Gib die URL für den Marlin Server ein. Die URL sollte http oder https enthalten und optional den Port.",
"read_more_about_marlin": "Erfahre mehr über Marlin.", "read_more_about_marlin": "Erfahre mehr über Marlin.",
"save_button": "Speichern", "save_button": "Speichern",
"toasts": { "toasts": {
"saved": "Gespeichert", "saved": "Gespeichert",
"refreshed": "Einstellungen vom Server aktualisiert" "refreshed": "Settings refreshed from server"
}, },
"refresh_from_server": "Einstellungen vom Server aktualisieren" "refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Streamystats aktivieren", "enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Streamystats deaktivieren", "disable_streamystats": "Disable Streamystats",
"enable_search": "Zum Suchen verwenden", "enable_search": "Use for Search",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "URL für den Streamystats-Server eingeben.", "streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
"read_more_about_streamystats": "Mehr über Streamystats erfahren.", "read_more_about_streamystats": "Read More About Streamystats.",
"save_button": "Speichern", "save_button": "Save",
"save": "Gespeichert", "save": "Save",
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Startseitenbereiche", "home_sections_title": "Home Sections",
"enable_movie_recommendations": "Filmempfehlungen", "enable_movie_recommendations": "Movie Recommendations",
"enable_series_recommendations": "Serienempfehlungen", "enable_series_recommendations": "Series Recommendations",
"enable_promoted_watchlists": "Empfohlene Merklisten", "enable_promoted_watchlists": "Promoted Watchlists",
"hide_watchlists_tab": "Merklisten-Tab ausblenden", "hide_watchlists_tab": "Hide Watchlists Tab",
"home_sections_hint": "Zeige personalisierte Empfehlungen und empfohlene Merklisten von Streamystats auf der Startseite.", "home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.",
"recommended_movies": "Empfohlene Filme", "recommended_movies": "Recommended Movies",
"recommended_series": "Empfohlene Serien", "recommended_series": "Recommended Series",
"toasts": { "toasts": {
"saved": "Gespeichert", "saved": "Saved",
"refreshed": "Einstellungen vom Server aktualisiert", "refreshed": "Settings refreshed from server",
"disabled": "Streamystats deaktiviert" "disabled": "Streamystats disabled"
}, },
"refresh_from_server": "Einstellungen vom Server aktualisieren" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Merklisten-Integration aktivieren", "watchlist_enabler": "Enable our Watchlist integration",
"watchlist_button": "Merklisten-Integration umschalten" "watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
"storage_title": "Speicher", "storage_title": "Speicher",
"app_usage": "App {{usedSpace}}%", "app_usage": "App {{usedSpace}}%",
"device_usage": "Gerät {{availableSpace}}%", "device_usage": "Gerät {{availableSpace}}%",
"size_used": "{{used}} von {{total}} genutzt", "size_used": "{{used}} von {{total}} benutzt",
"delete_all_downloaded_files": "Alle heruntergeladenen Dateien löschen", "delete_all_downloaded_files": "Alle Downloads löschen",
"music_cache_title": "Musik-Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Beim Anhören Titel automatisch in den Cache laden um bessere Wiedergabe und Offline-Wiedergabe zu ermöglichen", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Musik-Cache aktivieren", "enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Musik-Cache leeren", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} gechached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Musik-Cache geleert", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Alle heruntergeladenen Titel löschen", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} heruntergeladen", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Heruntergeladene Titel gelöscht" "downloaded_songs_deleted": "Downloaded songs deleted"
}, },
"intro": { "intro": {
"title": "Einführung", "title": "Intro ",
"show_intro": "Einführung anzeigen", "show_intro": "Show intro",
"reset_intro": "Einführung zurücksetzen" "reset_intro": "Reset intro"
}, },
"logs": { "logs": {
"logs_title": "Logs", "logs_title": "Logs",
"export_logs": "Logs exportieren", "export_logs": "Export logs",
"click_for_more_info": "Für mehr Informationen klicken", "click_for_more_info": "Click for more info",
"level": "Level", "level": "Level",
"no_logs_available": "Keine Logs verfügbar", "no_logs_available": "Keine Logs verfügbar",
"delete_all_logs": "Alle Logs löschen" "delete_all_logs": "Alle Logs löschen"
@@ -438,21 +438,21 @@
}, },
"downloads": { "downloads": {
"downloads_title": "Downloads", "downloads_title": "Downloads",
"tvseries": "Serien", "tvseries": "TV-Serien",
"movies": "Filme", "movies": "Filme",
"queue": "Warteschlange", "queue": "Warteschlange",
"other_media": "Andere Medien", "other_media": "Andere Medien",
"queue_hint": "Warteschlange und aktive Downloads gehen verloren wenn die App neu gestartet wird", "queue_hint": "Warteschlange und aktive Downloads gehen verloren bei App-Neustart",
"no_items_in_queue": "Keine Elemente in der Warteschlange", "no_items_in_queue": "Keine Elemente in der Warteschlange",
"no_downloaded_items": "Keine heruntergeladenen Elemente", "no_downloaded_items": "Keine heruntergeladenen Elemente",
"delete_all_movies_button": "Alle Filme löschen", "delete_all_movies_button": "Alle Filme löschen",
"delete_all_tvseries_button": "Alle Serien löschen", "delete_all_tvseries_button": "Alle TV-Serien löschen",
"delete_all_button": "Alles löschen", "delete_all_button": "Alles löschen",
"delete_all_other_media_button": "Alle anderen Medien löschen", "delete_all_other_media_button": "Andere Medien löschen",
"active_download": "Aktiver Download", "active_download": "Aktiver Download",
"no_active_downloads": "Keine aktiven Downloads", "no_active_downloads": "Keine aktiven Downloads",
"active_downloads": "Aktive Downloads", "active_downloads": "Aktive Downloads",
"new_app_version_requires_re_download": "Neue App-Version erfordert erneutes Herunterladen", "new_app_version_requires_re_download": "Die neue App-Version erfordert das erneute Herunterladen.",
"new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.", "new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.",
"back": "Zurück", "back": "Zurück",
"delete": "Löschen", "delete": "Löschen",
@@ -463,8 +463,8 @@
"you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen", "you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen",
"deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!", "deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!",
"failed_to_delete_all_movies": "Fehler beim Löschen aller Filme", "failed_to_delete_all_movies": "Fehler beim Löschen aller Filme",
"deleted_all_tvseries_successfully": "Alle Serien erfolgreich gelöscht!", "deleted_all_tvseries_successfully": "Alle TV-Serien erfolgreich gelöscht!",
"failed_to_delete_all_tvseries": "Fehler beim Löschen aller Serien", "failed_to_delete_all_tvseries": "Fehler beim Löschen aller TV-Serien",
"deleted_media_successfully": "Andere Medien erfolgreich gelöscht!", "deleted_media_successfully": "Andere Medien erfolgreich gelöscht!",
"failed_to_delete_media": "Fehler beim Löschen anderer Medien", "failed_to_delete_media": "Fehler beim Löschen anderer Medien",
"download_deleted": "Download gelöscht", "download_deleted": "Download gelöscht",
@@ -486,7 +486,7 @@
"all_files_folders_and_jobs_deleted_successfully": "Alle Dateien, Ordner und Jobs erfolgreich gelöscht", "all_files_folders_and_jobs_deleted_successfully": "Alle Dateien, Ordner und Jobs erfolgreich gelöscht",
"failed_to_clean_cache_directory": "Fehler beim Bereinigen des Cache-Verzeichnisses", "failed_to_clean_cache_directory": "Fehler beim Bereinigen des Cache-Verzeichnisses",
"could_not_get_download_url_for_item": "Download-URL für {{itemName}} konnte nicht geladen werden", "could_not_get_download_url_for_item": "Download-URL für {{itemName}} konnte nicht geladen werden",
"go_to_downloads": "Zu Downloads gehen", "go_to_downloads": "Gehe zu den Downloads",
"file_deleted": "{{item}} gelöscht" "file_deleted": "{{item}} gelöscht"
} }
} }
@@ -499,18 +499,18 @@
"subtitle": "Untertitel", "subtitle": "Untertitel",
"play": "Abspielen", "play": "Abspielen",
"none": "Keine", "none": "Keine",
"track": "Spur", "track": "Track",
"cancel": "Abbrechen", "cancel": "Cancel",
"delete": "Löschen", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Entfernen", "remove": "Remove",
"next": "Weiter", "next": "Next",
"back": "Zurück", "back": "Back",
"continue": "Fortsetzen", "continue": "Continue",
"verifying": "Verifiziere..." "verifying": "Verifying..."
}, },
"search": { "search": {
"search": "Suchen...", "search": "Suche...",
"x_items": "{{count}} Elemente", "x_items": "{{count}} Elemente",
"library": "Bibliothek", "library": "Bibliothek",
"discover": "Entdecken", "discover": "Entdecken",
@@ -521,33 +521,33 @@
"episodes": "Episoden", "episodes": "Episoden",
"collections": "Sammlungen", "collections": "Sammlungen",
"actors": "Schauspieler", "actors": "Schauspieler",
"artists": "Künstler", "artists": "Artists",
"albums": "Alben", "albums": "Albums",
"songs": "Titel", "songs": "Songs",
"playlists": "Playlists", "playlists": "Playlists",
"request_movies": "Film anfragen", "request_movies": "Film anfragen",
"request_series": "Serie anfragen", "request_series": "Serie anfragen",
"recently_added": "Kürzlich hinzugefügt", "recently_added": "Kürzlich hinzugefügt",
"recent_requests": "Kürzlich angefragt", "recent_requests": "Kürzlich angefragt",
"plex_watchlist": "Plex Merkliste", "plex_watchlist": "Plex Watchlist",
"trending": "Beliebt", "trending": "In den Trends",
"popular_movies": "Beliebte Filme", "popular_movies": "Beliebte Filme",
"movie_genres": "Film-Genres", "movie_genres": "Film-Genres",
"upcoming_movies": "Kommende Filme", "upcoming_movies": "Kommende Filme",
"studios": "Studios", "studios": "Studios",
"popular_tv": "Beliebte Serien", "popular_tv": "Beliebte TV-Serien",
"tv_genres": "Serien-Genres", "tv_genres": "TV-Serien-Genres",
"upcoming_tv": "Kommende Serien", "upcoming_tv": "Kommende TV-Serien",
"networks": "Sender", "networks": "Netzwerke",
"tmdb_movie_keyword": "TMDB Film-Schlüsselwort", "tmdb_movie_keyword": "TMDB Film-Schlüsselwort",
"tmdb_movie_genre": "TMDB Film-Genre", "tmdb_movie_genre": "TMDB Film-Genre",
"tmdb_tv_keyword": "TMDB Serien-Schlüsselwort", "tmdb_tv_keyword": "TMDB TV-Serien-Schlüsselwort",
"tmdb_tv_genre": "TMDB Serien-Genre", "tmdb_tv_genre": "TMDB TV-Serien-Genre",
"tmdb_search": "TMDB Suche", "tmdb_search": "TMDB Suche",
"tmdb_studio": "TMDB Studio", "tmdb_studio": "TMDB Studio",
"tmdb_network": "TMDB Netzwerk", "tmdb_network": "TMDB Netzwerk",
"tmdb_movie_streaming_services": "TMDB Film-Streaming-Dienste", "tmdb_movie_streaming_services": "TMDB Film-Streaming-Dienste",
"tmdb_tv_streaming_services": "TMDB Serien-Streaming-Dienste" "tmdb_tv_streaming_services": "TMDB TV-Serien-Streaming-Dienste"
}, },
"library": { "library": {
"no_results": "Keine Ergebnisse", "no_results": "Keine Ergebnisse",
@@ -572,7 +572,7 @@
"genres": "Genres", "genres": "Genres",
"years": "Jahre", "years": "Jahre",
"sort_by": "Sortieren nach", "sort_by": "Sortieren nach",
"filter_by": "Filtern nach", "filter_by": "Filter By",
"sort_order": "Sortierreihenfolge", "sort_order": "Sortierreihenfolge",
"tags": "Tags" "tags": "Tags"
} }
@@ -585,7 +585,7 @@
"boxsets": "Boxsets", "boxsets": "Boxsets",
"playlists": "Wiedergabelisten", "playlists": "Wiedergabelisten",
"noDataTitle": "Noch keine Favoriten", "noDataTitle": "Noch keine Favoriten",
"noData": "Elemente als Favoriten markieren, um sie hier anzuzeigen." "noData": "Markiere Elemente als Favoriten, damit sie hier für einen schnellen Zugriff angezeigt werden."
}, },
"custom_links": { "custom_links": {
"no_links": "Keine Links" "no_links": "Keine Links"
@@ -593,7 +593,7 @@
"player": { "player": {
"error": "Fehler", "error": "Fehler",
"failed_to_get_stream_url": "Fehler beim Abrufen der Stream-URL", "failed_to_get_stream_url": "Fehler beim Abrufen der Stream-URL",
"an_error_occured_while_playing_the_video": "Ein Fehler ist beim Abspielen des Videos aufgetreten. Logs in den Einstellungen überprüfen.", "an_error_occured_while_playing_the_video": "Ein Fehler ist beim Abspielen des Videos aufgetreten. Überprüf die Logs in den Einstellungen.",
"client_error": "Client-Fehler", "client_error": "Client-Fehler",
"could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen", "could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen",
"message_from_server": "Nachricht vom Server: {{message}}", "message_from_server": "Nachricht vom Server: {{message}}",
@@ -602,17 +602,17 @@
"audio_tracks": "Audiospuren:", "audio_tracks": "Audiospuren:",
"playback_state": "Wiedergabestatus:", "playback_state": "Wiedergabestatus:",
"index": "Index:", "index": "Index:",
"continue_watching": "Fortsetzen", "continue_watching": "Weiterschauen",
"go_back": "Zurück", "go_back": "Zurück",
"downloaded_file_title": "Diese Datei wurde bereits heruntergeladen", "downloaded_file_title": "Diese Datei wurde heruntergeladen",
"downloaded_file_message": "Heruntergeladene Datei abspielen?", "downloaded_file_message": "Möchten Sie die heruntergeladene Datei abspielen?",
"downloaded_file_yes": "Ja", "downloaded_file_yes": "Ja",
"downloaded_file_no": "Nein", "downloaded_file_no": "Nein",
"downloaded_file_cancel": "Abbrechen" "downloaded_file_cancel": "Abbrechen"
}, },
"item_card": { "item_card": {
"next_up": "Als Nächstes", "next_up": "Als Nächstes",
"no_items_to_display": "Keine Elemente", "no_items_to_display": "Keine Elemente zum Anzeigen",
"cast_and_crew": "Besetzung und Crew", "cast_and_crew": "Besetzung und Crew",
"series": "Serien", "series": "Serien",
"seasons": "Staffeln", "seasons": "Staffeln",
@@ -630,7 +630,7 @@
"subtitles": "Untertitel", "subtitles": "Untertitel",
"show_more": "Mehr anzeigen", "show_more": "Mehr anzeigen",
"show_less": "Weniger anzeigen", "show_less": "Weniger anzeigen",
"appeared_in": "Erschien in", "appeared_in": "Erschienen in",
"could_not_load_item": "Konnte Element nicht laden", "could_not_load_item": "Konnte Element nicht laden",
"none": "Keine", "none": "Keine",
"download": { "download": {
@@ -639,13 +639,13 @@
"download_episode": "Episode herunterladen", "download_episode": "Episode herunterladen",
"download_movie": "Film herunterladen", "download_movie": "Film herunterladen",
"download_x_item": "{{item_count}} Elemente herunterladen", "download_x_item": "{{item_count}} Elemente herunterladen",
"download_unwatched_only": "Nur Ungesehene", "download_unwatched_only": "Nur unbeobachtete",
"download_button": "Herunterladen" "download_button": "Herunterladen"
} }
}, },
"live_tv": { "live_tv": {
"next": "Nächste", "next": "Nächster",
"previous": "Vorherige", "previous": "Vorheriger",
"coming_soon": "Demnächst", "coming_soon": "Demnächst",
"on_now": "Jetzt", "on_now": "Jetzt",
"shows": "Serien", "shows": "Serien",
@@ -658,10 +658,10 @@
"confirm": "Bestätigen", "confirm": "Bestätigen",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"yes": "Ja", "yes": "Ja",
"whats_wrong": "Was stimmt nicht?", "whats_wrong": "Hast du Probleme?",
"issue_type": "Art des Problems", "issue_type": "Fehlerart",
"select_an_issue": "Wähle die Art des Problems aus", "select_an_issue": "Wähle einen Fehlerart aus",
"types": "Problem-Arten", "types": "Arten",
"describe_the_issue": "(optional) Beschreibe das Problem", "describe_the_issue": "(optional) Beschreibe das Problem",
"submit_button": "Absenden", "submit_button": "Absenden",
"report_issue_button": "Fehler melden", "report_issue_button": "Fehler melden",
@@ -671,7 +671,7 @@
"cast": "Besetzung", "cast": "Besetzung",
"details": "Details", "details": "Details",
"status": "Status", "status": "Status",
"original_title": "Originaltitel", "original_title": "Original Titel",
"series_type": "Serien Typ", "series_type": "Serien Typ",
"release_dates": "Veröffentlichungsdaten", "release_dates": "Veröffentlichungsdaten",
"first_air_date": "Erstausstrahlungsdatum", "first_air_date": "Erstausstrahlungsdatum",
@@ -687,10 +687,10 @@
"request_as": "Anfragen als", "request_as": "Anfragen als",
"tags": "Tags", "tags": "Tags",
"quality_profile": "Qualitätsprofil", "quality_profile": "Qualitätsprofil",
"root_folder": "Stammverzeichnis", "root_folder": "Root-Ordner",
"season_all": "Staffeln (alle)", "season_all": "Season (all)",
"season_number": "Staffel {{season_number}}", "season_number": "Staffel {{season_number}}",
"number_episodes": "{{episode_number}} Episoden", "number_episodes": "{{episode_number}} Folgen",
"born": "Geboren", "born": "Geboren",
"appearances": "Auftritte", "appearances": "Auftritte",
"approve": "Genehmigen", "approve": "Genehmigen",
@@ -698,9 +698,9 @@
"requested_by": "Angefragt von {{user}}", "requested_by": "Angefragt von {{user}}",
"unknown_user": "Unbekannter Nutzer", "unknown_user": "Unbekannter Nutzer",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Seerr-Server erfüllt nicht die minimalen Versionsanforderungen. Bitte den Seerr-Server auf mindestens 2.0.0 aktualisieren.", "jellyseer_does_not_meet_requirements": "Jellyseerr Server erfüllt nicht die Anforderungsversion. Bitte aktualisiere deinen Jellyseerr Server auf mindestens 2.0.0",
"jellyseerr_test_failed": "Seerr-Test fehlgeschlagen. Bitte erneut versuchen.", "jellyseerr_test_failed": "Jellyseerr-Test fehlgeschlagen. Bitte versuche es erneut.",
"failed_to_test_jellyseerr_server_url": "Fehler beim Test der Seerr-Server-URL", "failed_to_test_jellyseerr_server_url": "Fehler beim Testen der Jellyseerr-Server-URL",
"issue_submitted": "Problem eingereicht!", "issue_submitted": "Problem eingereicht!",
"requested_item": "{{item}} angefragt!", "requested_item": "{{item}} angefragt!",
"you_dont_have_permission_to_request": "Du hast keine Berechtigung Anfragen zu stellen", "you_dont_have_permission_to_request": "Du hast keine Berechtigung Anfragen zu stellen",
@@ -715,131 +715,131 @@
"home": "Startseite", "home": "Startseite",
"search": "Suche", "search": "Suche",
"library": "Bibliothek", "library": "Bibliothek",
"custom_links": "Links", "custom_links": "Benutzerdefinierte Links",
"favorites": "Favoriten" "favorites": "Favoriten"
}, },
"music": { "music": {
"title": "Musik", "title": "Music",
"tabs": { "tabs": {
"suggestions": "Vorschläge", "suggestions": "Suggestions",
"albums": "Alben", "albums": "Albums",
"artists": "Künstler", "artists": "Artists",
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "Titel" "tracks": "tracks"
}, },
"filters": { "filters": {
"all": "Alle" "all": "All"
}, },
"recently_added": "Kürzlich hinzugefügt", "recently_added": "Recently Added",
"recently_played": "Vor kurzem gehört", "recently_played": "Recently Played",
"frequently_played": "Oft gehört", "frequently_played": "Frequently Played",
"explore": "Entdecken", "explore": "Explore",
"top_tracks": "Top-Titel", "top_tracks": "Top Tracks",
"play": "Abspielen", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
"play_top_tracks": "Top-Tracks abspielen", "play_top_tracks": "Play Top Tracks",
"no_suggestions": "Keine Vorschläge verfügbar", "no_suggestions": "No suggestions available",
"no_albums": "Keine Alben gefunden", "no_albums": "No albums found",
"no_artists": "Keine Künstler gefunden", "no_artists": "No artists found",
"no_playlists": "Keine Playlists gefunden", "no_playlists": "No playlists found",
"album_not_found": "Album nicht gefunden", "album_not_found": "Album not found",
"artist_not_found": "Künstler nicht gefunden", "artist_not_found": "Artist not found",
"playlist_not_found": "Playlist nicht gefunden", "playlist_not_found": "Playlist not found",
"track_options": { "track_options": {
"play_next": "Als Nächstes wiedergeben", "play_next": "Play Next",
"add_to_queue": "Zur Warteschlange hinzufügen", "add_to_queue": "Add to Queue",
"add_to_playlist": "Zur Playlist hinzufügen", "add_to_playlist": "Add to Playlist",
"download": "Herunterladen", "download": "Download",
"downloaded": "Heruntergeladen", "downloaded": "Downloaded",
"downloading": "Wird heruntergeladen...", "downloading": "Downloading...",
"cached": "Gecached", "cached": "Cached",
"delete_download": "Download löschen", "delete_download": "Delete Download",
"delete_cache": "Aus dem Cache löschen", "delete_cache": "Remove from Cache",
"go_to_artist": "Zum Künstler gehen", "go_to_artist": "Go to Artist",
"go_to_album": "Zum Album gehen", "go_to_album": "Go to Album",
"add_to_favorites": "Zu Favoriten hinzufügen", "add_to_favorites": "Add to Favorites",
"remove_from_favorites": "Aus Favoriten entfernen", "remove_from_favorites": "Remove from Favorites",
"remove_from_playlist": "Aus Playlist entfernen" "remove_from_playlist": "Remove from Playlist"
}, },
"playlists": { "playlists": {
"create_playlist": "Playlist erstellen", "create_playlist": "Create Playlist",
"playlist_name": "Playlist Name", "playlist_name": "Playlist Name",
"enter_name": "Playlist Name eingeben", "enter_name": "Enter playlist name",
"create": "Erstellen", "create": "Create",
"search_playlists": "Playlisten durchsuchen...", "search_playlists": "Search playlists...",
"added_to": "Zu {{name}} hinzugefügt", "added_to": "Added to {{name}}",
"added": "Zur Playlist hinzugefügt", "added": "Added to playlist",
"removed_from": "Aus {{name}} entfernt", "removed_from": "Removed from {{name}}",
"removed": "Aus Playlist entfernt", "removed": "Removed from playlist",
"created": "Playlist erstellt", "created": "Playlist created",
"create_new": "Neue Playlist erstellen", "create_new": "Create New Playlist",
"failed_to_add": "Fehler beim Hinzufügen zur Playlist", "failed_to_add": "Failed to add to playlist",
"failed_to_remove": "Fehler beim Entfernen aus der Playlist", "failed_to_remove": "Failed to remove from playlist",
"failed_to_create": "Fehler beim Erstellen der Playlist", "failed_to_create": "Failed to create playlist",
"delete_playlist": "Playlist löschen", "delete_playlist": "Delete Playlist",
"delete_confirm": "Bist Du sicher, dass Du \"{{name}}\" löschen möchtest? Das kann nicht rückgängig gemacht werden.", "delete_confirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"deleted": "Playlist gelöscht", "deleted": "Playlist deleted",
"failed_to_delete": "Fehler beim Löschen der Playlist" "failed_to_delete": "Failed to delete playlist"
}, },
"sort": { "sort": {
"title": "Sortieren nach", "title": "Sort By",
"alphabetical": "Alphabetisch", "alphabetical": "Alphabetical",
"date_created": "Erstellungsdatum" "date_created": "Date Created"
} }
}, },
"watchlists": { "watchlists": {
"title": "Merklisten", "title": "Watchlists",
"my_watchlists": "Meine Merklisten", "my_watchlists": "My Watchlists",
"public_watchlists": "Öffentliche Merklisten", "public_watchlists": "Public Watchlists",
"create_title": "Merkliste erstellen", "create_title": "Create Watchlist",
"edit_title": "Merkliste bearbeiten", "edit_title": "Edit Watchlist",
"create_button": "Merkliste erstellen", "create_button": "Create Watchlist",
"save_button": "Änderungen speichern", "save_button": "Save Changes",
"delete_button": "Löschen", "delete_button": "Delete",
"remove_button": "Entfernen", "remove_button": "Remove",
"cancel_button": "Abbrechen", "cancel_button": "Cancel",
"name_label": "Name", "name_label": "Name",
"name_placeholder": "Merklistenname eingeben", "name_placeholder": "Enter watchlist name",
"description_label": "Beschreibung", "description_label": "Description",
"description_placeholder": "Beschreibung eingeben (optional)", "description_placeholder": "Enter description (optional)",
"is_public_label": "Öffentliche Merkliste", "is_public_label": "Public Watchlist",
"is_public_description": "Anderen erlauben diese Merkliste anzusehen", "is_public_description": "Allow others to view this watchlist",
"allowed_type_label": "Inhaltstyp", "allowed_type_label": "Content Type",
"sort_order_label": "Standard-Sortierreihenfolge", "sort_order_label": "Default Sort Order",
"empty_title": "Keine Merklisten", "empty_title": "No Watchlists",
"empty_description": "Erstelle deine erste Merkliste um deine Medien zu organisieren", "empty_description": "Create your first watchlist to start organizing your media",
"empty_watchlist": "Diese Merkliste ist leer", "empty_watchlist": "This watchlist is empty",
"empty_watchlist_hint": "Füge Elemente aus deiner Bibliothek zu dieser Merkliste hinzu", "empty_watchlist_hint": "Add items from your library to this watchlist",
"not_configured_title": "Streamystats nicht konfiguriert", "not_configured_title": "Streamystats Not Configured",
"not_configured_description": "Streamystats in den Einstellungen konfigurieren, um Merklisten zu verwenden", "not_configured_description": "Configure Streamystats in settings to use watchlists",
"go_to_settings": "Gehe zu Einstellungen", "go_to_settings": "Go to Settings",
"add_to_watchlist": "Zur Merkliste hinzufügen", "add_to_watchlist": "Add to Watchlist",
"remove_from_watchlist": "Von Merkliste entfernen", "remove_from_watchlist": "Remove from Watchlist",
"select_watchlist": "Merkliste auswählen", "select_watchlist": "Select Watchlist",
"create_new": "Neue Merkliste erstellen", "create_new": "Create New Watchlist",
"item": "Element", "item": "item",
"items": "Elemente", "items": "items",
"public": "Öffentlich", "public": "Public",
"private": "Privat", "private": "Private",
"you": "Du", "you": "You",
"by_owner": "Von einem anderen Benutzer", "by_owner": "By another user",
"not_found": "Merkliste nicht gefunden", "not_found": "Watchlist not found",
"delete_confirm_title": "Merkliste löschen", "delete_confirm_title": "Delete Watchlist",
"delete_confirm_message": "Bist Du sicher, dass Du \"{{name}}\" löschen möchtest? Das kann nicht rückgängig gemacht werden.", "delete_confirm_message": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"remove_item_title": "Von Merkliste entfernen", "remove_item_title": "Remove from Watchlist",
"remove_item_message": "\"{{name}}\" von dieser Merkliste entfernen?", "remove_item_message": "Remove \"{{name}}\" from this watchlist?",
"loading": "Lade Merklisten...", "loading": "Loading watchlists...",
"no_compatible_watchlists": "Keine kompatiblen Merklisten", "no_compatible_watchlists": "No compatible watchlists",
"create_one_first": "Erstelle eine Merkliste, welche diesen Inhaltstyp akzeptiert" "create_one_first": "Create a watchlist that accepts this content type"
}, },
"playback_speed": { "playback_speed": {
"title": "Wiedergabegeschwindigkeit", "title": "Playback Speed",
"apply_to": "Anwenden auf", "apply_to": "Apply To",
"speed": "Geschwindigkeit", "speed": "Speed",
"scope": { "scope": {
"media": "Nur hier", "media": "This media only",
"show": "Nur diese Serie", "show": "This show",
"all": "Alle (Standard)" "all": "All media (default)"
} }
} }
} }

View File

@@ -24,31 +24,6 @@
"too_old_server_text": "Unsupported Jellyfin Server Discovered", "too_old_server_text": "Unsupported Jellyfin Server Discovered",
"too_old_server_description": "Please update Jellyfin to the latest version" "too_old_server_description": "Please update Jellyfin to the latest version"
}, },
"player": {
"skip_intro": "Skip Intro",
"skip_outro": "Skip Outro",
"skip_recap": "Skip Recap",
"skip_commercial": "Skip Commercial",
"skip_preview": "Skip Preview",
"error": "Error",
"failed_to_get_stream_url": "Failed to get the stream URL",
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
"client_error": "Client Error",
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}",
"next_episode": "Next Episode",
"refresh_tracks": "Refresh Tracks",
"audio_tracks": "Audio Tracks:",
"playback_state": "Playback State:",
"index": "Index:",
"continue_watching": "Continue Watching",
"go_back": "Go Back",
"downloaded_file_title": "You have this file downloaded",
"downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes",
"downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel"
},
"server": { "server": {
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server", "enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",
"server_url_placeholder": "http(s)://your-server.com", "server_url_placeholder": "http(s)://your-server.com",
@@ -333,21 +308,6 @@
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
"auto_play_next_episode": "Auto-play Next Episode", "auto_play_next_episode": "Auto-play Next Episode",
"max_auto_play_episode_count": "Max Auto Play Episode Count", "max_auto_play_episode_count": "Max Auto Play Episode Count",
"segment_skip_settings": "Segment Skip Settings",
"segment_skip_settings_description": "Configure skip behavior for intros, credits, and other segments",
"skip_intro": "Skip Intro",
"skip_intro_description": "Action when intro segment is detected",
"skip_outro": "Skip Outro/Credits",
"skip_outro_description": "Action when outro/credits segment is detected",
"skip_recap": "Skip Recap",
"skip_recap_description": "Action when recap segment is detected",
"skip_commercial": "Skip Commercial",
"skip_commercial_description": "Action when commercial segment is detected",
"skip_preview": "Skip Preview",
"skip_preview_description": "Action when preview segment is detected",
"segment_skip_none": "None",
"segment_skip_ask": "Show Skip Button",
"segment_skip_auto": "Auto Skip",
"disabled": "Disabled" "disabled": "Disabled"
}, },
"downloads": { "downloads": {
@@ -630,6 +590,26 @@
"custom_links": { "custom_links": {
"no_links": "No Links" "no_links": "No Links"
}, },
"player": {
"error": "Error",
"failed_to_get_stream_url": "Failed to get the stream URL",
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
"client_error": "Client Error",
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}",
"next_episode": "Next Episode",
"refresh_tracks": "Refresh Tracks",
"audio_tracks": "Audio Tracks:",
"playback_state": "Playback State:",
"index": "Index:",
"continue_watching": "Continue Watching",
"go_back": "Go Back",
"downloaded_file_title": "You have this file downloaded",
"downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes",
"downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel"
},
"item_card": { "item_card": {
"next_up": "Next Up", "next_up": "Next Up",
"no_items_to_display": "No Items to Display", "no_items_to_display": "No Items to Display",

View File

@@ -39,39 +39,39 @@
"please_login_again": "Su sesión guardada ha caducado. Por favor, inicie sesión de nuevo.", "please_login_again": "Su sesión guardada ha caducado. Por favor, inicie sesión de nuevo.",
"remove_saved_login": "Eliminar inicio de sesión guardado", "remove_saved_login": "Eliminar inicio de sesión guardado",
"remove_saved_login_description": "Esto eliminará tus credenciales guardadas para este servidor. Tendrás que volver a introducir tu nombre de usuario y contraseña la próxima vez.", "remove_saved_login_description": "Esto eliminará tus credenciales guardadas para este servidor. Tendrás que volver a introducir tu nombre de usuario y contraseña la próxima vez.",
"accounts_count": "{{count}} cuentas", "accounts_count": "{{count}} accounts",
"select_account": "Seleccione una cuenta", "select_account": "Select Account",
"add_account": "Añadir cuenta", "add_account": "Add Account",
"remove_account_description": "Esto eliminará las credenciales guardadas para {{username}}." "remove_account_description": "This will remove the saved credentials for {{username}}."
}, },
"save_account": { "save_account": {
"title": "Guardar Cuenta", "title": "Save Account",
"save_for_later": "Guardar esta cuenta", "save_for_later": "Save this account",
"security_option": "Opciones de seguridad", "security_option": "Security Option",
"no_protection": "Sin Protección", "no_protection": "No protection",
"no_protection_desc": "Inicio de sesión rápido sin autenticación", "no_protection_desc": "Quick login without authentication",
"pin_code": "Código PIN", "pin_code": "PIN code",
"pin_code_desc": "PIN de 4 dígitos requerido al cambiar", "pin_code_desc": "4-digit PIN required when switching",
"password": "Vuelva a introducir la contraseña", "password": "Re-enter password",
"password_desc": "Contraseña requerida al cambiar", "password_desc": "Password required when switching",
"save_button": "Guardar", "save_button": "Save",
"cancel_button": "Cancelar" "cancel_button": "Cancel"
}, },
"pin": { "pin": {
"enter_pin": "Introduce el PIN", "enter_pin": "Enter PIN",
"enter_pin_for": "Introduzca el PIN para {{username}}", "enter_pin_for": "Enter PIN for {{username}}",
"enter_4_digits": "Introduce 4 dígitos", "enter_4_digits": "Enter 4 digits",
"invalid_pin": "PIN inválido", "invalid_pin": "Invalid PIN",
"setup_pin": "Configurar PIN", "setup_pin": "Set Up PIN",
"confirm_pin": "Confirmar PIN", "confirm_pin": "Confirm PIN",
"pins_dont_match": "Los códigos PIN no coinciden", "pins_dont_match": "PINs don't match",
"forgot_pin": "¿Olvidó el PIN?", "forgot_pin": "Forgot PIN?",
"forgot_pin_desc": "Sus credenciales guardadas serán eliminadas" "forgot_pin_desc": "Your saved credentials will be removed"
}, },
"password": { "password": {
"enter_password": "Introduzca la contraseña", "enter_password": "Enter Password",
"enter_password_for": "Introduzca la contraseña para {{username}}", "enter_password_for": "Enter password for {{username}}",
"invalid_password": "Contraseña inválida" "invalid_password": "Invalid password"
}, },
"home": { "home": {
"checking_server_connection": "Comprobando conexión con el servidor...", "checking_server_connection": "Comprobando conexión con el servidor...",
@@ -124,32 +124,32 @@
"hide_remote_session_button": "Ocultar botón de sesión remota" "hide_remote_session_button": "Ocultar botón de sesión remota"
}, },
"network": { "network": {
"title": "Cadena", "title": "Network",
"local_network": "Red local", "local_network": "Local Network",
"auto_switch_enabled": "Cambiar automáticamente en casa", "auto_switch_enabled": "Auto-switch when at home",
"auto_switch_description": "Cambiar automáticamente a la URL local cuando se conecta a la WiFi de casa", "auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
"local_url": "URL local", "local_url": "Local URL",
"local_url_hint": "Introduzca la dirección de su servidor local (por ejemplo, http://192.168.1.100:8096)", "local_url_hint": "Enter your local server address (e.g., http://192.168.1.100:8096)",
"local_url_placeholder": "http://192.168.1.100:8096", "local_url_placeholder": "http://192.168.1.100:8096",
"home_wifi_networks": "Redes WiFi domésticas", "home_wifi_networks": "Home WiFi Networks",
"add_current_network": "Añadir \"{{ssid}}\"", "add_current_network": "Add \"{{ssid}}\"",
"not_connected_to_wifi": "No está conectado a WiFi", "not_connected_to_wifi": "Not connected to WiFi",
"no_networks_configured": "No hay redes configuradas", "no_networks_configured": "No networks configured",
"add_network_hint": "Añade tu red WiFi doméstica para activar el cambio automático", "add_network_hint": "Add your home WiFi network to enable auto-switching",
"current_wifi": "WiFi actual", "current_wifi": "Current WiFi",
"using_url": "Utilizando", "using_url": "Using",
"local": "URL local", "local": "Local URL",
"remote": "URL Remota", "remote": "Remote URL",
"not_connected": "Sin conexión", "not_connected": "Not connected",
"current_server": "Servidor actual", "current_server": "Current Server",
"remote_url": "URL Remota", "remote_url": "Remote URL",
"active_url": "URL Activa", "active_url": "Active URL",
"not_configured": "Sin configurar", "not_configured": "Not configured",
"network_added": "Red añadida", "network_added": "Network added",
"network_already_added": "Red ya añadida", "network_already_added": "Network already added",
"no_wifi_connected": "Sin conexión a WiFi", "no_wifi_connected": "Not connected to WiFi",
"permission_denied": "Permiso de ubicación denegado", "permission_denied": "Location permission denied",
"permission_denied_explanation": "Se necesita el permiso de ubicación para detectar la red WiFi para cambiar automáticamente. Por favor, actívala en Configuración." "permission_denied_explanation": "Location permission is required to detect WiFi network for auto-switching. Please enable it in Settings."
}, },
"user_info": { "user_info": {
"user_info_title": "Información de usuario", "user_info_title": "Información de usuario",
@@ -195,12 +195,12 @@
"none": "Ninguno", "none": "Ninguno",
"language": "Idioma", "language": "Idioma",
"transcode_mode": { "transcode_mode": {
"title": "Transcodificación de audio", "title": "Audio Transcoding",
"description": "Controla cómo el audio envolvente (7.1, TrueHD, DTS-HD) es manejado", "description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
"auto": "Auto", "auto": "Auto",
"stereo": "Forzar salida estéreo", "stereo": "Force Stereo",
"5_1": "Permitir 5.1", "5_1": "Allow 5.1",
"passthrough": "Directo" "passthrough": "Passthrough"
} }
}, },
"subtitles": { "subtitles": {
@@ -259,16 +259,16 @@
"hardware_decode_description": "Utilizar la aceleración de hardware para la decodificación de vídeo. Deshabilite si experimenta problemas de reproducción." "hardware_decode_description": "Utilizar la aceleración de hardware para la decodificación de vídeo. Deshabilite si experimenta problemas de reproducción."
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "Configuración de subtítulos VLC", "title": "VLC Subtitle Settings",
"hint": "Personalizar la apariencia de los subtítulos para el reproductor VLC. Los cambios tendrán efecto en la próxima reproducción.", "hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Color del texto", "text_color": "Text Color",
"background_color": "Color del fondo", "background_color": "Background Color",
"background_opacity": "Opacidad del fondo", "background_opacity": "Background Opacity",
"outline_color": "Color del contorno", "outline_color": "Outline Color",
"outline_opacity": "Opacidad del contorno", "outline_opacity": "Outline Opacity",
"outline_thickness": "Grosor del contorno", "outline_thickness": "Outline Thickness",
"bold": "Texto en negrita", "bold": "Bold Text",
"margin": "Margen inferior" "margin": "Bottom Margin"
}, },
"video_player": { "video_player": {
"title": "Reproductor de vídeo", "title": "Reproductor de vídeo",
@@ -300,13 +300,13 @@
"VLC_4": "VLC 4 (Experimental + PiP)" "VLC_4": "VLC 4 (Experimental + PiP)"
}, },
"show_custom_menu_links": "Mostrar enlaces de menú personalizados", "show_custom_menu_links": "Mostrar enlaces de menú personalizados",
"show_large_home_carousel": "Mostrar carrusel del menú principal grande (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"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",
"default_playback_speed": "Velocidad de reproducción predeterminada", "default_playback_speed": "Velocidad de reproducción predeterminada",
"auto_play_next_episode": "Reproducir automáticamente el siguiente episodio", "auto_play_next_episode": "Auto-play Next Episode",
"max_auto_play_episode_count": "Máximo número de episodios de Auto Play", "max_auto_play_episode_count": "Máximo número de episodios de Auto Play",
"disabled": "Deshabilitado" "disabled": "Deshabilitado"
}, },
@@ -317,10 +317,10 @@
"title": "Música", "title": "Música",
"playback_title": "Reproducir", "playback_title": "Reproducir",
"playback_description": "Configurar cómo se reproduce la música.", "playback_description": "Configurar cómo se reproduce la música.",
"prefer_downloaded": "Preferir las canciones descargadas", "prefer_downloaded": "Prefer Downloaded Songs",
"caching_title": "Almacenando en caché", "caching_title": "Almacenando en caché",
"caching_description": "Cachear automáticamente las próximas canciones para una reproducción más suave.", "caching_description": "Automatically cache upcoming tracks for smoother playback.",
"lookahead_enabled": "Activar el look-Ahead Cache", "lookahead_enabled": "Enable Look-Ahead Caching",
"lookahead_count": "", "lookahead_count": "",
"max_cache_size": "Tamaño máximo del caché" "max_cache_size": "Tamaño máximo del caché"
}, },
@@ -399,7 +399,7 @@
"size_used": "{{used}} de {{total}} usado", "size_used": "{{used}} de {{total}} usado",
"delete_all_downloaded_files": "Eliminar todos los archivos descargados", "delete_all_downloaded_files": "Eliminar todos los archivos descargados",
"music_cache_title": "Caché de música", "music_cache_title": "Caché de música",
"music_cache_description": "Cachear automáticamente las canciones mientras escuchas una reproducción más suave y soporte sin conexión", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Activar Caché de Música", "enable_music_cache": "Activar Caché de Música",
"clear_music_cache": "Borrar Caché de Música", "clear_music_cache": "Borrar Caché de Música",
"music_cache_size": "Caché {{Tamaño}}", "music_cache_size": "Caché {{Tamaño}}",
@@ -504,10 +504,10 @@
"delete": "Borrar", "delete": "Borrar",
"ok": "Aceptar", "ok": "Aceptar",
"remove": "Eliminar", "remove": "Eliminar",
"next": "Siguiente", "next": "Next",
"back": "Atrás", "back": "Back",
"continue": "Continuar", "continue": "Continue",
"verifying": "Verificando..." "verifying": "Verifying..."
}, },
"search": { "search": {
"search": "Buscar...", "search": "Buscar...",
@@ -753,8 +753,8 @@
"downloaded": "Descargado", "downloaded": "Descargado",
"downloading": "Descargando...", "downloading": "Descargando...",
"cached": "En caché", "cached": "En caché",
"delete_download": "Eliminar descarga", "delete_download": "Delete Download",
"delete_cache": "Borrar del caché", "delete_cache": "Remove from Cache",
"go_to_artist": "Ir al artista", "go_to_artist": "Ir al artista",
"go_to_album": "Ir al álbum", "go_to_album": "Ir al álbum",
"add_to_favorites": "Añadir a Favoritos", "add_to_favorites": "Añadir a Favoritos",

View File

@@ -305,39 +305,24 @@
"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",
"default_playback_speed": "Vitesse de lecture par défaut", "default_playback_speed": "Default Playback Speed",
"auto_play_next_episode": "Lecture automatique de l'épisode suivant", "auto_play_next_episode": "Auto-play Next Episode",
"max_auto_play_episode_count": "Nombre d'épisodes en lecture automatique max", "max_auto_play_episode_count": "Nombre d'épisodes en lecture automatique max",
"disabled": "Désactivé", "disabled": "Désactivé"
"segment_skip_settings": "Saut de segments",
"segment_skip_settings_description": "Configurer le saut pour les intros, génériques et autres segments",
"skip_intro": "Sauter l'intro",
"skip_intro_description": "Action lorsqu'un segment d'intro est détecté",
"skip_outro": "Sauter générique / outro",
"skip_outro_description": "Action lorsqu'un segment de générique/outro est détecté",
"skip_recap": "Sauter le résumé",
"skip_recap_description": "Action lorsqu'un segment de résumé est détecté",
"skip_commercial": "Sauter la publicité",
"skip_commercial_description": "Action lorsqu'un segment publicitaire est détecté",
"skip_preview": "Sauter l'aperçu",
"skip_preview_description": "Action lorsqu'un segment d'aperçu est détecté",
"segment_skip_none": "Aucune",
"segment_skip_ask": "Afficher le bouton",
"segment_skip_auto": "Saut automatique"
}, },
"downloads": { "downloads": {
"downloads_title": "Téléchargements" "downloads_title": "Téléchargements"
}, },
"music": { "music": {
"title": "Musique", "title": "Music",
"playback_title": "Lecture", "playback_title": "Playback",
"playback_description": "Configurer le mode de lecture de la musique.", "playback_description": "Configure how music is played.",
"prefer_downloaded": "Supprimer toutes les musiques téléchargées", "prefer_downloaded": "Prefer Downloaded Songs",
"caching_title": "Mise en cache", "caching_title": "Caching",
"caching_description": "Mettre automatiquement en cache les pistes à venir pour une lecture plus fluide.", "caching_description": "Automatically cache upcoming tracks for smoother playback.",
"lookahead_enabled": "Activer la mise en cache guidée", "lookahead_enabled": "Enable Look-Ahead Caching",
"lookahead_count": "Pistes à pré-mettre en cache", "lookahead_count": "Tracks to Pre-cache",
"max_cache_size": "Taille max de cache" "max_cache_size": "Max Cache Size"
}, },
"plugins": { "plugins": {
"plugins_title": "Plugins", "plugins_title": "Plugins",
@@ -372,19 +357,19 @@
"save_button": "Enregistrer", "save_button": "Enregistrer",
"toasts": { "toasts": {
"saved": "Enregistré", "saved": "Enregistré",
"refreshed": "Paramètres actualisés depuis le serveur" "refreshed": "Settings refreshed from server"
}, },
"refresh_from_server": "Rafraîchir les paramètres depuis le serveur" "refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Activer Streamystats", "enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Désactiver Streamystats", "disable_streamystats": "Disable Streamystats",
"enable_search": "Utiliser pour la recherche", "enable_search": "Use for Search",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Entrez l'URL de votre serveur Streamystats. L'URL doit inclure http ou https et éventuellement le port.", "streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
"read_more_about_streamystats": "En savoir plus sur Streamystats.", "read_more_about_streamystats": "Read More About Streamystats.",
"save_button": "Enregistrer", "save_button": "Save",
"save": "Enregistrer", "save": "Enregistrer",
"features_title": "Fonctionnalités", "features_title": "Fonctionnalités",
"home_sections_title": "Sections de la page d´accueil", "home_sections_title": "Sections de la page d´accueil",
@@ -587,7 +572,7 @@
"genres": "Genres", "genres": "Genres",
"years": "Années", "years": "Années",
"sort_by": "Trier par", "sort_by": "Trier par",
"filter_by": "Filtrer par", "filter_by": "Filter By",
"sort_order": "Ordre de tri", "sort_order": "Ordre de tri",
"tags": "Tags" "tags": "Tags"
} }
@@ -606,11 +591,6 @@
"no_links": "Aucuns liens" "no_links": "Aucuns liens"
}, },
"player": { "player": {
"skip_intro": "Passer l'intro",
"skip_outro": "Passer l'outro",
"skip_recap": "Passer le résumé",
"skip_commercial": "Passer la pub",
"skip_preview": "Passer l'aperçu",
"error": "Erreur", "error": "Erreur",
"failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux", "failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux",
"an_error_occured_while_playing_the_video": "Une erreur sest produite lors de la lecture de la vidéo. Vérifiez les journaux dans les paramètres.", "an_error_occured_while_playing_the_video": "Une erreur sest produite lors de la lecture de la vidéo. Vérifiez les journaux dans les paramètres.",
@@ -739,127 +719,127 @@
"favorites": "Favoris" "favorites": "Favoris"
}, },
"music": { "music": {
"title": "Musique", "title": "Music",
"tabs": { "tabs": {
"suggestions": "Suggestions", "suggestions": "Suggestions",
"albums": "Albums", "albums": "Albums",
"artists": "Artistes", "artists": "Artists",
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "morceaux" "tracks": "tracks"
}, },
"filters": { "filters": {
"all": "Toutes" "all": "All"
}, },
"recently_added": "Ajoutés récemment", "recently_added": "Recently Added",
"recently_played": "Récemment joué", "recently_played": "Recently Played",
"frequently_played": "Fréquemment joué", "frequently_played": "Frequently Played",
"explore": "Explorez", "explore": "Explore",
"top_tracks": "Top chansons", "top_tracks": "Top Tracks",
"play": "Lecture", "play": "Play",
"shuffle": "Aléatoire", "shuffle": "Shuffle",
"play_top_tracks": "Jouer les pistes les plus populaires", "play_top_tracks": "Play Top Tracks",
"no_suggestions": "Pas de suggestion disponible", "no_suggestions": "No suggestions available",
"no_albums": "Pas d'albums trouvés", "no_albums": "No albums found",
"no_artists": "Pas d'artistes trouvé", "no_artists": "No artists found",
"no_playlists": "Pas de playlists trouvées", "no_playlists": "No playlists found",
"album_not_found": "Album introuvable", "album_not_found": "Album not found",
"artist_not_found": "Artiste introuvable", "artist_not_found": "Artist not found",
"playlist_not_found": "Playlist introuvable", "playlist_not_found": "Playlist not found",
"track_options": { "track_options": {
"play_next": "Lecture suivante", "play_next": "Play Next",
"add_to_queue": "Ajouter à la file d'attente", "add_to_queue": "Add to Queue",
"add_to_playlist": "Ajouter à la playlist", "add_to_playlist": "Add to Playlist",
"download": "Télécharger", "download": "Download",
"downloaded": "Téléchargé", "downloaded": "Downloaded",
"downloading": "Téléchargement en cours...", "downloading": "Downloading...",
"cached": "En cache", "cached": "Cached",
"delete_download": "Supprimer un téléchargement", "delete_download": "Delete Download",
"delete_cache": "Supprimer du cache", "delete_cache": "Remove from Cache",
"go_to_artist": "Voir l'artiste", "go_to_artist": "Go to Artist",
"go_to_album": "Aller à lalbum", "go_to_album": "Go to Album",
"add_to_favorites": "Ajouter aux favoris", "add_to_favorites": "Add to Favorites",
"remove_from_favorites": "Retirer des favoris", "remove_from_favorites": "Remove from Favorites",
"remove_from_playlist": "Retirer de la playlist" "remove_from_playlist": "Remove from Playlist"
}, },
"playlists": { "playlists": {
"create_playlist": "Créer une Playlist", "create_playlist": "Create Playlist",
"playlist_name": "Nom de la Playlist", "playlist_name": "Playlist Name",
"enter_name": "Entrer le nom de la playlist", "enter_name": "Enter playlist name",
"create": "Créer", "create": "Create",
"search_playlists": "Rechercher des playlists...", "search_playlists": "Search playlists...",
"added_to": "Ajouté à {{name}}", "added_to": "Added to {{name}}",
"added": "Ajouté à la playlist", "added": "Added to playlist",
"removed_from": "Retiré de {{name}}", "removed_from": "Removed from {{name}}",
"removed": "Retiré de la playlist", "removed": "Removed from playlist",
"created": "Playlist créée", "created": "Playlist created",
"create_new": "Créer une nouvelle playlist", "create_new": "Create New Playlist",
"failed_to_add": "Échec de l'ajout à la playlist", "failed_to_add": "Failed to add to playlist",
"failed_to_remove": "Échec de la suppression de la playlist", "failed_to_remove": "Failed to remove from playlist",
"failed_to_create": "Échec de la suppression de la playlist", "failed_to_create": "Failed to create playlist",
"delete_playlist": "Supprimer la playlist", "delete_playlist": "Delete Playlist",
"delete_confirm": "Êtes-vous sûr de vouloir supprimer « {{ name }} » ? Cette action est irréversible.", "delete_confirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"deleted": "Playlist supprimée", "deleted": "Playlist deleted",
"failed_to_delete": "Échec de la suppression de la playlist" "failed_to_delete": "Failed to delete playlist"
}, },
"sort": { "sort": {
"title": "Trier par", "title": "Sort By",
"alphabetical": "Ordre alphabétique", "alphabetical": "Alphabetical",
"date_created": "Date de création" "date_created": "Date Created"
} }
}, },
"watchlists": { "watchlists": {
"title": "Watchlists", "title": "Watchlists",
"my_watchlists": "My Watchlists", "my_watchlists": "My Watchlists",
"public_watchlists": "Watchlist publique", "public_watchlists": "Public Watchlists",
"create_title": "Créer une Watchlist", "create_title": "Create Watchlist",
"edit_title": "Modifier la Watchlist", "edit_title": "Edit Watchlist",
"create_button": "Créer une Watchlist", "create_button": "Create Watchlist",
"save_button": "Enregistrer les modifications", "save_button": "Save Changes",
"delete_button": "Supprimer", "delete_button": "Delete",
"remove_button": "Retirer", "remove_button": "Remove",
"cancel_button": "Annuler", "cancel_button": "Cancel",
"name_label": "Nom", "name_label": "Name",
"name_placeholder": "Entrer le nom de la playlist", "name_placeholder": "Enter watchlist name",
"description_label": "Description", "description_label": "Description",
"description_placeholder": "Entrez la description (facultatif)", "description_placeholder": "Enter description (optional)",
"is_public_label": "Public Watchlist", "is_public_label": "Public Watchlist",
"is_public_description": "Autoriser d'autres personnes à voir cette liste de suivi", "is_public_description": "Allow others to view this watchlist",
"allowed_type_label": "Type de contenu", "allowed_type_label": "Content Type",
"sort_order_label": "Ordre de tri par défaut", "sort_order_label": "Default Sort Order",
"empty_title": "Pas de Watchlists", "empty_title": "No Watchlists",
"empty_description": "Créez votre première liste de suivi pour commencer à organiser vos médias", "empty_description": "Create your first watchlist to start organizing your media",
"empty_watchlist": "Cette liste de suivi est vide", "empty_watchlist": "This watchlist is empty",
"empty_watchlist_hint": "Ajouter des éléments de votre bibliothèque à cette liste de suivi", "empty_watchlist_hint": "Add items from your library to this watchlist",
"not_configured_title": "Streamystats non configuré", "not_configured_title": "Streamystats Not Configured",
"not_configured_description": "Configurer Streamystats dans les paramètres pour utiliser les listes de suivi", "not_configured_description": "Configure Streamystats in settings to use watchlists",
"go_to_settings": "Accédez aux Paramètres", "go_to_settings": "Go to Settings",
"add_to_watchlist": "Ajouter à la Watchlist", "add_to_watchlist": "Add to Watchlist",
"remove_from_watchlist": "Retirer de la Watchlist", "remove_from_watchlist": "Remove from Watchlist",
"select_watchlist": "Sélectionner la liste de suivi", "select_watchlist": "Select Watchlist",
"create_new": "Créer une Watchlist", "create_new": "Create New Watchlist",
"item": "médias", "item": "item",
"items": "élément", "items": "items",
"public": "Publique", "public": "Public",
"private": "Privée", "private": "Private",
"you": "Vous-même", "you": "You",
"by_owner": "Par un autre utilisateur", "by_owner": "By another user",
"not_found": "Playlist introuvable", "not_found": "Watchlist not found",
"delete_confirm_title": "Supprimer la Watchlist", "delete_confirm_title": "Delete Watchlist",
"delete_confirm_message": "Tous les médias (par défaut)", "delete_confirm_message": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"remove_item_title": "Retirer de la Watchlist", "remove_item_title": "Remove from Watchlist",
"remove_item_message": "Retirer «{{name}}» de cette liste de suivi?", "remove_item_message": "Remove \"{{name}}\" from this watchlist?",
"loading": "Chargement des listes de suivi...", "loading": "Loading watchlists...",
"no_compatible_watchlists": "Aucune liste de suivi compatible", "no_compatible_watchlists": "No compatible watchlists",
"create_one_first": "Créer une liste de suivi qui accepte ce type de contenu" "create_one_first": "Create a watchlist that accepts this content type"
}, },
"playback_speed": { "playback_speed": {
"title": "Vitesse de lecture", "title": "Playback Speed",
"apply_to": "Appliquer à", "apply_to": "Apply To",
"speed": "Vitesse", "speed": "Speed",
"scope": { "scope": {
"media": "Ce média uniquement", "media": "This media only",
"show": "Cette série", "show": "This show",
"all": "Tous les médias (par défaut)" "all": "All media (default)"
} }
} }
} }

View File

@@ -34,9 +34,9 @@
"search_for_local_servers": "Ricerca dei server locali", "search_for_local_servers": "Ricerca dei server locali",
"searching": "Cercando...", "searching": "Cercando...",
"servers": "Server", "servers": "Server",
"saved": "Salvato", "saved": "Saved",
"session_expired": "Session Expired", "session_expired": "Session Expired",
"please_login_again": "La tua sessione è scaduta. Si prega di eseguire nuovamente l'accesso.", "please_login_again": "Your saved session has expired. Please log in again.",
"remove_saved_login": "Remove Saved Login", "remove_saved_login": "Remove Saved Login",
"remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.", "remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
@@ -125,7 +125,7 @@
}, },
"network": { "network": {
"title": "Network", "title": "Network",
"local_network": "", "local_network": "Local Network",
"auto_switch_enabled": "Auto-switch when at home", "auto_switch_enabled": "Auto-switch when at home",
"auto_switch_description": "Automatically switch to local URL when connected to home WiFi", "auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
"local_url": "Local URL", "local_url": "Local URL",
@@ -137,7 +137,7 @@
"no_networks_configured": "No networks configured", "no_networks_configured": "No networks configured",
"add_network_hint": "Add your home WiFi network to enable auto-switching", "add_network_hint": "Add your home WiFi network to enable auto-switching",
"current_wifi": "Current WiFi", "current_wifi": "Current WiFi",
"using_url": "Sta utilizzando", "using_url": "Using",
"local": "Local URL", "local": "Local URL",
"remote": "Remote URL", "remote": "Remote URL",
"not_connected": "Not connected", "not_connected": "Not connected",

View File

@@ -30,7 +30,7 @@
"connect_button": "Verbinden", "connect_button": "Verbinden",
"previous_servers": "vorige servers", "previous_servers": "vorige servers",
"clear_button": "Wissen", "clear_button": "Wissen",
"swipe_to_remove": "Swipe om te verwijderen.", "swipe_to_remove": "Swipe to remove",
"search_for_local_servers": "Zoek naar lokale servers", "search_for_local_servers": "Zoek naar lokale servers",
"searching": "Zoeken...", "searching": "Zoeken...",
"servers": "Servers", "servers": "Servers",
@@ -40,38 +40,38 @@
"remove_saved_login": "Opgeslagen login verwijderen", "remove_saved_login": "Opgeslagen login verwijderen",
"remove_saved_login_description": "Hiermee worden uw opgeslagen gegevens voor deze server verwijderd. U moet uw gebruikersnaam en wachtwoord de volgende keer opnieuw invoeren.", "remove_saved_login_description": "Hiermee worden uw opgeslagen gegevens voor deze server verwijderd. U moet uw gebruikersnaam en wachtwoord de volgende keer opnieuw invoeren.",
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Account selecteren", "select_account": "Select Account",
"add_account": "Account toevoegen", "add_account": "Add Account",
"remove_account_description": "Hiermee worden de opgeslagen inloggegevens voor {{username}} verwijderd." "remove_account_description": "This will remove the saved credentials for {{username}}."
}, },
"save_account": { "save_account": {
"title": "Account opslaan", "title": "Save Account",
"save_for_later": "Dit account opslaan", "save_for_later": "Save this account",
"security_option": "Beveiligingsopties", "security_option": "Security Option",
"no_protection": "Geen beveiliging", "no_protection": "No protection",
"no_protection_desc": "Snelle login zonder authenticatie", "no_protection_desc": "Quick login without authentication",
"pin_code": "Pincode", "pin_code": "PIN code",
"pin_code_desc": "4-cijferige pincode vereist bij wisselen", "pin_code_desc": "4-digit PIN required when switching",
"password": "Wachtwoord opnieuw invoeren", "password": "Re-enter password",
"password_desc": "Wachtwoord vereist bij wisselen", "password_desc": "Password required when switching",
"save_button": "Opslaan", "save_button": "Save",
"cancel_button": "Annuleren" "cancel_button": "Cancel"
}, },
"pin": { "pin": {
"enter_pin": "Pincode invoeren", "enter_pin": "Enter PIN",
"enter_pin_for": "Pincode voor {{username}} invoeren", "enter_pin_for": "Enter PIN for {{username}}",
"enter_4_digits": "Voer 6 cijfers in", "enter_4_digits": "Enter 4 digits",
"invalid_pin": "Ongeldige pincode", "invalid_pin": "Invalid PIN",
"setup_pin": "Pincode instellen", "setup_pin": "Set Up PIN",
"confirm_pin": "Pincode bevestigen", "confirm_pin": "Confirm PIN",
"pins_dont_match": "Pincodes komen niet overeen", "pins_dont_match": "PINs don't match",
"forgot_pin": "Pincode vergeten?", "forgot_pin": "Forgot PIN?",
"forgot_pin_desc": "Je opgeslagen inloggegevens worden verwijderd" "forgot_pin_desc": "Your saved credentials will be removed"
}, },
"password": { "password": {
"enter_password": "Voer wachtwoord in", "enter_password": "Enter Password",
"enter_password_for": "Voer wachtwoord voor {{username}} in", "enter_password_for": "Enter password for {{username}}",
"invalid_password": "Ongeldig wachtwoord" "invalid_password": "Invalid password"
}, },
"home": { "home": {
"checking_server_connection": "Serververbinding controleren...", "checking_server_connection": "Serververbinding controleren...",
@@ -84,7 +84,7 @@
"server_unreachable": "Server onbereikbaar", "server_unreachable": "Server onbereikbaar",
"server_unreachable_message": "Kon de server niet bereiken.\nControleer uw netwerkverbinding.", "server_unreachable_message": "Kon de server niet bereiken.\nControleer uw netwerkverbinding.",
"oops": "Oeps!", "oops": "Oeps!",
"error_message": "Er ging iets fout\nProbeer opnieuw in te loggen.", "error_message": "Er ging iets fout\nGelieve af en aan te melden.",
"continue_watching": "Verder Kijken", "continue_watching": "Verder Kijken",
"next_up": "Volgende", "next_up": "Volgende",
"continue_and_next_up": "Doorgaan & Volgende", "continue_and_next_up": "Doorgaan & Volgende",
@@ -124,32 +124,32 @@
"hide_remote_session_button": "Verberg Knop voor Externe Sessie" "hide_remote_session_button": "Verberg Knop voor Externe Sessie"
}, },
"network": { "network": {
"title": "Netwerk", "title": "Network",
"local_network": "Lokaal netwerk", "local_network": "Local Network",
"auto_switch_enabled": "Automatisch wisselen wanneer thuis", "auto_switch_enabled": "Auto-switch when at home",
"auto_switch_description": "Automatisch wisselen naar lokale URL wanneer verbonden met thuisnetwerk", "auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
"local_url": "Lokale URL", "local_url": "Local URL",
"local_url_hint": "Voer uw lokale serveradres in (bijv. http://192.168.1.100:8096)", "local_url_hint": "Enter your local server address (e.g., http://192.168.1.100:8096)",
"local_url_placeholder": "http://192.168.1.100:8096", "local_url_placeholder": "http://192.168.1.100:8096",
"home_wifi_networks": "Wi-Fi netwerken", "home_wifi_networks": "Home WiFi Networks",
"add_current_network": "Voeg \"{{ssid}} \" toe", "add_current_network": "Add \"{{ssid}}\"",
"not_connected_to_wifi": "Niet verbonden met Wi-Fi", "not_connected_to_wifi": "Not connected to WiFi",
"no_networks_configured": "Geen netwerken geconfigureerd", "no_networks_configured": "No networks configured",
"add_network_hint": "Voeg je thuisnetwerk toe om automatisch wisselen in te schakelen", "add_network_hint": "Add your home WiFi network to enable auto-switching",
"current_wifi": "Huidige Wi-Fi", "current_wifi": "Current WiFi",
"using_url": "Gebruik makend van", "using_url": "Using",
"local": "Lokale URL", "local": "Local URL",
"remote": "Externe URL", "remote": "Remote URL",
"not_connected": "Niet verbonden", "not_connected": "Not connected",
"current_server": "Huidige Server", "current_server": "Current Server",
"remote_url": "Externe URL", "remote_url": "Remote URL",
"active_url": "Actieve URL", "active_url": "Active URL",
"not_configured": "Niet geconfigureerd", "not_configured": "Not configured",
"network_added": "Netwerk toegevoegd", "network_added": "Network added",
"network_already_added": "Netwerk reeds toegevoegd", "network_already_added": "Network already added",
"no_wifi_connected": "Niet verbonden met Wi-Fi", "no_wifi_connected": "Not connected to WiFi",
"permission_denied": "Locatie toestemming geweigerd", "permission_denied": "Location permission denied",
"permission_denied_explanation": "Locatie permissie is vereist om Wifi-netwerk te kunnen detecteren voor automatisch wisselen. Schakel het in via Instellingen." "permission_denied_explanation": "Location permission is required to detect WiFi network for auto-switching. Please enable it in Settings."
}, },
"user_info": { "user_info": {
"user_info_title": "Gebruiker Info", "user_info_title": "Gebruiker Info",
@@ -195,11 +195,11 @@
"none": "Geen", "none": "Geen",
"language": "Taal", "language": "Taal",
"transcode_mode": { "transcode_mode": {
"title": "Audio-transcoding", "title": "Audio Transcoding",
"description": "Bepaalt hoe surround audio (7.1, TrueHD, DTS-HD) wordt behandeld", "description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
"auto": "Automatisch", "auto": "Auto",
"stereo": "Stereo forceren", "stereo": "Force Stereo",
"5_1": "5.1 toestaan", "5_1": "Allow 5.1",
"passthrough": "Passthrough" "passthrough": "Passthrough"
} }
}, },
@@ -231,7 +231,7 @@
"Black": "Zwart", "Black": "Zwart",
"Gray": "Grijs", "Gray": "Grijs",
"Silver": "Zilver", "Silver": "Zilver",
"White": "Wit", "White": "wit",
"Maroon": "Kastanjebruin", "Maroon": "Kastanjebruin",
"Red": "Rood", "Red": "Rood",
"Fuchsia": "Fuchsia", "Fuchsia": "Fuchsia",
@@ -259,14 +259,14 @@
"hardware_decode_description": "Gebruik hardware acceleratie voor video-decodering. Uitschakelen als u problemen met afspelen ondervindt." "hardware_decode_description": "Gebruik hardware acceleratie voor video-decodering. Uitschakelen als u problemen met afspelen ondervindt."
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC ondertitel instellingen", "title": "VLC Subtitle Settings",
"hint": "Aanpassen van ondertiteling voor VLC-speler. Wijzigingen worden toegepast bij het afspelen.", "hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Tekstkleur", "text_color": "Text Color",
"background_color": "Achtergrondkleur", "background_color": "Background Color",
"background_opacity": "Doorzichtigheid achtergrond", "background_opacity": "Background Opacity",
"outline_color": "Kleur omlijning", "outline_color": "Outline Color",
"outline_opacity": "Omtrek opaciteit", "outline_opacity": "Outline Opacity",
"outline_thickness": "Omtrek dikte", "outline_thickness": "Outline Thickness",
"bold": "Bold Text", "bold": "Bold Text",
"margin": "Bottom Margin" "margin": "Bottom Margin"
}, },
@@ -306,7 +306,7 @@
"disable_haptic_feedback": "Haptische feedback uitschakelen", "disable_haptic_feedback": "Haptische feedback uitschakelen",
"default_quality": "Standaard kwaliteit", "default_quality": "Standaard kwaliteit",
"default_playback_speed": "Standaard Afspeelsnelheid", "default_playback_speed": "Standaard Afspeelsnelheid",
"auto_play_next_episode": "Volgende aflevering automatisch afspelen", "auto_play_next_episode": "Auto-play Next Episode",
"max_auto_play_episode_count": "Max Automatisch Aflevering Aantal", "max_auto_play_episode_count": "Max Automatisch Aflevering Aantal",
"disabled": "Uitgeschakeld" "disabled": "Uitgeschakeld"
}, },
@@ -378,12 +378,12 @@
"enable_promoted_watchlists": "Gepromote Kijklijst", "enable_promoted_watchlists": "Gepromote Kijklijst",
"hide_watchlists_tab": "Hide Watchlists Tab", "hide_watchlists_tab": "Hide Watchlists Tab",
"home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.", "home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.",
"recommended_movies": "Aanbevolen films", "recommended_movies": "Recommended Movies",
"recommended_series": "Aanbevolen serie", "recommended_series": "Recommended Series",
"toasts": { "toasts": {
"saved": "Opgeslagen", "saved": "Saved",
"refreshed": "Settings refreshed from server", "refreshed": "Settings refreshed from server",
"disabled": "Streamystats uitgeschakeld" "disabled": "Streamystats disabled"
}, },
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
@@ -402,24 +402,24 @@
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Enable Music Cache", "enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} gecached", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Muziek cache gewist", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Verwijder alle gedownloade liedjes", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} gedownload", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted" "downloaded_songs_deleted": "Downloaded songs deleted"
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
"show_intro": "Toon intro", "show_intro": "Toon intro",
"reset_intro": "Reset Intro" "reset_intro": "intro opnieuw instellen"
}, },
"logs": { "logs": {
"logs_title": "Logboek", "logs_title": "Logboek",
"export_logs": "Export logs", "export_logs": "Export logs",
"click_for_more_info": "Klik voor meer info", "click_for_more_info": "Click for more info",
"level": "Niveau", "level": "Niveau",
"no_logs_available": "Geen logs beschikbaar", "no_logs_available": "Geen logs beschikbaar",
"delete_all_logs": "Alle logs verwijderen" "delete_all_logs": "Verwijder alle logs"
}, },
"languages": { "languages": {
"title": "Talen", "title": "Talen",
@@ -500,14 +500,14 @@
"play": "Afspelen", "play": "Afspelen",
"none": "Geen", "none": "Geen",
"track": "Spoor", "track": "Spoor",
"cancel": "Annuleren", "cancel": "Cancel",
"delete": "Verwijderen", "delete": "Delete",
"ok": "O", "ok": "OK",
"remove": "Verwijderen", "remove": "Remove",
"next": "Volgende", "next": "Next",
"back": "Terug", "back": "Back",
"continue": "Doorgaan", "continue": "Continue",
"verifying": "Verifiëren..." "verifying": "Verifying..."
}, },
"search": { "search": {
"search": "Zoek...", "search": "Zoek...",
@@ -521,10 +521,10 @@
"episodes": "Afleveringen", "episodes": "Afleveringen",
"collections": "Collecties", "collections": "Collecties",
"actors": "Acteurs", "actors": "Acteurs",
"artists": "Artiesten", "artists": "Artists",
"albums": "Albums", "albums": "Albums",
"songs": "Nummers", "songs": "Songs",
"playlists": "Afspeellijsten", "playlists": "Playlists",
"request_movies": "Vraag films aan", "request_movies": "Vraag films aan",
"request_series": "Vraag series aan", "request_series": "Vraag series aan",
"recently_added": "Recent Toegevoegd", "recently_added": "Recent Toegevoegd",
@@ -572,7 +572,7 @@
"genres": "Genres", "genres": "Genres",
"years": "Jaren", "years": "Jaren",
"sort_by": "Sorteren op", "sort_by": "Sorteren op",
"filter_by": "Filteren op", "filter_by": "Filter By",
"sort_order": "Sorteer volgorde", "sort_order": "Sorteer volgorde",
"tags": "Labels" "tags": "Labels"
} }
@@ -719,127 +719,127 @@
"favorites": "Favorieten" "favorites": "Favorieten"
}, },
"music": { "music": {
"title": "Muziek", "title": "Music",
"tabs": { "tabs": {
"suggestions": "Suggesties", "suggestions": "Suggestions",
"albums": "Albums", "albums": "Albums",
"artists": "Artiesten", "artists": "Artists",
"playlists": "Afspeellijsten", "playlists": "Playlists",
"tracks": "Nummers" "tracks": "tracks"
}, },
"filters": { "filters": {
"all": "Alle" "all": "All"
}, },
"recently_added": "Recent toegevoegd", "recently_added": "Recently Added",
"recently_played": "Onlangs afgespeeld", "recently_played": "Recently Played",
"frequently_played": "Vaak afgespeeld", "frequently_played": "Frequently Played",
"explore": "Ontdek", "explore": "Explore",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Afspelen", "play": "Play",
"shuffle": "Shuffle", "shuffle": "Shuffle",
"play_top_tracks": "Play Top Tracks", "play_top_tracks": "Play Top Tracks",
"no_suggestions": "Geen suggesties beschikbaar", "no_suggestions": "No suggestions available",
"no_albums": "Geen albums gevonden", "no_albums": "No albums found",
"no_artists": "Geen artiesten gevonden", "no_artists": "No artists found",
"no_playlists": "Geen afspeellijsten gevonden", "no_playlists": "No playlists found",
"album_not_found": "Album niet gevonden", "album_not_found": "Album not found",
"artist_not_found": "Artiest niet gevonden", "artist_not_found": "Artist not found",
"playlist_not_found": "Afspeellijst niet gevonden", "playlist_not_found": "Playlist not found",
"track_options": { "track_options": {
"play_next": "Speel volgende af", "play_next": "Play Next",
"add_to_queue": "Toevoegen aan wachtrij", "add_to_queue": "Add to Queue",
"add_to_playlist": "Voeg toe aan afspeellijst", "add_to_playlist": "Add to Playlist",
"download": "Downloaden", "download": "Download",
"downloaded": "Gedownload", "downloaded": "Downloaded",
"downloading": "Downloaden...", "downloading": "Downloading...",
"cached": "Gecached", "cached": "Cached",
"delete_download": "Download verwijderen", "delete_download": "Delete Download",
"delete_cache": "Verwijderen uit cache", "delete_cache": "Remove from Cache",
"go_to_artist": "Ga naar artiest", "go_to_artist": "Go to Artist",
"go_to_album": "Ga naar album", "go_to_album": "Go to Album",
"add_to_favorites": "Toevoegen aan favorieten", "add_to_favorites": "Add to Favorites",
"remove_from_favorites": "Verwijderen uit favorieten", "remove_from_favorites": "Remove from Favorites",
"remove_from_playlist": "Verwijder uit afspeellijst" "remove_from_playlist": "Remove from Playlist"
}, },
"playlists": { "playlists": {
"create_playlist": "Afspeellijst aanmaken", "create_playlist": "Create Playlist",
"playlist_name": "Afspeellijst naam", "playlist_name": "Playlist Name",
"enter_name": "Enter playlist name", "enter_name": "Enter playlist name",
"create": "Aanmaken", "create": "Create",
"search_playlists": "Playlist zoeken...", "search_playlists": "Search playlists...",
"added_to": "Toegevoegd aan {{name}}", "added_to": "Added to {{name}}",
"added": "Toegevoegd aan playlist", "added": "Added to playlist",
"removed_from": "Verwijderd uit {{name}}", "removed_from": "Removed from {{name}}",
"removed": "Verwijderd uit playlist", "removed": "Removed from playlist",
"created": "Playlist created", "created": "Playlist created",
"create_new": "Create New Playlist", "create_new": "Create New Playlist",
"failed_to_add": "Failed to add to playlist", "failed_to_add": "Failed to add to playlist",
"failed_to_remove": "Verwijderen uit afspeellijst is mislukt", "failed_to_remove": "Failed to remove from playlist",
"failed_to_create": "Het maken van de afspeellijst is mislukt", "failed_to_create": "Failed to create playlist",
"delete_playlist": "Afspeellijst verwijderen", "delete_playlist": "Delete Playlist",
"delete_confirm": "Weet u zeker dat u \"{{name}}\"wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", "delete_confirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"deleted": "Afspeellijst verwijderd.", "deleted": "Playlist deleted",
"failed_to_delete": "Verwijderen van afspeellijst mislukt" "failed_to_delete": "Failed to delete playlist"
}, },
"sort": { "sort": {
"title": "Sorteren op", "title": "Sort By",
"alphabetical": "Alfabetisch", "alphabetical": "Alphabetical",
"date_created": "Aanmaakdatum" "date_created": "Date Created"
} }
}, },
"watchlists": { "watchlists": {
"title": "Watchlist", "title": "Watchlists",
"my_watchlists": "Mijn watchlists", "my_watchlists": "My Watchlists",
"public_watchlists": "Public Watchlists", "public_watchlists": "Public Watchlists",
"create_title": "Create Watchlist", "create_title": "Create Watchlist",
"edit_title": "Edit Watchlist", "edit_title": "Edit Watchlist",
"create_button": "Create Watchlist", "create_button": "Create Watchlist",
"save_button": "Wijzigingen opslaan", "save_button": "Save Changes",
"delete_button": "Verwijder", "delete_button": "Delete",
"remove_button": "Verwijderen", "remove_button": "Remove",
"cancel_button": "Annuleren", "cancel_button": "Cancel",
"name_label": "Naam", "name_label": "Name",
"name_placeholder": "Voer naam van kijklijst in", "name_placeholder": "Enter watchlist name",
"description_label": "Beschrijving", "description_label": "Description",
"description_placeholder": "Voer beschrijving in (optioneel)", "description_placeholder": "Enter description (optional)",
"is_public_label": "Openbare Kijklijst", "is_public_label": "Public Watchlist",
"is_public_description": "Sta anderen toe om deze kijklijst te bekijken", "is_public_description": "Allow others to view this watchlist",
"allowed_type_label": "Inhoudstype", "allowed_type_label": "Content Type",
"sort_order_label": "Standaard Sortering", "sort_order_label": "Default Sort Order",
"empty_title": "Geen Kijklijsten", "empty_title": "No Watchlists",
"empty_description": "Maak je eerste kijklijst om je media te organiseren", "empty_description": "Create your first watchlist to start organizing your media",
"empty_watchlist": "Deze watchlist is leeg", "empty_watchlist": "This watchlist is empty",
"empty_watchlist_hint": "Voeg items uit je bibliotheek toe aan deze kijklijst", "empty_watchlist_hint": "Add items from your library to this watchlist",
"not_configured_title": "Streamystats niet geconfigureerd", "not_configured_title": "Streamystats Not Configured",
"not_configured_description": "Configureer Streamystats in instellingen om kijklijsten te gebruiken", "not_configured_description": "Configure Streamystats in settings to use watchlists",
"go_to_settings": "Ga naar Instellingen", "go_to_settings": "Go to Settings",
"add_to_watchlist": "Voeg toe aan kijklijst", "add_to_watchlist": "Add to Watchlist",
"remove_from_watchlist": "Verwijder van kijklijst", "remove_from_watchlist": "Remove from Watchlist",
"select_watchlist": "Selecteer kijklijst", "select_watchlist": "Select Watchlist",
"create_new": "Nieuwe kijklijst aanmaken", "create_new": "Create New Watchlist",
"item": "item", "item": "item",
"items": "items", "items": "items",
"public": "Publiek", "public": "Public",
"private": "Privé", "private": "Private",
"you": "Jij", "you": "You",
"by_owner": "Door een andere gebruiker", "by_owner": "By another user",
"not_found": "Kijklijst niet gevonden", "not_found": "Watchlist not found",
"delete_confirm_title": "Verwijder kijklijst", "delete_confirm_title": "Delete Watchlist",
"delete_confirm_message": "Weet u zeker dat u \"{{name}}\"wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", "delete_confirm_message": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"remove_item_title": "Verwijder van watchlist", "remove_item_title": "Remove from Watchlist",
"remove_item_message": "Verwijder \"{{name}}\" uit deze watchlist?", "remove_item_message": "Remove \"{{name}}\" from this watchlist?",
"loading": "Laden van watchlists...", "loading": "Loading watchlists...",
"no_compatible_watchlists": "Geen compatibele watchlist", "no_compatible_watchlists": "No compatible watchlists",
"create_one_first": "Maak een watchlist aan die dit inhoudstype accepteert" "create_one_first": "Create a watchlist that accepts this content type"
}, },
"playback_speed": { "playback_speed": {
"title": "Afspeelsnelheid", "title": "Playback Speed",
"apply_to": "Toepassen op", "apply_to": "Apply To",
"speed": "Snelheid", "speed": "Speed",
"scope": { "scope": {
"media": "Alleen deze media", "media": "This media only",
"show": "Deze serie", "show": "This show",
"all": "Alle media (standaard)" "all": "All media (default)"
} }
} }
} }

View File

@@ -34,44 +34,44 @@
"search_for_local_servers": "Поиск локальных серверов", "search_for_local_servers": "Поиск локальных серверов",
"searching": "Поиск...", "searching": "Поиск...",
"servers": "Сервера", "servers": "Сервера",
"saved": "Сохранено", "saved": "Saved",
"session_expired": "Сессия истекла", "session_expired": "Session Expired",
"please_login_again": "Ваша сессия истекла. Пожалуйста, войдите снова.", "please_login_again": "Your saved session has expired. Please log in again.",
"remove_saved_login": "Удалить сохраненный аккаунт", "remove_saved_login": "Remove Saved Login",
"remove_saved_login_description": "Ваши сохранённые данные для входа от этого сервера будут удалены. Вам придётся ввести ваши логин и пароль ещё раз.", "remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
"accounts_count": "{{count}} аккаунтов", "accounts_count": "{{count}} accounts",
"select_account": "Выбрать аккаунт", "select_account": "Select Account",
"add_account": "Добавить аккаунт", "add_account": "Add Account",
"remove_account_description": "Данные для входа {{username}} будут удалены." "remove_account_description": "This will remove the saved credentials for {{username}}."
}, },
"save_account": { "save_account": {
"title": "Сохранить аккаунт", "title": "Save Account",
"save_for_later": "Сохранить этот аккаунт", "save_for_later": "Save this account",
"security_option": "Опции безопасности", "security_option": "Security Option",
"no_protection": "Без защиты", "no_protection": "No protection",
"no_protection_desc": "Быстрый вход без ввода данных", "no_protection_desc": "Quick login without authentication",
"pin_code": "PIN-код", "pin_code": "PIN code",
"pin_code_desc": "При переключении будет требоваться 4-значный PIN", "pin_code_desc": "4-digit PIN required when switching",
"password": "Пароль", "password": "Re-enter password",
"password_desc": "При переключении будет требоваться пароль", "password_desc": "Password required when switching",
"save_button": "Сохранить", "save_button": "Save",
"cancel_button": "Отмена" "cancel_button": "Cancel"
}, },
"pin": { "pin": {
"enter_pin": "Введите PIN", "enter_pin": "Enter PIN",
"enter_pin_for": "Введите PIN для {{username}}", "enter_pin_for": "Enter PIN for {{username}}",
"enter_4_digits": "Введите 4 цифры", "enter_4_digits": "Enter 4 digits",
"invalid_pin": "Некорректный PIN", "invalid_pin": "Invalid PIN",
"setup_pin": "Установить PIN", "setup_pin": "Set Up PIN",
"confirm_pin": "Подтвердите PIN", "confirm_pin": "Confirm PIN",
"pins_dont_match": "PIN-коды не совпадают", "pins_dont_match": "PINs don't match",
"forgot_pin": "Забыли PIN?", "forgot_pin": "Forgot PIN?",
"forgot_pin_desc": "Ваши данные для входа будут удалены" "forgot_pin_desc": "Your saved credentials will be removed"
}, },
"password": { "password": {
"enter_password": "Введите пароль", "enter_password": "Enter Password",
"enter_password_for": "Введите пароль для {{username}}", "enter_password_for": "Enter password for {{username}}",
"invalid_password": "Неверный пароль" "invalid_password": "Invalid password"
}, },
"home": { "home": {
"checking_server_connection": "Проверка соединения с сервером...", "checking_server_connection": "Проверка соединения с сервером...",
@@ -82,12 +82,12 @@
"go_to_downloads": "В загрузки", "go_to_downloads": "В загрузки",
"retry": "Повторить", "retry": "Повторить",
"server_unreachable": "Сервер недоступен", "server_unreachable": "Сервер недоступен",
"server_unreachable_message": "Не удалось соединиться с сервером.\nПожалуйста, проверьте настройки сети.", "server_unreachable_message": "Could not reach the server.\nPlease check your network connection.",
"oops": "Упс!", "oops": "Упс!",
"error_message": "Что-то пошло не так.\nПожалуйста выйдите и зайдите снова.", "error_message": "Что-то пошло не так.\nПожалуйста выйдите и зайдите снова.",
"continue_watching": "Продолжить", "continue_watching": "Продолжить просмотр",
"next_up": "Далее", "next_up": "Следующее",
"continue_and_next_up": "Продолжить и Далее", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Недавно добавлено в {{libraryName}}", "recently_added_in": "Недавно добавлено в {{libraryName}}",
"suggested_movies": "Предложенные фильмы", "suggested_movies": "Предложенные фильмы",
"suggested_episodes": "Предложенные серии", "suggested_episodes": "Предложенные серии",
@@ -110,46 +110,46 @@
"settings_title": "Настройки", "settings_title": "Настройки",
"log_out_button": "Выйти", "log_out_button": "Выйти",
"categories": { "categories": {
"title": "Категории" "title": "Categories"
}, },
"playback_controls": { "playback_controls": {
"title": "Воспроизведение и управление" "title": "Playback & Controls"
}, },
"audio_subtitles": { "audio_subtitles": {
"title": "Аудио и субтитры" "title": "Audio & Subtitles"
}, },
"appearance": { "appearance": {
"title": "Внешний вид", "title": "Appearance",
"merge_next_up_continue_watching": "Объединить «Продолжить» и «Далее»", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Скрыть кнопку «Удалённый сеанс»" "hide_remote_session_button": "Hide Remote Session Button"
}, },
"network": { "network": {
"title": "Сеть", "title": "Network",
"local_network": "Локальная сеть", "local_network": "Local Network",
"auto_switch_enabled": "Переключаться дома автоматически", "auto_switch_enabled": "Auto-switch when at home",
"auto_switch_description": "Автоматически переключаться на локальный URL при присоединении к домашней WiFi сети", "auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
"local_url": "Локальный URL", "local_url": "Local URL",
"local_url_hint": "Введите локальный URL вашего сервера (e.g., http://192.168.1.100:8096)", "local_url_hint": "Enter your local server address (e.g., http://192.168.1.100:8096)",
"local_url_placeholder": "http://192.168.1.100:8096", "local_url_placeholder": "http://192.168.1.100:8096",
"home_wifi_networks": "Домашние WiFi сети", "home_wifi_networks": "Home WiFi Networks",
"add_current_network": "Добавить \"{{ssid}}\"", "add_current_network": "Add \"{{ssid}}\"",
"not_connected_to_wifi": "Нет WiFi соединения", "not_connected_to_wifi": "Not connected to WiFi",
"no_networks_configured": "Нет настроенных сетей", "no_networks_configured": "No networks configured",
"add_network_hint": "Добавьте вашу домашнюю сеть WiFi для включения автоматического переключения", "add_network_hint": "Add your home WiFi network to enable auto-switching",
"current_wifi": "Текущая WiFi сеть", "current_wifi": "Current WiFi",
"using_url": "Используется", "using_url": "Using",
"local": "Локальный", "local": "Local URL",
"remote": "Внешний", "remote": "Remote URL",
"not_connected": "Нет соединения", "not_connected": "Not connected",
"current_server": "Текущий сервер", "current_server": "Current Server",
"remote_url": "Внешний URL", "remote_url": "Remote URL",
"active_url": "Активный URL", "active_url": "Active URL",
"not_configured": "Не настроено", "not_configured": "Not configured",
"network_added": "Сеть добавлена", "network_added": "Network added",
"network_already_added": "Сеть уже добавлена", "network_already_added": "Network already added",
"no_wifi_connected": "Нет WiFi соединения", "no_wifi_connected": "Not connected to WiFi",
"permission_denied": "Нет доступа к местоположению", "permission_denied": "Location permission denied",
"permission_denied_explanation": "Разрешение на доступ к местоположению обязательно для обнаружения WiFi сети при автоматическом переключении. Пожалуйста, включите его в настройках." "permission_denied_explanation": "Location permission is required to detect WiFi network for auto-switching. Please enable it in Settings."
}, },
"user_info": { "user_info": {
"user_info_title": "Информация о пользователе", "user_info_title": "Информация о пользователе",
@@ -170,22 +170,22 @@
}, },
"media_controls": { "media_controls": {
"media_controls_title": "Медиа-контроль", "media_controls_title": "Медиа-контроль",
"forward_skip_length": "Шаг перемотки вперёд", "forward_skip_length": "Длина пропуска вперед",
"rewind_length": "Шаг перемотки назад", "rewind_length": "Длина перемотки",
"seconds_unit": "c" "seconds_unit": "c"
}, },
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Управление жестами", "gesture_controls_title": "Управление жестами",
"horizontal_swipe_skip": "Горизонтальный свайп для перемотки", "horizontal_swipe_skip": "Горизонтальный свайп, чтобы пропустить",
"horizontal_swipe_skip_description": "Проведите влево/вправо, когда элементы управления скрыты, чтобы пропустить", "horizontal_swipe_skip_description": "Проведите влево/вправо, когда элементы управления скрыты, чтобы пропустить",
"left_side_brightness": "Управление яркостью левой стороны", "left_side_brightness": "Управление яркостью левой стороны",
"left_side_brightness_description": "Смахните вверх/вниз на левой стороне для настройки яркости", "left_side_brightness_description": "Смахните вверх/вниз на левой стороне для настройки яркости",
"right_side_volume": "Управление громкостью справа", "right_side_volume": "Управление громкостью справа",
"right_side_volume_description": "Свайп вверх/вниз с правой стороны для настройки громкости", "right_side_volume_description": "Свайп вверх/вниз с правой стороны для настройки громкости",
"hide_volume_slider": "Скрыть индикатор громкости", "hide_volume_slider": "Hide Volume Slider",
"hide_volume_slider_description": "Скрывает индикатор громкости в плеере", "hide_volume_slider_description": "Hide the volume slider in the video player",
"hide_brightness_slider": "Скрыть индикатор яркости", "hide_brightness_slider": "Hide Brightness Slider",
"hide_brightness_slider_description": "Скрывает индикатор яркости в плеере" "hide_brightness_slider_description": "Hide the brightness slider in the video player"
}, },
"audio": { "audio": {
"audio_title": "Аудио", "audio_title": "Аудио",
@@ -195,17 +195,17 @@
"none": "Отсутствует", "none": "Отсутствует",
"language": "Язык", "language": "Язык",
"transcode_mode": { "transcode_mode": {
"title": "Перекодировка аудио", "title": "Audio Transcoding",
"description": "Управляет обработкой пространственного звука (7.1, TrueHD, DTS-HD)", "description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
"auto": "Авто", "auto": "Auto",
"stereo": "Принудительно в стерео", "stereo": "Force Stereo",
"5_1": "Разрешить 5.1", "5_1": "Allow 5.1",
"passthrough": "Ничего не изменять" "passthrough": "Passthrough"
} }
}, },
"subtitles": { "subtitles": {
"subtitle_title": "Субтитры", "subtitle_title": "Субтитры",
"subtitle_hint": "Настройки отображения субтитров", "subtitle_hint": "Настроить субтитры.",
"subtitle_language": "Язык субтитров", "subtitle_language": "Язык субтитров",
"subtitle_mode": "Режим субтитров", "subtitle_mode": "Режим субтитров",
"set_subtitle_track": "Устанавливать субтитры из предыдущего элемента", "set_subtitle_track": "Устанавливать субтитры из предыдущего элемента",
@@ -226,24 +226,24 @@
"outline_thickness": "Толщина контура", "outline_thickness": "Толщина контура",
"background_opacity": "Прозрачность фона", "background_opacity": "Прозрачность фона",
"outline_opacity": "Прозрачность контура", "outline_opacity": "Прозрачность контура",
"bold_text": "Жирный", "bold_text": "Bold Text",
"colors": { "colors": {
"Black": "Черный", "Black": "Черный",
"Gray": "Серый", "Gray": "Серый",
"Silver": "Серебристый", "Silver": "Серебряный",
"White": "Белый", "White": "Белый",
"Maroon": "Бордовый", "Maroon": "Марун",
"Red": "Красный", "Red": "Красный",
"Fuchsia": "Пурпурный", "Fuchsia": "Fuchsia",
"Yellow": "Жёлтый", "Yellow": "Жёлтый",
"Olive": "Оливковый", "Olive": "Олив",
"Green": "Зелёный", "Green": "Зелёный",
"Teal": "Бирюзовый", "Teal": "Бирюзовый",
"Lime": "Лаймовый", "Lime": "Лаймовый",
"Purple": "Фиолетовый", "Purple": "Фиолетовый",
"Navy": "Тёмно-синий", "Navy": "Тёмно-синий",
"Blue": "Синий", "Blue": "Синий",
"Aqua": "Голубой" "Aqua": "Акваа"
}, },
"thickness": { "thickness": {
"None": "Отсутствует", "None": "Отсутствует",
@@ -251,29 +251,29 @@
"Normal": "Обычный", "Normal": "Обычный",
"Thick": "Толстый" "Thick": "Толстый"
}, },
"subtitle_color": "Цвет субтитров", "subtitle_color": "Subtitle Color",
"subtitle_background_color": "Цвет фона", "subtitle_background_color": "Background Color",
"subtitle_font": "Шрифт субтитров", "subtitle_font": "Subtitle Font",
"ksplayer_title": "Настройки KSPlayer", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Аппаратное декодирование", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Использовать аппаратное ускорение для декодирования видео. Выключите, если наблюдаете проблемы с воспроизведением." "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "Настройки субтитров в VLC", "title": "VLC Subtitle Settings",
"hint": "Настройте внешний вид субтитров в VLC плеере. Изменения применятся при следующем воспроизведении.", "hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Цвет текста", "text_color": "Text Color",
"background_color": "Цвет фона", "background_color": "Background Color",
"background_opacity": "Прозрачность фона", "background_opacity": "Background Opacity",
"outline_color": "Цвет контура", "outline_color": "Outline Color",
"outline_opacity": "Прозрачность контура", "outline_opacity": "Outline Opacity",
"outline_thickness": "Толщина контура", "outline_thickness": "Outline Thickness",
"bold": "Жирный", "bold": "Bold Text",
"margin": "Отступ снизу" "margin": "Bottom Margin"
}, },
"video_player": { "video_player": {
"title": "Видеоплеер", "title": "Video Player",
"video_player": "Видеоплеер", "video_player": "Video Player",
"video_player_description": "Выберите видеоплеер в iOS.", "video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer", "ksplayer": "KSPlayer",
"vlc": "VLC" "vlc": "VLC"
}, },
@@ -294,19 +294,19 @@
"UNKNOWN": "Неизвестное" "UNKNOWN": "Неизвестное"
}, },
"safe_area_in_controls": "Безопасная зона в элементах управления", "safe_area_in_controls": "Безопасная зона в элементах управления",
"video_player": "Видеоплеер", "video_player": "Видео прейер",
"video_players": { "video_players": {
"VLC_3": "VLC 3", "VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Экспериментальный + PiP)" "VLC_4": "VLC 4 (Экспериментальный + PiP)"
}, },
"show_custom_menu_links": "Показать ссылки кастомного меню", "show_custom_menu_links": "Показать ссылки кастомного меню",
"show_large_home_carousel": "Показывать большую карусель (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"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": "Качество по умолчанию",
"default_playback_speed": "Скорость воспроизведения по умолчанию", "default_playback_speed": "Default Playback Speed",
"auto_play_next_episode": "Автоматически воспроизводить следующий эпизод", "auto_play_next_episode": "Auto-play Next Episode",
"max_auto_play_episode_count": "Максимальное количество автовоспроизведения эпизодов", "max_auto_play_episode_count": "Максимальное количество автовоспроизведения эпизодов",
"disabled": "Отключено" "disabled": "Отключено"
}, },
@@ -314,15 +314,15 @@
"downloads_title": "Загрузки" "downloads_title": "Загрузки"
}, },
"music": { "music": {
"title": "Музыка", "title": "Music",
"playback_title": "Воспроизведение", "playback_title": "Playback",
"playback_description": "Настройте воспроизведение музыки.", "playback_description": "Configure how music is played.",
"prefer_downloaded": "Предпочитать скачанные песни", "prefer_downloaded": "Prefer Downloaded Songs",
"caching_title": "Кеширование", "caching_title": "Caching",
"caching_description": "Автоматически предкешировать следующие треки для стабильного воспроизведения.", "caching_description": "Automatically cache upcoming tracks for smoother playback.",
"lookahead_enabled": "Включить предкеширование", "lookahead_enabled": "Enable Look-Ahead Caching",
"lookahead_count": "Сколько предкешировать", "lookahead_count": "Tracks to Pre-cache",
"max_cache_size": "Максимальное число предкешированных треков" "max_cache_size": "Max Cache Size"
}, },
"plugins": { "plugins": {
"plugins_title": "Плагины", "plugins_title": "Плагины",
@@ -357,39 +357,39 @@
"save_button": "Сохранить", "save_button": "Сохранить",
"toasts": { "toasts": {
"saved": "Сохранено", "saved": "Сохранено",
"refreshed": "Настройки обновлены с сервера" "refreshed": "Settings refreshed from server"
}, },
"refresh_from_server": "Обновить настройки с сервера" "refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Включить Streamystats", "enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Выключить Streamystats", "disable_streamystats": "Disable Streamystats",
"enable_search": "Использовать в поиске", "enable_search": "Use for Search",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Введите URL вашего сервера Streamystats. URL должен включать http/https и порт при необходимости.", "streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
"read_more_about_streamystats": "Узнать больше про Streamystats.", "read_more_about_streamystats": "Read More About Streamystats.",
"save_button": "Сохранить", "save_button": "Save",
"save": "Сохранить", "save": "Save",
"features_title": "Функции", "features_title": "Features",
"home_sections_title": "Показывать на главной", "home_sections_title": "Home Sections",
"enable_movie_recommendations": "Рекомендации фильмов", "enable_movie_recommendations": "Movie Recommendations",
"enable_series_recommendations": "Рекомендации сериалов", "enable_series_recommendations": "Series Recommendations",
"enable_promoted_watchlists": "Продвигаемые списки просмотра", "enable_promoted_watchlists": "Promoted Watchlists",
"hide_watchlists_tab": "Скрыть вкладку со списками", "hide_watchlists_tab": "Hide Watchlists Tab",
"home_sections_hint": "Показывать персонализированные рекомендации и подходящие списки просмотров из Streamystats на главной странице.", "home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.",
"recommended_movies": "Рекомендованные фильмы", "recommended_movies": "Recommended Movies",
"recommended_series": "Рекомендованные сериалы", "recommended_series": "Recommended Series",
"toasts": { "toasts": {
"saved": "Сохранено", "saved": "Saved",
"refreshed": "Настройки обновлены с сервера", "refreshed": "Settings refreshed from server",
"disabled": "Streamystats отключен" "disabled": "Streamystats disabled"
}, },
"refresh_from_server": "Обновить настройки с сервера" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Включить интеграцию со списками просмотра", "watchlist_enabler": "Enable our Watchlist integration",
"watchlist_button": "Изменить интеграцию со списками просмотра" "watchlist_button": "Toggle Watchlist integration"
} }
}, },
"storage": { "storage": {
@@ -398,18 +398,18 @@
"device_usage": "Устройство {{availableSpace}}%", "device_usage": "Устройство {{availableSpace}}%",
"size_used": "{{used}} из {{total}} использовано", "size_used": "{{used}} из {{total}} использовано",
"delete_all_downloaded_files": "Удалить все загруженные файлы", "delete_all_downloaded_files": "Удалить все загруженные файлы",
"music_cache_title": "Кеш музыки", "music_cache_title": "Music Cache",
"music_cache_description": "Автоматически прекешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Кешировать музыку", "enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Очистить кеш музыки", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} кешировано", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Кеш музыки очищен", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Удалить все скачанные песни", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} скачано", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Скачанные песни удалены" "downloaded_songs_deleted": "Downloaded songs deleted"
}, },
"intro": { "intro": {
"title": "Вступление", "title": "Intro",
"show_intro": "Показать вступление", "show_intro": "Показать вступление",
"reset_intro": "Сбросить вступление" "reset_intro": "Сбросить вступление"
}, },
@@ -441,24 +441,24 @@
"tvseries": "Сериалы", "tvseries": "Сериалы",
"movies": "Фильмы", "movies": "Фильмы",
"queue": "Очередь", "queue": "Очередь",
"other_media": "Прочие файлы", "other_media": "Другие медиа",
"queue_hint": "Очередь очистится после перезапуска", "queue_hint": "Очередь и загрузки будут удалены при перезагрузке приложения",
"no_items_in_queue": "Нет элементов в очереди", "no_items_in_queue": "Нет элементов в очереди",
"no_downloaded_items": "Нет загруженных файлов", "no_downloaded_items": "Нет загруженых предметов",
"delete_all_movies_button": "Удалить все фильмы", "delete_all_movies_button": "Удалить все фильмы",
"delete_all_tvseries_button": "Удалить все сериалы", "delete_all_tvseries_button": "Удалить все сериалы",
"delete_all_button": "Удалить все", "delete_all_button": "Удалить все",
"delete_all_other_media_button": "Удалить прочие файлы", "delete_all_other_media_button": "Удалить другой материал",
"active_download": "Загружается", "active_download": "Активно загружается",
"no_active_downloads": "Нет активных загрузок", "no_active_downloads": "Нет активных загрузок",
"active_downloads": "Активные", "active_downloads": "Активные загрузки",
"new_app_version_requires_re_download": "Новая версия приложения требует повторной загрузки", "new_app_version_requires_re_download": "Новая версия приложения требует повторной загрузки",
"new_app_version_requires_re_download_description": "Новая версия приложения требует повторной загрузки. Пожалуйста удалите всё и попробуйте заново.", "new_app_version_requires_re_download_description": "Новая версия приложения требует повторной загрузки. Пожалуйста удалите всё и попробуйте заново.",
"back": "Назад", "back": "Назад",
"delete": "Удалить", "delete": "Удалить",
"something_went_wrong": "Что-то пошло не так", "something_went_wrong": "Что-то пошло не так",
"could_not_get_stream_url_from_jellyfin": "Не удалось получить ссылку трансляции из Jellyfin", "could_not_get_stream_url_from_jellyfin": "Не удалось получить ссылку трансляции из Jellyfin",
"eta": "Осталось {{eta}}", "eta": "ETA {{eta}}",
"toasts": { "toasts": {
"you_are_not_allowed_to_download_files": "Нет разрешения на скачивание файлов.", "you_are_not_allowed_to_download_files": "Нет разрешения на скачивание файлов.",
"deleted_all_movies_successfully": "Все фильмы были успешно удалены!", "deleted_all_movies_successfully": "Все фильмы были успешно удалены!",
@@ -467,64 +467,64 @@
"failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов", "failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов",
"deleted_media_successfully": "Другие носители успешно удалены!", "deleted_media_successfully": "Другие носители успешно удалены!",
"failed_to_delete_media": "Не удалось удалить другой файл", "failed_to_delete_media": "Не удалось удалить другой файл",
"download_deleted": "Удалено", "download_deleted": "Загрузка удалена",
"download_cancelled": "Загрузка отменена", "download_cancelled": "Загрузка отменена",
"could_not_delete_download": "Не удалось удалить загрузку", "could_not_delete_download": "Не удалось удалить загрузку",
"download_paused": "На паузе", "download_paused": "Загрузка приостановлена",
"could_not_pause_download": "Не удалось приостановить загрузку", "could_not_pause_download": "Не удалось приостановить загрузку",
"download_resumed": "Продолжено", "download_resumed": "Загрузка возобновлена",
"could_not_resume_download": "Не удалось продолжить загрузку", "could_not_resume_download": "Не удалось продолжить загрузку",
"download_completed": "Завершено", "download_completed": "Загрузка завершена",
"download_failed": "Не удалось загрузить", "download_failed": "Download Failed",
"download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}", "download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}",
"download_completed_for_item": "{{item}} успешно загружен", "download_completed_for_item": "{{item}} успешно загружен",
"download_started_for_item": "Загрузка началась для {{item}}", "download_started_for_item": "Загрузка началась для {{item}}",
"failed_to_start_download": "Не удалось начать загрузку", "failed_to_start_download": "Не удалось начать загрузку",
"item_already_downloading": "{{item}} уже загружается", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "Все загрузки удалены", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} удалён(о)", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Все файлы, папки, и задачи были успешно удалены", "all_files_folders_and_jobs_deleted_successfully": "Все файлы, папки, и задачи были успешно удалены",
"failed_to_clean_cache_directory": "Не удалось очистить директорию кэша", "failed_to_clean_cache_directory": "Не удалось очистить директорию кэша",
"could_not_get_download_url_for_item": "Не удалось получить URL загрузки для {{itemName}}", "could_not_get_download_url_for_item": "Не удалось получить URL загрузки для {{itemName}}",
"go_to_downloads": "В загрузки", "go_to_downloads": "В загрузки",
"file_deleted": "{{item}} удалён" "file_deleted": "{{item}} deleted"
} }
} }
}, },
"common": { "common": {
"select": "Выбрать", "select": "Выбрать",
"no_trailer_available": "Трейлер недоступен", "no_trailer_available": "Прицеп недоступен",
"video": "Видео", "video": "Видео",
"audio": "Звук", "audio": "Звук",
"subtitle": "Субтитры", "subtitle": "Субтитры",
"play": "Воспроизвести", "play": "Играть",
"none": "Отсутствует", "none": "None",
"track": "Трек", "track": "Track",
"cancel": "Отмена", "cancel": "Cancel",
"delete": "Удалить", "delete": "Delete",
"ok": "ОК", "ok": "OK",
"remove": "Удалить", "remove": "Remove",
"next": "Вперед", "next": "Next",
"back": "Назад", "back": "Back",
"continue": "Продолжить", "continue": "Continue",
"verifying": "Проверка..." "verifying": "Verifying..."
}, },
"search": { "search": {
"search": "Поиск...", "search": "Поиск...",
"x_items": "{{count}} элементов", "x_items": "{{count}} предметов",
"library": "Библиотека", "library": "Библиотека",
"discover": "Найти новое", "discover": "Найти новое",
"no_results": "Ничего не найдено", "no_results": "Нет результатов",
"no_results_found_for": "Ничего не найдено по запросу", "no_results_found_for": "Не было результатов при поиске",
"movies": "Фильмы", "movies": "Фильмы",
"series": "Сериалы", "series": "Сериалы",
"episodes": "Серии", "episodes": "Серии",
"collections": "Коллекции", "collections": "Коллекции",
"actors": "Актеры", "actors": "Актеры",
"artists": "Артисты", "artists": "Artists",
"albums": "Альбомы", "albums": "Albums",
"songs": "Песни", "songs": "Songs",
"playlists": "Плейлисты", "playlists": "Playlists",
"request_movies": "Запросить фильмы", "request_movies": "Запросить фильмы",
"request_series": "Запросить сериалы", "request_series": "Запросить сериалы",
"recently_added": "Недавно добавлено", "recently_added": "Недавно добавлено",
@@ -553,7 +553,7 @@
"no_results": "Нет результатов", "no_results": "Нет результатов",
"no_libraries_found": "Библиотеки не найдены", "no_libraries_found": "Библиотеки не найдены",
"item_types": { "item_types": {
"movies": "Фильмы", "movies": "фильмы",
"series": "Сериалы", "series": "Сериалы",
"boxsets": "Коллекции", "boxsets": "Коллекции",
"items": "элементы" "items": "элементы"
@@ -571,9 +571,9 @@
"filters": { "filters": {
"genres": "Жанры", "genres": "Жанры",
"years": "Года", "years": "Года",
"sort_by": "Сортировка", "sort_by": "Сортировать по",
"filter_by": "Фильтр", "filter_by": "Filter By",
"sort_order": "Порядок", "sort_order": "Порядок сортировки",
"tags": "Тэги" "tags": "Тэги"
} }
}, },
@@ -604,14 +604,14 @@
"index": "Индекс:", "index": "Индекс:",
"continue_watching": "Продолжить просмотр", "continue_watching": "Продолжить просмотр",
"go_back": "Назад", "go_back": "Назад",
"downloaded_file_title": "Этот файл уже скачан", "downloaded_file_title": "You have this file downloaded",
"downloaded_file_message": "Хотите воспроизвести скачанный файл?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Да", "downloaded_file_yes": "Yes",
"downloaded_file_no": "Нет", "downloaded_file_no": "No",
"downloaded_file_cancel": "Отмена" "downloaded_file_cancel": "Cancel"
}, },
"item_card": { "item_card": {
"next_up": "Далее", "next_up": "Следующее",
"no_items_to_display": "Нет элементов для отображения", "no_items_to_display": "Нет элементов для отображения",
"cast_and_crew": "Актеры и съемочная группа", "cast_and_crew": "Актеры и съемочная группа",
"series": "Серии", "series": "Серии",
@@ -644,7 +644,7 @@
} }
}, },
"live_tv": { "live_tv": {
"next": "Далее", "next": "Следующая",
"previous": "Предыдущая", "previous": "Предыдущая",
"coming_soon": "Скоро", "coming_soon": "Скоро",
"on_now": "Сейчас в эфире", "on_now": "Сейчас в эфире",
@@ -675,7 +675,7 @@
"series_type": "Тип сериала", "series_type": "Тип сериала",
"release_dates": "Дата релиза", "release_dates": "Дата релиза",
"first_air_date": "Первая дата выхода в эфир", "first_air_date": "Первая дата выхода в эфир",
"next_air_date": "Ближайшая дата выхода в эфир", "next_air_date": "Следующая дата выхода в эфир",
"revenue": "Прибыль", "revenue": "Прибыль",
"budget": "Бюджет", "budget": "Бюджет",
"original_language": "Оригинальный язык", "original_language": "Оригинальный язык",
@@ -693,10 +693,10 @@
"number_episodes": "{{episode_number}} серий", "number_episodes": "{{episode_number}} серий",
"born": "Рожден", "born": "Рожден",
"appearances": "Появления", "appearances": "Появления",
"approve": "Одобрить", "approve": "Approve",
"decline": "Отклонить", "decline": "Decline",
"requested_by": "Запрошено {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Неизвестный пользователь", "unknown_user": "Unknown User",
"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 не пройден. Попробуйте еще раз.",
@@ -705,141 +705,141 @@
"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": "Что-то пошло не так при запросе медиафайлов!",
"request_approved": "Запрос одобрен!", "request_approved": "Request Approved!",
"request_declined": "Запрос отклонён!", "request_declined": "Request Declined!",
"failed_to_approve_request": "Не удалось одобрить запрос", "failed_to_approve_request": "Failed to Approve Request",
"failed_to_decline_request": "Не удалось отклонить запрос" "failed_to_decline_request": "Failed to Decline Request"
} }
}, },
"tabs": { "tabs": {
"home": "Главная", "home": "Дом",
"search": "Поиск", "search": "Поиск",
"library": "Библиотека", "library": "Библиотека",
"custom_links": "Ссылки", "custom_links": "Кастомные ссылки",
"favorites": "Избранное" "favorites": "Избранное"
}, },
"music": { "music": {
"title": "Музыка", "title": "Music",
"tabs": { "tabs": {
"suggestions": "Рекомендации", "suggestions": "Suggestions",
"albums": "Альбомы", "albums": "Albums",
"artists": "Исполнители", "artists": "Artists",
"playlists": "Плейлисты", "playlists": "Playlists",
"tracks": "треки" "tracks": "tracks"
}, },
"filters": { "filters": {
"all": "Все" "all": "All"
}, },
"recently_added": "Недавно добавлено", "recently_added": "Recently Added",
"recently_played": "Недавно воспроизведено", "recently_played": "Recently Played",
"frequently_played": "Часто играет", "frequently_played": "Frequently Played",
"explore": "Найти новое", "explore": "Explore",
"top_tracks": "Топ", "top_tracks": "Top Tracks",
"play": "Воспроизвести", "play": "Play",
"shuffle": "Перемешать", "shuffle": "Shuffle",
"play_top_tracks": "Воспроизвести топ", "play_top_tracks": "Play Top Tracks",
"no_suggestions": "Нет рекомендаций", "no_suggestions": "No suggestions available",
"no_albums": "Альбомы не найдены", "no_albums": "No albums found",
"no_artists": "Исполнители не найдены", "no_artists": "No artists found",
"no_playlists": "Плейлисты не найдены", "no_playlists": "No playlists found",
"album_not_found": "Альбом не найден", "album_not_found": "Album not found",
"artist_not_found": "Исполнитель не найден", "artist_not_found": "Artist not found",
"playlist_not_found": "Плейлист не найден", "playlist_not_found": "Playlist not found",
"track_options": { "track_options": {
"play_next": "Далее", "play_next": "Play Next",
"add_to_queue": "Добавить в очередь", "add_to_queue": "Add to Queue",
"add_to_playlist": "Добавить в плейлист", "add_to_playlist": "Add to Playlist",
"download": "Скачать", "download": "Download",
"downloaded": "Скачано", "downloaded": "Downloaded",
"downloading": "Скачивается...", "downloading": "Downloading...",
"cached": "Кешировано", "cached": "Cached",
"delete_download": "Удалить загрузку", "delete_download": "Delete Download",
"delete_cache": "Удалить из кеша", "delete_cache": "Remove from Cache",
"go_to_artist": "К исполнителю", "go_to_artist": "Go to Artist",
"go_to_album": "К альбому", "go_to_album": "Go to Album",
"add_to_favorites": "В избранное", "add_to_favorites": "Add to Favorites",
"remove_from_favorites": "Удалить из избранного", "remove_from_favorites": "Remove from Favorites",
"remove_from_playlist": "Удалить из плейлиста" "remove_from_playlist": "Remove from Playlist"
}, },
"playlists": { "playlists": {
"create_playlist": "Создать плейлист", "create_playlist": "Create Playlist",
"playlist_name": "Название плейлиста", "playlist_name": "Playlist Name",
"enter_name": "Введите название плейлиста", "enter_name": "Enter playlist name",
"create": "Создать", "create": "Create",
"search_playlists": "Поиск плейлистов...", "search_playlists": "Search playlists...",
"added_to": "Добавлено в {{name}}", "added_to": "Added to {{name}}",
"added": "Добавлено в плейлист", "added": "Added to playlist",
"removed_from": "Удалено из {{name}}", "removed_from": "Removed from {{name}}",
"removed": "Удалено из плейлиста", "removed": "Removed from playlist",
"created": "Плейлист создан", "created": "Playlist created",
"create_new": "Добавить новый плейлист", "create_new": "Create New Playlist",
"failed_to_add": "Не удалось добавить в плейлист", "failed_to_add": "Failed to add to playlist",
"failed_to_remove": "Не удалось удалить из плейлиста", "failed_to_remove": "Failed to remove from playlist",
"failed_to_create": "Не удалось создать плейлист", "failed_to_create": "Failed to create playlist",
"delete_playlist": "Удалить плейлист", "delete_playlist": "Delete Playlist",
"delete_confirm": "Вы уверены, что хотите удалить \"{{name}}\"? Это действие необратимо.", "delete_confirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"deleted": "Плейлист удалён", "deleted": "Playlist deleted",
"failed_to_delete": "Не удалось удалить плейлист" "failed_to_delete": "Failed to delete playlist"
}, },
"sort": { "sort": {
"title": "Сортировка", "title": "Sort By",
"alphabetical": "По алфавиту", "alphabetical": "Alphabetical",
"date_created": "По дате создания" "date_created": "Date Created"
} }
}, },
"watchlists": { "watchlists": {
"title": "Списки просмотров", "title": "Watchlists",
"my_watchlists": "Мои списки", "my_watchlists": "My Watchlists",
"public_watchlists": "Публичные списки", "public_watchlists": "Public Watchlists",
"create_title": "Создать список", "create_title": "Create Watchlist",
"edit_title": "Редактировать список", "edit_title": "Edit Watchlist",
"create_button": "Создать список", "create_button": "Create Watchlist",
"save_button": "Сохранить изменения", "save_button": "Save Changes",
"delete_button": "Удалить", "delete_button": "Delete",
"remove_button": "Удалить", "remove_button": "Remove",
"cancel_button": "Отмена", "cancel_button": "Cancel",
"name_label": "Название", "name_label": "Name",
"name_placeholder": "Введите название списка", "name_placeholder": "Enter watchlist name",
"description_label": "Описание", "description_label": "Description",
"description_placeholder": "Введите описание (не обязательно)", "description_placeholder": "Enter description (optional)",
"is_public_label": "Публичный", "is_public_label": "Public Watchlist",
"is_public_description": "Разрешить остальным пользователям видеть этот список", "is_public_description": "Allow others to view this watchlist",
"allowed_type_label": "Тип контента", "allowed_type_label": "Content Type",
"sort_order_label": "Сортировка по умолчанию", "sort_order_label": "Default Sort Order",
"empty_title": "Нет списков", "empty_title": "No Watchlists",
"empty_description": "Создайте ваш первый список для управления вашими медиа", "empty_description": "Create your first watchlist to start organizing your media",
"empty_watchlist": "Этот список пуст", "empty_watchlist": "This watchlist is empty",
"empty_watchlist_hint": "Добавляйте элементы из библиотеки в этот список", "empty_watchlist_hint": "Add items from your library to this watchlist",
"not_configured_title": "Streamystats не настроен", "not_configured_title": "Streamystats Not Configured",
"not_configured_description": "Настройте Streamystats для использования функционала списков", "not_configured_description": "Configure Streamystats in settings to use watchlists",
"go_to_settings": "В настройки", "go_to_settings": "Go to Settings",
"add_to_watchlist": "Добавить в список просмотра", "add_to_watchlist": "Add to Watchlist",
"remove_from_watchlist": "Удалить из списка просмотра", "remove_from_watchlist": "Remove from Watchlist",
"select_watchlist": "Выбрать список", "select_watchlist": "Select Watchlist",
"create_new": "Создать новый список", "create_new": "Create New Watchlist",
"item": "элемент", "item": "item",
"items": "элементы", "items": "items",
"public": "Публичный", "public": "Public",
"private": "Личный", "private": "Private",
"you": "Ваш", "you": "You",
"by_owner": "Другим пользователем", "by_owner": "By another user",
"not_found": "Список не найден", "not_found": "Watchlist not found",
"delete_confirm_title": "Удалить список", "delete_confirm_title": "Delete Watchlist",
"delete_confirm_message": "Вы уверены, что хотите удалить список \"{{name}}\"? Это действие необратимо.", "delete_confirm_message": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"remove_item_title": "Удалить из списка", "remove_item_title": "Remove from Watchlist",
"remove_item_message": "Удалить \"{{name}}\" из списка?", "remove_item_message": "Remove \"{{name}}\" from this watchlist?",
"loading": "Загрузка списков...", "loading": "Loading watchlists...",
"no_compatible_watchlists": "Нет совместимых списков", "no_compatible_watchlists": "No compatible watchlists",
"create_one_first": "Создайте список просмотра с подходящим типом контента" "create_one_first": "Create a watchlist that accepts this content type"
}, },
"playback_speed": { "playback_speed": {
"title": "Скорость воспроизведения", "title": "Playback Speed",
"apply_to": "Применять к", "apply_to": "Apply To",
"speed": "Скорость", "speed": "Speed",
"scope": { "scope": {
"media": "Только в этот раз", "media": "This media only",
"show": "Ко всему сериалу", "show": "This show",
"all": "Ко всем файлам (по умолчанию)" "all": "All media (default)"
} }
} }
} }

View File

@@ -7,88 +7,88 @@
"username_placeholder": "Kullanıcı adı", "username_placeholder": "Kullanıcı adı",
"password_placeholder": "Şifre", "password_placeholder": "Şifre",
"login_button": "Giriş yap", "login_button": "Giriş yap",
"quick_connect": "Hızlı Bağlan", "quick_connect": "Hızlı Bağlantı",
"enter_code_to_login": "Giriş yapmak için {{code}} kodunu girin", "enter_code_to_login": "Giriş yapmak için {{code}} kodunu girin",
"failed_to_initiate_quick_connect": "Hızlı Bağlan başlatılamadı", "failed_to_initiate_quick_connect": "Quick Connect başlatılamadı",
"got_it": "Anlaşıldı", "got_it": "Anlaşıldı",
"connection_failed": "Bağlantı başarısız", "connection_failed": "Bağlantı başarısız",
"could_not_connect_to_server": "Sunucuya bağlanılamadı. Lütfen URL'yi ve ağ bağlantınızı kontrol edin.", "could_not_connect_to_server": "Sunucuya bağlanılamadı. Lütfen URL'yi ve ağ bağlantınızı kontrol edin",
"an_unexpected_error_occured": "Beklenmedik bir hata oluştu", "an_unexpected_error_occured": "Beklenmedik bir hata oluştu",
"change_server": "Sunucu değiştir", "change_server": "Sunucuyu değiştir",
"invalid_username_or_password": "Geçersiz kullanıcı adı veya şifre", "invalid_username_or_password": "Geçersiz kullanıcı adı veya şifre",
"user_does_not_have_permission_to_log_in": "Kullanıcının giriş yapma izni yok", "user_does_not_have_permission_to_log_in": "Kullanıcının giriş yapma izni yok",
"server_is_taking_too_long_to_respond_try_again_later": "Sunucunun yanıt vermesi çok uzun sürüyor, lütfen daha sonra tekrar deneyin", "server_is_taking_too_long_to_respond_try_again_later": "Sunucu yanıt vermekte çok uzun sürüyor, lütfen tekrar deneyin",
"server_received_too_many_requests_try_again_later": "Sunucu çok fazla istek aldı, lütfen daha sonra tekrar deneyin.", "server_received_too_many_requests_try_again_later": "Sunucu çok fazla istek aldı, lütfen tekrar deneyin.",
"there_is_a_server_error": "Sunucu hatası var", "there_is_a_server_error": "Sunucu hatası var",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Beklenmedik bir hata oluştu. Sunucu URL'sini doğru girdiğinizden emin misiniz?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Beklenmedik bir hata oluştu. Sunucu URL'sini doğru girdiğinizden emin oldunuz mu?",
"too_old_server_text": "Desteklenmeyen Jellyfin Sunucu sürümü bulundu.", "too_old_server_text": "Unsupported Jellyfin Server Discovered",
"too_old_server_description": "Lütfen Jellyfin'i en son sürüme güncelleyin." "too_old_server_description": "Please update Jellyfin to the latest version"
}, },
"server": { "server": {
"enter_url_to_jellyfin_server": "Jellyfin sunucunusun URL adresini girin", "enter_url_to_jellyfin_server": "Jellyfin sunucunusun URL'sini girin",
"server_url_placeholder": "http(s)://sunucunuz.com", "server_url_placeholder": "http(s)://sunucunuz.com",
"connect_button": "Bağlan", "connect_button": "Bağlan",
"previous_servers": "Önceki sunucular", "previous_servers": "Önceki sunucular",
"clear_button": "Temizle", "clear_button": "Temizle",
"swipe_to_remove": "Kaldırmak için kaydırın", "swipe_to_remove": "Swipe to remove",
"search_for_local_servers": "Yerel sunucuları ara", "search_for_local_servers": "Yerel sunucuları ara",
"searching": "Aranıyor...", "searching": "Aranıyor...",
"servers": "Sunucular", "servers": "Sunucular",
"saved": "Kaydedildi", "saved": "Saved",
"session_expired": "Oturum süresi doldu", "session_expired": "Session Expired",
"please_login_again": "Kaydedilmiş oturumunuzun süresi doldu. Lütfen tekrar giriş yapın.", "please_login_again": "Your saved session has expired. Please log in again.",
"remove_saved_login": "Kayıtlı oturumu kaldır", "remove_saved_login": "Remove Saved Login",
"remove_saved_login_description": "Bu sunucu için kaydedilmiş kimlik bilgileriniz kaldırılacaktır. Bir sonraki sefere kullanıcı adı ve şifrenizi yeniden girmeniz gerekecek.", "remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
"accounts_count": "{{count}} hesap", "accounts_count": "{{count}} accounts",
"select_account": "Hesap Seç", "select_account": "Select Account",
"add_account": "Hesap Ekle", "add_account": "Add Account",
"remove_account_description": "{{username}} için kayıtlı bilgiler kaldırılacaktır." "remove_account_description": "This will remove the saved credentials for {{username}}."
}, },
"save_account": { "save_account": {
"title": "Hesabı Kaydet", "title": "Save Account",
"save_for_later": "Bu hesabı kaydet", "save_for_later": "Save this account",
"security_option": "Güvenlik Seçeneği", "security_option": "Security Option",
"no_protection": "No protection", "no_protection": "No protection",
"no_protection_desc": "Kimlik doğrulamasız hızlı giriş", "no_protection_desc": "Quick login without authentication",
"pin_code": "PIN kodu", "pin_code": "PIN code",
"pin_code_desc": "Geçiş yaparken 4 haneli PIN kodu gereklidir", "pin_code_desc": "4-digit PIN required when switching",
"password": "Şifrenizi tekrar girin ", "password": "Re-enter password",
"password_desc": "Geçiş yaparken şifre gereklidir", "password_desc": "Password required when switching",
"save_button": "Kaydet", "save_button": "Save",
"cancel_button": "Vazgeç" "cancel_button": "Cancel"
}, },
"pin": { "pin": {
"enter_pin": "PIN kodunu girin", "enter_pin": "Enter PIN",
"enter_pin_for": "{{username}} için PIN kodunu girin", "enter_pin_for": "Enter PIN for {{username}}",
"enter_4_digits": "4 hane girin", "enter_4_digits": "Enter 4 digits",
"invalid_pin": "Geçersiz PIN kodu", "invalid_pin": "Invalid PIN",
"setup_pin": "PIN kodunu ayarla", "setup_pin": "Set Up PIN",
"confirm_pin": "PIN kodunu onayla", "confirm_pin": "Confirm PIN",
"pins_dont_match": "PIN kodları eşleşmiyor", "pins_dont_match": "PINs don't match",
"forgot_pin": "PIN kodunu mu unuttunuz?", "forgot_pin": "Forgot PIN?",
"forgot_pin_desc": "Kayıtlı bilgileriniz kaldırılacaktır" "forgot_pin_desc": "Your saved credentials will be removed"
}, },
"password": { "password": {
"enter_password": "Şifrenizi girin", "enter_password": "Enter Password",
"enter_password_for": "{{username}} için şifrenizi girin", "enter_password_for": "Enter password for {{username}}",
"invalid_password": "Geçersiz şifre" "invalid_password": "Invalid password"
}, },
"home": { "home": {
"checking_server_connection": "Sunucu bağlantısı kontrol ediliyor...", "checking_server_connection": "Checking server connection...",
"no_internet": "İnternet Yok", "no_internet": "İnternet Yok",
"no_items": "Öge Yok", "no_items": "Öge Yok",
"no_internet_message": "Endişelenmeyin, indirilmiş içerikleri izleyebilirsiniz.", "no_internet_message": "Endişelenmeyin, hala\ndownloaded içerik izleyebilirsiniz.",
"checking_server_connection_message": "Sunucuya bağlantı kontrol ediliyor", "checking_server_connection_message": "Checking connection to server",
"go_to_downloads": "İndirilenlere git", "go_to_downloads": "İndirmelere Git",
"retry": "Tekrar dene", "retry": "Retry",
"server_unreachable": "Sunucuya ulaşılamıyor", "server_unreachable": "Server Unreachable",
"server_unreachable_message": "Sunucuya bağlanılamadı. Lütfen ağ bağlantınızı kontrol edin.", "server_unreachable_message": "Could not reach the server.\nPlease check your network connection.",
"oops": "Hups!", "oops": "Hups!",
"error_message": "Bir şeyler ters gitti.\nLütfen çıkış yapıp tekrar giriş yapın.", "error_message": "Bir şeyler ters gitti.\nLütfen çıkış yapın ve tekrar giriş yapın.",
"continue_watching": "İzlemeye Devam Et", "continue_watching": "İzlemeye Devam Et",
"next_up": "Sonraki", "next_up": "Sonraki",
"continue_and_next_up": "İzlemeye Devam Et & Sıradakiler", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "{{libraryName}} Kütüphanesine Son Eklenenler", "recently_added_in": "{{libraryName}}'de Yakınlarda Eklendi",
"suggested_movies": "Önerilen Filmler", "suggested_movies": "Önerilen Filmler",
"suggested_episodes": "Önerilen Bölümler", "suggested_episodes": "Önerilen Bölümler",
"intro": { "intro": {
@@ -110,52 +110,52 @@
"settings_title": "Ayarlar", "settings_title": "Ayarlar",
"log_out_button": ıkış Yap", "log_out_button": ıkış Yap",
"categories": { "categories": {
"title": "Kategoriler" "title": "Categories"
}, },
"playback_controls": { "playback_controls": {
"title": "Oynatma & Kontroller" "title": "Playback & Controls"
}, },
"audio_subtitles": { "audio_subtitles": {
"title": "Ses & Altyazılar" "title": "Audio & Subtitles"
}, },
"appearance": { "appearance": {
"title": "Görünüm", "title": "Appearance",
"merge_next_up_continue_watching": "İzlemeye Devam Et & Sıradakiler'i birleştir", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Uzak Oturum Butonunu Gizle" "hide_remote_session_button": "Hide Remote Session Button"
}, },
"network": { "network": {
"title": "", "title": "Network",
"local_network": "Yerel Ağ", "local_network": "Local Network",
"auto_switch_enabled": "Evdeyken otomatik geçiş yap", "auto_switch_enabled": "Auto-switch when at home",
"auto_switch_description": "Ev WiFi'sine bağlanınca otomatik olarak yerek URL adresine geçiş yap", "auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
"local_url": "Yerel URL Adresi", "local_url": "Local URL",
"local_url_hint": "Yerel sunucu adresinizi girin (http://192.168.1.100:8096, gibi)", "local_url_hint": "Enter your local server address (e.g., http://192.168.1.100:8096)",
"local_url_placeholder": "http://192.168.1.100:8096", "local_url_placeholder": "http://192.168.1.100:8096",
"home_wifi_networks": "Ev WiFi ağları", "home_wifi_networks": "Home WiFi Networks",
"add_current_network": "\"{{ssid}}\"'yi ekle", "add_current_network": "Add \"{{ssid}}\"",
"not_connected_to_wifi": "WiFi'a bağlı değil", "not_connected_to_wifi": "Not connected to WiFi",
"no_networks_configured": "Herhangi bir ağ ayarlanmadı", "no_networks_configured": "No networks configured",
"add_network_hint": "Otomatik geçişi etkinleştirmek için ev WiFi'nizi ekleyin", "add_network_hint": "Add your home WiFi network to enable auto-switching",
"current_wifi": "Şu anki WiFi", "current_wifi": "Current WiFi",
"using_url": "Kullanılıyor", "using_url": "Using",
"local": "Yerel URL Adresi", "local": "Local URL",
"remote": "Uzak URL Adresi", "remote": "Remote URL",
"not_connected": "Bağlı değil", "not_connected": "Not connected",
"current_server": "Geçerli Sunucu", "current_server": "Current Server",
"remote_url": "Uzak URL Adresi", "remote_url": "Remote URL",
"active_url": "Aktif URL Adresi", "active_url": "Active URL",
"not_configured": "Yapılandırılmamış", "not_configured": "Not configured",
"network_added": "Ağ eklendi", "network_added": "Network added",
"network_already_added": "Ağ zaten eklendi", "network_already_added": "Network already added",
"no_wifi_connected": "WiFi'a bağlı değil", "no_wifi_connected": "Not connected to WiFi",
"permission_denied": "Konum izni reddedildi", "permission_denied": "Location permission denied",
"permission_denied_explanation": "Otomatik geçiş yapabilmek için WiFi ağını algılayabilmek için konum izni gereklidir. Lütfen Ayarlarda etkinleştirin." "permission_denied_explanation": "Location permission is required to detect WiFi network for auto-switching. Please enable it in Settings."
}, },
"user_info": { "user_info": {
"user_info_title": "Kullanıcı Bilgisi", "user_info_title": "Kullanıcı Bilgisi",
"user": "Kullanıcı", "user": "Kullanıcı",
"server": "Sunucu", "server": "Sunucu",
"token": "Erişim Anahtarı", "token": "Token",
"app_version": "Uygulama Sürümü" "app_version": "Uygulama Sürümü"
}, },
"quick_connect": { "quick_connect": {
@@ -172,20 +172,20 @@
"media_controls_title": "Medya Kontrolleri", "media_controls_title": "Medya Kontrolleri",
"forward_skip_length": "İleri Sarma Uzunluğu", "forward_skip_length": "İleri Sarma Uzunluğu",
"rewind_length": "Geri Sarma Uzunluğu", "rewind_length": "Geri Sarma Uzunluğu",
"seconds_unit": "sn" "seconds_unit": "s"
}, },
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Hareketle Kontrol", "gesture_controls_title": "Gesture Controls",
"horizontal_swipe_skip": "Atlamak için yatay kaydırma", "horizontal_swipe_skip": "Horizontal Swipe to Skip",
"horizontal_swipe_skip_description": "Kontroller gizliyken sola/sağa kaydırarak atlama", "horizontal_swipe_skip_description": "Swipe left/right when controls are hidden to skip",
"left_side_brightness": "Sol Taraf Parlaklık Kontrolü", "left_side_brightness": "Left Side Brightness Control",
"left_side_brightness_description": "Sol tarafta aşağı/yukarı kaydırarak parlaklık ayarı", "left_side_brightness_description": "Swipe up/down on left side to adjust brightness",
"right_side_volume": "Sağ Taraf Ses Kontrolü", "right_side_volume": "Right Side Volume Control",
"right_side_volume_description": "Sağ tarafta aşağı/yukarı kaydırarak ses ayarı", "right_side_volume_description": "Swipe up/down on right side to adjust volume",
"hide_volume_slider": "Ses Ayarını Gizle", "hide_volume_slider": "Hide Volume Slider",
"hide_volume_slider_description": "Video oynatıcıda ses ayarını gizle", "hide_volume_slider_description": "Hide the volume slider in the video player",
"hide_brightness_slider": "Parlaklık Ayarını Gizle", "hide_brightness_slider": "Hide Brightness Slider",
"hide_brightness_slider_description": "Video oynatıcıda parlaklık ayarını gizle" "hide_brightness_slider_description": "Hide the brightness slider in the video player"
}, },
"audio": { "audio": {
"audio_title": "Ses", "audio_title": "Ses",
@@ -195,12 +195,12 @@
"none": "Yok", "none": "Yok",
"language": "Dil", "language": "Dil",
"transcode_mode": { "transcode_mode": {
"title": "Ses Kod Dönüştürmesi", "title": "Audio Transcoding",
"description": "Surround sesin (7.1, TrueHD, DTS-HD) nasıl işleneceğini kontrol eder.", "description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
"auto": "Oto", "auto": "Auto",
"stereo": "Stereo'ya zorla", "stereo": "Force Stereo",
"5_1": "5.1'e izin ver", "5_1": "Allow 5.1",
"passthrough": "Doğrudan geçiş" "passthrough": "Passthrough"
} }
}, },
"subtitles": { "subtitles": {
@@ -220,60 +220,60 @@
"None": "Yok", "None": "Yok",
"OnlyForced": "Sadece Zorunlu" "OnlyForced": "Sadece Zorunlu"
}, },
"text_color": "Metin Rengi", "text_color": "Text Color",
"background_color": "Arkaplan Rengi", "background_color": "Background Color",
"outline_color": "Kenarlık Rengi", "outline_color": "Outline Color",
"outline_thickness": "Kenarlık kalınlığı", "outline_thickness": "Outline Thickness",
"background_opacity": "Arkaplan Opaklığı", "background_opacity": "Background Opacity",
"outline_opacity": "Kenarlık Opaklığı", "outline_opacity": "Outline Opacity",
"bold_text": "Kalın Metin", "bold_text": "Bold Text",
"colors": { "colors": {
"Black": "Siyah", "Black": "Black",
"Gray": "Gri", "Gray": "Gray",
"Silver": "Gümüş", "Silver": "Silver",
"White": "Beyaz", "White": "White",
"Maroon": "Kestane", "Maroon": "Maroon",
"Red": "Kırmızı", "Red": "Red",
"Fuchsia": "Fuşya", "Fuchsia": "Fuchsia",
"Yellow": "Sarı", "Yellow": "Yellow",
"Olive": "Zeytin yeşili", "Olive": "Olive",
"Green": "Yeşil", "Green": "Green",
"Teal": "Deniz mavisi", "Teal": "Teal",
"Lime": "Limon", "Lime": "Lime",
"Purple": "Mor", "Purple": "Purple",
"Navy": "Lacivert", "Navy": "Navy",
"Blue": "Mavi", "Blue": "Blue",
"Aqua": "Açık Mavi" "Aqua": "Aqua"
}, },
"thickness": { "thickness": {
"None": "Hiçbiri", "None": "Hiçbiri",
"Thin": "İnce", "Thin": "Thin",
"Normal": "Normal", "Normal": "Normal",
"Thick": "Kalın" "Thick": "Thick"
}, },
"subtitle_color": "Altyazı Rengi", "subtitle_color": "Subtitle Color",
"subtitle_background_color": "Arkaplan Rengi", "subtitle_background_color": "Background Color",
"subtitle_font": "Altyazı Yazı Tipi", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Ayarları", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Donanımsal Kod Çözme", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Video kod çözme için donanımsal hızlandırma kullan. Oynatma sorunları yaşıyorsanız devre dışı bırakın." "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Altyazı Ayarları", "title": "VLC Subtitle Settings",
"hint": "VLC oynatıcı için altyazı görünümünü değiştirin. Değişiklikler bir sonraki oynatmada etkili olacak.", "hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Metin Rengi", "text_color": "Text Color",
"background_color": "Arkaplan Rengi", "background_color": "Background Color",
"background_opacity": "Arkaplan Opaklığı", "background_opacity": "Background Opacity",
"outline_color": "Kenarlık Rengi", "outline_color": "Outline Color",
"outline_opacity": "Kenarlık Opaklığı", "outline_opacity": "Outline Opacity",
"outline_thickness": "Kenarlık Kalınlığı", "outline_thickness": "Outline Thickness",
"bold": "Kalın Metin", "bold": "Bold Text",
"margin": "Alt Kenar Boşluğu" "margin": "Bottom Margin"
}, },
"video_player": { "video_player": {
"title": "Video oynatıcısı", "title": "Video Player",
"video_player": "Video oynatıcısı", "video_player": "Video Player",
"video_player_description": "iOS'da hangi video oynatıcının kullanılacağını seçin.", "video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer", "ksplayer": "KSPlayer",
"vlc": "VLC" "vlc": "VLC"
}, },
@@ -297,7 +297,7 @@
"video_player": "Video player", "video_player": "Video player",
"video_players": { "video_players": {
"VLC_3": "VLC 3", "VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Deneysel + PiP)" "VLC_4": "VLC 4 (Experimental + PiP)"
}, },
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster", "show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
@@ -305,24 +305,24 @@
"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", "default_quality": "Varsayılan kalite",
"default_playback_speed": "Varsayılan Oynatma Hızı", "default_playback_speed": "Default Playback Speed",
"auto_play_next_episode": "Otomatik Sonraki Bölümü Oynat", "auto_play_next_episode": "Auto-play Next Episode",
"max_auto_play_episode_count": "En Fazla Otomatik Oynatılacak Bölüm Sayısı", "max_auto_play_episode_count": "Max Auto Play Episode Count",
"disabled": "Devre dışı" "disabled": "Devre dışı"
}, },
"downloads": { "downloads": {
"downloads_title": "İndirmeler" "downloads_title": "İndirmeler"
}, },
"music": { "music": {
"title": "Müzik", "title": "Music",
"playback_title": "Oynatma", "playback_title": "Playback",
"playback_description": "Müziğin nasıl çalınacağını ayarlayın.", "playback_description": "Configure how music is played.",
"prefer_downloaded": "İndirilmiş Şarkıları Tercih Et", "prefer_downloaded": "Prefer Downloaded Songs",
"caching_title": "Önbellekleme", "caching_title": "Caching",
"caching_description": "Akıcı oynatım için gelecek şarkıları otomatik önbelleğe al.", "caching_description": "Automatically cache upcoming tracks for smoother playback.",
"lookahead_enabled": "Enable Look-Ahead Caching", "lookahead_enabled": "Enable Look-Ahead Caching",
"lookahead_count": "Önden Önbelleklenecek Parça Sayısı", "lookahead_count": "Tracks to Pre-cache",
"max_cache_size": "Maksimum Önbellek Boyutu" "max_cache_size": "Max Cache Size"
}, },
"plugins": { "plugins": {
"plugins_title": "Eklentiler", "plugins_title": "Eklentiler",
@@ -345,7 +345,7 @@
"order_by": { "order_by": {
"DEFAULT": "Varsayılan", "DEFAULT": "Varsayılan",
"VOTE_COUNT_AND_AVERAGE": "Vote count and average", "VOTE_COUNT_AND_AVERAGE": "Vote count and average",
"POPULARITY": "Popülerlik" "POPULARITY": "Popularity"
} }
}, },
"marlin_search": { "marlin_search": {
@@ -357,35 +357,35 @@
"save_button": "Kaydet", "save_button": "Kaydet",
"toasts": { "toasts": {
"saved": "Kaydedildi", "saved": "Kaydedildi",
"refreshed": "Ayarlar sunucudan yeniden alındı" "refreshed": "Settings refreshed from server"
}, },
"refresh_from_server": "Ayarları Sunucudan Yeniden Al" "refresh_from_server": "Refresh Settings from Server"
}, },
"streamystats": { "streamystats": {
"enable_streamystats": "Streamystats'ı Etkinleştir", "enable_streamystats": "Enable Streamystats",
"disable_streamystats": "Streamystats'ı Devre Dışı Bırak", "disable_streamystats": "Disable Streamystats",
"enable_search": "Arama için kullan", "enable_search": "Use for Search",
"url": "URL Adresi", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Streamystats sunucu URL'sini girin. URL, http veya https içermeli ve isteğe bağlı olarak portu içerebilir.", "streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
"read_more_about_streamystats": "Streamystats hakkında daha fazla bilgi.", "read_more_about_streamystats": "Read More About Streamystats.",
"save_button": "Kaydet", "save_button": "Save",
"save": "Kaydet", "save": "Save",
"features_title": "Özellikler", "features_title": "Features",
"home_sections_title": "Home Sections", "home_sections_title": "Home Sections",
"enable_movie_recommendations": "Film Önerileri", "enable_movie_recommendations": "Movie Recommendations",
"enable_series_recommendations": "Dizi Önerileri", "enable_series_recommendations": "Series Recommendations",
"enable_promoted_watchlists": "Promoted Watchlists", "enable_promoted_watchlists": "Promoted Watchlists",
"hide_watchlists_tab": "Hide Watchlists Tab", "hide_watchlists_tab": "Hide Watchlists Tab",
"home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.", "home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.",
"recommended_movies": "Önerilen Filmler", "recommended_movies": "Recommended Movies",
"recommended_series": "Önerilen Diziler", "recommended_series": "Recommended Series",
"toasts": { "toasts": {
"saved": "Kaydedildi", "saved": "Saved",
"refreshed": "Ayarlar sunucudan yeniden alındı", "refreshed": "Settings refreshed from server",
"disabled": "Streamystats devre dışı" "disabled": "Streamystats disabled"
}, },
"refresh_from_server": "Ayarları Sunucudan Yeniden Al" "refresh_from_server": "Refresh Settings from Server"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Enable our Watchlist integration", "watchlist_enabler": "Enable our Watchlist integration",
@@ -398,18 +398,18 @@
"device_usage": "Cihaz {{availableSpace}}%", "device_usage": "Cihaz {{availableSpace}}%",
"size_used": "{{used}} / {{total}} kullanıldı", "size_used": "{{used}} / {{total}} kullanıldı",
"delete_all_downloaded_files": "Tüm indirilen dosyaları sil", "delete_all_downloaded_files": "Tüm indirilen dosyaları sil",
"music_cache_title": "Müzik Ön Belleği", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"enable_music_cache": "Müzik Ön Belleğini Etkinleştir", "enable_music_cache": "Enable Music Cache",
"clear_music_cache": "Müzik Ön Belleğini Temizle", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} ön belleklendi", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Müzik ön belleği temizlendi", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Tüm İndirilen Müzikleri Sil", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} indirildi", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "İndirilen müzikler silindi" "downloaded_songs_deleted": "Downloaded songs deleted"
}, },
"intro": { "intro": {
"title": "Giriş", "title": "Intro",
"show_intro": "Tanıtımı Göster", "show_intro": "Tanıtımı Göster",
"reset_intro": "Tanıtımı Sıfırla" "reset_intro": "Tanıtımı Sıfırla"
}, },
@@ -417,7 +417,7 @@
"logs_title": "Günlükler", "logs_title": "Günlükler",
"export_logs": "Export logs", "export_logs": "Export logs",
"click_for_more_info": "Click for more info", "click_for_more_info": "Click for more info",
"level": "Düzey", "level": "Level",
"no_logs_available": "Günlükler mevcut değil", "no_logs_available": "Günlükler mevcut değil",
"delete_all_logs": "Tüm günlükleri sil" "delete_all_logs": "Tüm günlükleri sil"
}, },
@@ -433,22 +433,22 @@
} }
}, },
"sessions": { "sessions": {
"title": "Oturumlar", "title": "Sessions",
"no_active_sessions": "Aktif Oturum Yok" "no_active_sessions": "No Active Sessions"
}, },
"downloads": { "downloads": {
"downloads_title": "İndirilenler", "downloads_title": "İndirilenler",
"tvseries": "Diziler", "tvseries": "Diziler",
"movies": "Filmler", "movies": "Filmler",
"queue": "Sıra", "queue": "Sıra",
"other_media": "Diğer medya", "other_media": "Other media",
"queue_hint": "Sıra ve indirmeler uygulama yeniden başlatıldığında kaybolacaktır", "queue_hint": "Sıra ve indirmeler uygulama yeniden başlatıldığında kaybolacaktır",
"no_items_in_queue": "Sırada öğe yok", "no_items_in_queue": "Sırada öğe yok",
"no_downloaded_items": "İndirilen öğe yok", "no_downloaded_items": "İndirilen öğe yok",
"delete_all_movies_button": "Tüm Filmleri Sil", "delete_all_movies_button": "Tüm Filmleri Sil",
"delete_all_tvseries_button": "Tüm Dizileri Sil", "delete_all_tvseries_button": "Tüm Dizileri Sil",
"delete_all_button": "Tümünü Sil", "delete_all_button": "Tümünü Sil",
"delete_all_other_media_button": "Diğer medyayı sil", "delete_all_other_media_button": "Delete other media",
"active_download": "Aktif indirme", "active_download": "Aktif indirme",
"no_active_downloads": "Aktif indirme yok", "no_active_downloads": "Aktif indirme yok",
"active_downloads": "Aktif indirmeler", "active_downloads": "Aktif indirmeler",
@@ -465,49 +465,49 @@
"failed_to_delete_all_movies": "Filmler silinemedi", "failed_to_delete_all_movies": "Filmler silinemedi",
"deleted_all_tvseries_successfully": "Tüm diziler başarıyla silindi!", "deleted_all_tvseries_successfully": "Tüm diziler başarıyla silindi!",
"failed_to_delete_all_tvseries": "Diziler silinemedi", "failed_to_delete_all_tvseries": "Diziler silinemedi",
"deleted_media_successfully": "Der medya başarıyla silindi!", "deleted_media_successfully": "Deleted other media Successfully!",
"failed_to_delete_media": "Failed to Delete other media", "failed_to_delete_media": "Failed to Delete other media",
"download_deleted": "İndirme silindi", "download_deleted": "Download Deleted",
"download_cancelled": "İndirme iptal edildi", "download_cancelled": "İndirme iptal edildi",
"could_not_delete_download": "İndirme Silinemedi", "could_not_delete_download": "Could Not Delete Download",
"download_paused": "İndirme Duraklatıldı", "download_paused": "Download Paused",
"could_not_pause_download": "İndirme Duraklatılamadı", "could_not_pause_download": "Could Not Pause Download",
"download_resumed": "İndirme Devam Ediyor", "download_resumed": "Download Resumed",
"could_not_resume_download": "İndirme Devam Ettirilemedi", "could_not_resume_download": "Could Not Resume Download",
"download_completed": "İndirme tamamlandı", "download_completed": "İndirme tamamlandı",
"download_failed": "İndirme başarısız oldu", "download_failed": "Download Failed",
"download_failed_for_item": "{{item}} için indirme başarısız oldu - {{error}}", "download_failed_for_item": "{{item}} için indirme başarısız oldu - {{error}}",
"download_completed_for_item": "{{item}} için indirme tamamlandı", "download_completed_for_item": "{{item}} için indirme tamamlandı",
"download_started_for_item": "{{item}} için indirme başladı", "download_started_for_item": "Download Started for {{item}}",
"failed_to_start_download": "İndirme başlatılamadı", "failed_to_start_download": "Failed to start download",
"item_already_downloading": "{{item}} zaten indiriliyor", "item_already_downloading": "{{item}} is already downloading",
"all_files_deleted": "Bütün indirilenler başarıyla silindi", "all_files_deleted": "All Downloads Deleted Successfully",
"files_deleted_by_type": "{{count}} {{type}} silindi", "files_deleted_by_type": "{{count}} {{type}} deleted",
"all_files_folders_and_jobs_deleted_successfully": "Tüm dosyalar, klasörler ve işler başarıyla silindi", "all_files_folders_and_jobs_deleted_successfully": "Tüm dosyalar, klasörler ve işler başarıyla silindi",
"failed_to_clean_cache_directory": "Önbellek dizini temizlenemedi", "failed_to_clean_cache_directory": "Failed to clean cache directory",
"could_not_get_download_url_for_item": "{{itemName}} için indirme URL'si alınamadı", "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "İndirmelere git", "go_to_downloads": "İndirmelere git",
"file_deleted": "{{item}} silindi" "file_deleted": "{{item}} deleted"
} }
} }
}, },
"common": { "common": {
"select": "Seç", "select": "Select",
"no_trailer_available": "Fragman mevcut değil", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Ses", "audio": "Ses",
"subtitle": "Altyazı", "subtitle": "Altyazı",
"play": "Oynat", "play": "Play",
"none": "Hiçbiri", "none": "None",
"track": "Parça", "track": "Track",
"cancel": "Vazgeç", "cancel": "Cancel",
"delete": "Sil", "delete": "Delete",
"ok": "Tamam", "ok": "OK",
"remove": "Kaldır", "remove": "Remove",
"next": "Sonraki", "next": "Next",
"back": "Geri", "back": "Back",
"continue": "Devam", "continue": "Continue",
"verifying": "Doğrulanıyor..." "verifying": "Verifying..."
}, },
"search": { "search": {
"search": "Ara...", "search": "Ara...",
@@ -521,10 +521,10 @@
"episodes": "Bölümler", "episodes": "Bölümler",
"collections": "Koleksiyonlar", "collections": "Koleksiyonlar",
"actors": "Oyuncular", "actors": "Oyuncular",
"artists": "Sanatçılar", "artists": "Artists",
"albums": "Albümler", "albums": "Albums",
"songs": "Şarkılar", "songs": "Songs",
"playlists": "Çalma listeleri", "playlists": "Playlists",
"request_movies": "Film Talep Et", "request_movies": "Film Talep Et",
"request_series": "Dizi Talep Et", "request_series": "Dizi Talep Et",
"recently_added": "Son Eklenenler", "recently_added": "Son Eklenenler",
@@ -572,7 +572,7 @@
"genres": "Türler", "genres": "Türler",
"years": "Yıllar", "years": "Yıllar",
"sort_by": "Sırala", "sort_by": "Sırala",
"filter_by": "Filtrele", "filter_by": "Filter By",
"sort_order": "Sıralama düzeni", "sort_order": "Sıralama düzeni",
"tags": "Etiketler" "tags": "Etiketler"
} }
@@ -604,11 +604,11 @@
"index": "İndeks:", "index": "İndeks:",
"continue_watching": "İzlemeye devam et", "continue_watching": "İzlemeye devam et",
"go_back": "Geri", "go_back": "Geri",
"downloaded_file_title": "Bu dosya indirilmiş", "downloaded_file_title": "You have this file downloaded",
"downloaded_file_message": "İndirilmiş dosyayı oynatmak ister misiniz?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Evet", "downloaded_file_yes": "Yes",
"downloaded_file_no": "Hayır", "downloaded_file_no": "No",
"downloaded_file_cancel": "Vazgeç" "downloaded_file_cancel": "Cancel"
}, },
"item_card": { "item_card": {
"next_up": "Sıradaki", "next_up": "Sıradaki",
@@ -624,7 +624,7 @@
"no_similar_items_found": "Benzer öge bulunamadı", "no_similar_items_found": "Benzer öge bulunamadı",
"video": "Video", "video": "Video",
"more_details": "Daha fazla detay", "more_details": "Daha fazla detay",
"media_options": "Medya Seçenekleri", "media_options": "Media Options",
"quality": "Kalite", "quality": "Kalite",
"audio": "Ses", "audio": "Ses",
"subtitles": "Altyazı", "subtitles": "Altyazı",
@@ -639,7 +639,7 @@
"download_episode": "Bölümü indir", "download_episode": "Bölümü indir",
"download_movie": "Filmi indir", "download_movie": "Filmi indir",
"download_x_item": "{{item_count}} tane ögeyi indir", "download_x_item": "{{item_count}} tane ögeyi indir",
"download_unwatched_only": "Yalnızca İzlenmemişler", "download_unwatched_only": "Unwatched Only",
"download_button": "İndir" "download_button": "İndir"
} }
}, },
@@ -693,10 +693,10 @@
"number_episodes": "Bölüm {{episode_number}}", "number_episodes": "Bölüm {{episode_number}}",
"born": "Doğum", "born": "Doğum",
"appearances": "Görünmeler", "appearances": "Görünmeler",
"approve": "Onayla", "approve": "Approve",
"decline": "Reddet", "decline": "Decline",
"requested_by": "{{user}} tarafından istendi", "requested_by": "Requested by {{user}}",
"unknown_user": "Bilinmeyen Kullanıcı", "unknown_user": "Unknown User",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Jellyseerr sunucusu minimum sürüm gereksinimlerini karşılamıyor! Lütfen en az 2.0.0 sürümüne güncelleyin", "jellyseer_does_not_meet_requirements": "Jellyseerr sunucusu minimum sürüm gereksinimlerini karşılamıyor! Lütfen en az 2.0.0 sürümüne güncelleyin",
"jellyseerr_test_failed": "Jellyseerr testi başarısız oldu. Lütfen tekrar deneyin.", "jellyseerr_test_failed": "Jellyseerr testi başarısız oldu. Lütfen tekrar deneyin.",
@@ -705,10 +705,10 @@
"requested_item": "{{item}} talep edildi!", "requested_item": "{{item}} talep edildi!",
"you_dont_have_permission_to_request": "İstek göndermeye izniniz yok!", "you_dont_have_permission_to_request": "İstek göndermeye izniniz yok!",
"something_went_wrong_requesting_media": "Medya talep edilirken bir şeyler ters gitti!", "something_went_wrong_requesting_media": "Medya talep edilirken bir şeyler ters gitti!",
"request_approved": "İstek Onaylandı!", "request_approved": "Request Approved!",
"request_declined": "İstek Reddedildi!", "request_declined": "Request Declined!",
"failed_to_approve_request": "İsteği Onaylama Başarısız Oldu", "failed_to_approve_request": "Failed to Approve Request",
"failed_to_decline_request": "İsteği Reddetme Başarısız Oldu" "failed_to_decline_request": "Failed to Decline Request"
} }
}, },
"tabs": { "tabs": {
@@ -719,127 +719,127 @@
"favorites": "Favoriler" "favorites": "Favoriler"
}, },
"music": { "music": {
"title": "Müzik", "title": "Music",
"tabs": { "tabs": {
"suggestions": "Öneriler", "suggestions": "Suggestions",
"albums": "Albümler", "albums": "Albums",
"artists": "Sanatçılar", "artists": "Artists",
"playlists": "Çalma listeleri", "playlists": "Playlists",
"tracks": "parçalar" "tracks": "tracks"
}, },
"filters": { "filters": {
"all": "Tümü" "all": "All"
}, },
"recently_added": "Son Eklenenler", "recently_added": "Recently Added",
"recently_played": "Son Oynatılanlar", "recently_played": "Recently Played",
"frequently_played": "Sık Oynatılanlar", "frequently_played": "Frequently Played",
"explore": "Keşfet", "explore": "Explore",
"top_tracks": "En Popülar Parçalar", "top_tracks": "Top Tracks",
"play": "Oynat", "play": "Play",
"shuffle": "Karıştır", "shuffle": "Shuffle",
"play_top_tracks": "En Çok Oynatılan Parçaları Oynat", "play_top_tracks": "Play Top Tracks",
"no_suggestions": "Öneri mevcut değil", "no_suggestions": "No suggestions available",
"no_albums": "Hiç albüm bulunamadı", "no_albums": "No albums found",
"no_artists": "Hiç sanatçı bulunamadı", "no_artists": "No artists found",
"no_playlists": "Hiç çalma listesi bulunamadı", "no_playlists": "No playlists found",
"album_not_found": "Albüm bulunamadı", "album_not_found": "Album not found",
"artist_not_found": "Sanatçı bulunamadı", "artist_not_found": "Artist not found",
"playlist_not_found": "Çalma listesi bulunamadı", "playlist_not_found": "Playlist not found",
"track_options": { "track_options": {
"play_next": "Sıradakini Çal", "play_next": "Play Next",
"add_to_queue": "Sıraya Ekle", "add_to_queue": "Add to Queue",
"add_to_playlist": "Çalma listesine ekle", "add_to_playlist": "Add to Playlist",
"download": "İndir", "download": "Download",
"downloaded": "İndirildi", "downloaded": "Downloaded",
"downloading": "İndiriliyor...", "downloading": "Downloading...",
"cached": "Önbellekte", "cached": "Cached",
"delete_download": "İndirmeyi Sil", "delete_download": "Delete Download",
"delete_cache": "Ön bellekten kaldır", "delete_cache": "Remove from Cache",
"go_to_artist": "Sanatçıya Git", "go_to_artist": "Go to Artist",
"go_to_album": "Albüme Git", "go_to_album": "Go to Album",
"add_to_favorites": "Favorilere Ekle", "add_to_favorites": "Add to Favorites",
"remove_from_favorites": "Favorilerden Kaldır", "remove_from_favorites": "Remove from Favorites",
"remove_from_playlist": "Çalma Listesinden Kaldır" "remove_from_playlist": "Remove from Playlist"
}, },
"playlists": { "playlists": {
"create_playlist": "Çalma Listesi Oluştur", "create_playlist": "Create Playlist",
"playlist_name": "Çalma Listesi Adı", "playlist_name": "Playlist Name",
"enter_name": "Çalma listesi adı girin", "enter_name": "Enter playlist name",
"create": "Oluştur", "create": "Create",
"search_playlists": "Çalma listelerini ara...", "search_playlists": "Search playlists...",
"added_to": "Şu çalma listesine eklendi: {{name}}", "added_to": "Added to {{name}}",
"added": "Çalma listesine eklendi", "added": "Added to playlist",
"removed_from": "Şu çalma listesinden kaldırıldı: {{name}}", "removed_from": "Removed from {{name}}",
"removed": "Çalma listesinden kaldır", "removed": "Removed from playlist",
"created": "Çalma listesi oluşturuldu", "created": "Playlist created",
"create_new": "Yeni Çalma Listesi Oluştur", "create_new": "Create New Playlist",
"failed_to_add": "Çalma listesine eklenemedi", "failed_to_add": "Failed to add to playlist",
"failed_to_remove": "Çalma listesinden kaldırılamadı", "failed_to_remove": "Failed to remove from playlist",
"failed_to_create": "Çalma listesi oluşturulamadı", "failed_to_create": "Failed to create playlist",
"delete_playlist": "Çalma Listesini Sil", "delete_playlist": "Delete Playlist",
"delete_confirm": "\"{{name}}\" adlı çalma listesini silmek istediğinize emin misiniz? Bu işlem geri alınamaz.", "delete_confirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"deleted": "Çalma listesi silindi", "deleted": "Playlist deleted",
"failed_to_delete": "Çalma listesi oluşturulamadı" "failed_to_delete": "Failed to delete playlist"
}, },
"sort": { "sort": {
"title": "Sırala", "title": "Sort By",
"alphabetical": "Alfabetik", "alphabetical": "Alphabetical",
"date_created": "Oluşturulma Tarihi" "date_created": "Date Created"
} }
}, },
"watchlists": { "watchlists": {
"title": "İzleme listeleri", "title": "Watchlists",
"my_watchlists": "İzleme listelerim", "my_watchlists": "My Watchlists",
"public_watchlists": "Herkese açık izleme listeleri", "public_watchlists": "Public Watchlists",
"create_title": "İzleme listesi oluştur", "create_title": "Create Watchlist",
"edit_title": "İzleme listesini düzenle", "edit_title": "Edit Watchlist",
"create_button": "İzleme listesi oluştur", "create_button": "Create Watchlist",
"save_button": "Değişiklikleri Kaydet", "save_button": "Save Changes",
"delete_button": "Sil", "delete_button": "Delete",
"remove_button": "Kaldır", "remove_button": "Remove",
"cancel_button": "Vazgeç", "cancel_button": "Cancel",
"name_label": "Name", "name_label": "Name",
"name_placeholder": "İzleme listesi adını girin", "name_placeholder": "Enter watchlist name",
"description_label": "ıklama", "description_label": "Description",
"description_placeholder": "ıklama girin (isteğe bağlı)", "description_placeholder": "Enter description (optional)",
"is_public_label": "Herkese açık izleme listesi", "is_public_label": "Public Watchlist",
"is_public_description": "Başkalarının da bu izleme listesini görmesine izin ver", "is_public_description": "Allow others to view this watchlist",
"allowed_type_label": "İçerik Türü", "allowed_type_label": "Content Type",
"sort_order_label": "Varsayılan Sıralama", "sort_order_label": "Default Sort Order",
"empty_title": "İzleme listesi yok", "empty_title": "No Watchlists",
"empty_description": "Create your first watchlist to start organizing your media", "empty_description": "Create your first watchlist to start organizing your media",
"empty_watchlist": "Bu izleme listesi boş", "empty_watchlist": "This watchlist is empty",
"empty_watchlist_hint": "Kütüphanenizdeki nesneleri bu izleme listesine ekleyin", "empty_watchlist_hint": "Add items from your library to this watchlist",
"not_configured_title": "Streamystats ayarlanmamış", "not_configured_title": "Streamystats Not Configured",
"not_configured_description": "İzleme listelerini kullanmak için ayarlardan Streamystats'ı ayarlayın", "not_configured_description": "Configure Streamystats in settings to use watchlists",
"go_to_settings": "Ayarlara git", "go_to_settings": "Go to Settings",
"add_to_watchlist": "İzleme Listesine Ekle", "add_to_watchlist": "Add to Watchlist",
"remove_from_watchlist": "İzleme Listesinden Kaldır", "remove_from_watchlist": "Remove from Watchlist",
"select_watchlist": "İzleme Listesi Seç", "select_watchlist": "Select Watchlist",
"create_new": "Yeni İzleme Listesi Oluştur", "create_new": "Create New Watchlist",
"item": "öğe", "item": "item",
"items": "öğeler", "items": "items",
"public": "Herkese Açık", "public": "Public",
"private": "Özel", "private": "Private",
"you": "Siz", "you": "You",
"by_owner": "Başka kullanıcı tarafından", "by_owner": "By another user",
"not_found": "İzleme listesi bulunamadı", "not_found": "Watchlist not found",
"delete_confirm_title": "İzleme listesini sil", "delete_confirm_title": "Delete Watchlist",
"delete_confirm_message": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.", "delete_confirm_message": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"remove_item_title": "İzleme Listesinden Kaldır", "remove_item_title": "Remove from Watchlist",
"remove_item_message": "{{name}} bu izleme listesinden kaldırılsın mı?", "remove_item_message": "Remove \"{{name}}\" from this watchlist?",
"loading": "İzleme listeleri yükleniyor...", "loading": "Loading watchlists...",
"no_compatible_watchlists": "Uyumlu izleme listesi yok", "no_compatible_watchlists": "No compatible watchlists",
"create_one_first": "Bu içerik türünü kabul eden bir izleme listesi oluşturun" "create_one_first": "Create a watchlist that accepts this content type"
}, },
"playback_speed": { "playback_speed": {
"title": "Oynatma Hızı", "title": "Playback Speed",
"apply_to": "Şuna Uygula", "apply_to": "Apply To",
"speed": "Hız", "speed": "Speed",
"scope": { "scope": {
"media": "Yalnızca bu medyada", "media": "This media only",
"show": "Bu dizide", "show": "This show",
"all": "Bütün medyalarda (varsayılan)" "all": "All media (default)"
} }
} }
} }

View File

@@ -134,9 +134,6 @@ export enum VideoPlayer {
MPV = 0, MPV = 0,
} }
// Segment skip behavior options
export type SegmentSkipMode = "none" | "ask" | "auto";
// Audio transcoding mode - controls how surround audio is handled // Audio transcoding mode - controls how surround audio is handled
// This controls server-side transcoding behavior for audio streams. // This controls server-side transcoding behavior for audio streams.
// MPV decodes via FFmpeg and supports most formats, but mobile devices // MPV decodes via FFmpeg and supports most formats, but mobile devices
@@ -184,12 +181,6 @@ export type Settings = {
maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount; maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount;
autoPlayEpisodeCount: number; autoPlayEpisodeCount: number;
autoPlayNextEpisode: boolean; autoPlayNextEpisode: boolean;
// Media segment skip preferences
skipIntro: SegmentSkipMode;
skipOutro: SegmentSkipMode;
skipRecap: SegmentSkipMode;
skipCommercial: SegmentSkipMode;
skipPreview: SegmentSkipMode;
// Playback speed settings // Playback speed settings
defaultPlaybackSpeed: number; defaultPlaybackSpeed: number;
playbackSpeedPerMedia: Record<string, number>; playbackSpeedPerMedia: Record<string, number>;
@@ -275,12 +266,6 @@ export const defaultValues: Settings = {
maxAutoPlayEpisodeCount: { key: "3", value: 3 }, maxAutoPlayEpisodeCount: { key: "3", value: 3 },
autoPlayEpisodeCount: 0, autoPlayEpisodeCount: 0,
autoPlayNextEpisode: true, autoPlayNextEpisode: true,
// Media segment skip defaults
skipIntro: "ask",
skipOutro: "ask",
skipRecap: "ask",
skipCommercial: "ask",
skipPreview: "ask",
// Playback speed defaults // Playback speed defaults
defaultPlaybackSpeed: 1.0, defaultPlaybackSpeed: 1.0,
playbackSpeedPerMedia: {}, playbackSpeedPerMedia: {},

View File

@@ -1,40 +1,46 @@
import { Api } from "@jellyfin/sdk"; import { Api } from "@jellyfin/sdk";
import { MediaSegmentType } from "@jellyfin/sdk/lib/generated-client/models/media-segment-type";
import { getMediaSegmentsApi } from "@jellyfin/sdk/lib/utils/api/media-segments-api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import React from "react"; import React from "react";
import { DownloadedItem, MediaTimeSegment } from "@/providers/Downloads/types"; import { DownloadedItem, MediaTimeSegment } from "@/providers/Downloads/types";
import { getAuthHeaders } from "./jellyfin/jellyfin"; import { getAuthHeaders } from "./jellyfin/jellyfin";
export interface SegmentBuckets { // New Jellyfin 10.11+ Media Segments API types
introSegments: MediaTimeSegment[]; interface MediaSegmentDto {
creditSegments: MediaTimeSegment[]; Id: string;
recapSegments: MediaTimeSegment[]; ItemId: string;
commercialSegments: MediaTimeSegment[]; Type: "Intro" | "Outro" | "Recap" | "Commercial" | "Preview";
previewSegments: MediaTimeSegment[]; StartTicks: number;
EndTicks: number;
} }
// Legacy endpoints (intro-skipper / chapter-credits plugins on pre-10.11 servers) interface MediaSegmentsResponse {
Items: MediaSegmentDto[];
}
// Legacy API types (for fallback)
interface IntroTimestamps { interface IntroTimestamps {
IntroStart: number; EpisodeId: string;
HideSkipPromptAt: number;
IntroEnd: number; IntroEnd: number;
IntroStart: number;
ShowSkipPromptAt: number;
Valid: boolean; Valid: boolean;
} }
interface CreditTimestamps { interface CreditTimestamps {
Credits: { Start: number; End: number; Valid: boolean }; Introduction: {
Start: number;
End: number;
Valid: boolean;
};
Credits: {
Start: number;
End: number;
Valid: boolean;
};
} }
const TICKS_PER_SECOND = 10_000_000; const TICKS_PER_SECOND = 10000000;
const ticksToSeconds = (ticks: number): number => ticks / TICKS_PER_SECOND;
const emptyBuckets = (): SegmentBuckets => ({
introSegments: [],
creditSegments: [],
recapSegments: [],
commercialSegments: [],
previewSegments: [],
});
export const useSegments = ( export const useSegments = (
itemId: string, itemId: string,
@@ -42,6 +48,7 @@ export const useSegments = (
downloadedFiles: DownloadedItem[] | undefined, downloadedFiles: DownloadedItem[] | undefined,
api: Api | null, api: Api | null,
) => { ) => {
// Memoize the lookup so the array is only traversed when dependencies change
const downloadedItem = React.useMemo( const downloadedItem = React.useMemo(
() => downloadedFiles?.find((d) => d.item.Id === itemId), () => downloadedFiles?.find((d) => d.item.Id === itemId),
[downloadedFiles, itemId], [downloadedFiles, itemId],
@@ -58,110 +65,141 @@ export const useSegments = (
} }
return fetchAndParseSegments(itemId, api); return fetchAndParseSegments(itemId, api);
}, },
enabled: !!itemId && (isOffline ? !!downloadedItem : !!api), enabled: isOffline ? !!downloadedItem : !!api,
}); });
}; };
export const getSegmentsForItem = (item: DownloadedItem): SegmentBuckets => ({ export const getSegmentsForItem = (
introSegments: item.introSegments || [], item: DownloadedItem,
creditSegments: item.creditSegments || [], ): {
recapSegments: item.recapSegments || [], introSegments: MediaTimeSegment[];
commercialSegments: item.commercialSegments || [], creditSegments: MediaTimeSegment[];
previewSegments: item.previewSegments || [], } => {
}); return {
introSegments: item.introSegments || [],
creditSegments: item.creditSegments || [],
};
};
/** Jellyfin 10.11+ unified MediaSegments API. Returns null so the caller can fall back. */ /**
* Converts Jellyfin ticks to seconds
*/
const ticksToSeconds = (ticks: number): number => ticks / TICKS_PER_SECOND;
/**
* Fetches segments using the new Jellyfin 10.11+ MediaSegments API
*/
const fetchMediaSegments = async ( const fetchMediaSegments = async (
itemId: string, itemId: string,
api: Api, api: Api,
): Promise<SegmentBuckets | null> => { ): Promise<{
introSegments: MediaTimeSegment[];
creditSegments: MediaTimeSegment[];
} | null> => {
try { try {
const response = await getMediaSegmentsApi(api).getItemSegments({ const response = await api.axiosInstance.get<MediaSegmentsResponse>(
itemId, `${api.basePath}/MediaSegments/${itemId}`,
includeSegmentTypes: [ {
MediaSegmentType.Intro, headers: getAuthHeaders(api),
MediaSegmentType.Outro, params: {
MediaSegmentType.Recap, includeSegmentTypes: ["Intro", "Outro"],
MediaSegmentType.Commercial, },
MediaSegmentType.Preview, },
], );
});
const buckets = emptyBuckets(); const introSegments: MediaTimeSegment[] = [];
for (const segment of response.data.Items ?? []) { const creditSegments: MediaTimeSegment[] = [];
if (segment.StartTicks == null || segment.EndTicks == null) continue;
response.data.Items.forEach((segment) => {
const timeSegment: MediaTimeSegment = { const timeSegment: MediaTimeSegment = {
startTime: ticksToSeconds(segment.StartTicks), startTime: ticksToSeconds(segment.StartTicks),
endTime: ticksToSeconds(segment.EndTicks), endTime: ticksToSeconds(segment.EndTicks),
text: segment.Type ?? "", text: segment.Type,
}; };
switch (segment.Type) { switch (segment.Type) {
case MediaSegmentType.Intro: case "Intro":
buckets.introSegments.push(timeSegment); introSegments.push(timeSegment);
break; break;
case MediaSegmentType.Outro: case "Outro":
buckets.creditSegments.push(timeSegment); creditSegments.push(timeSegment);
break; break;
case MediaSegmentType.Recap: // Optionally handle other types like Recap, Commercial, Preview
buckets.recapSegments.push(timeSegment); default:
break;
case MediaSegmentType.Commercial:
buckets.commercialSegments.push(timeSegment);
break;
case MediaSegmentType.Preview:
buckets.previewSegments.push(timeSegment);
break; break;
} }
} });
return buckets; return { introSegments, creditSegments };
} catch { } catch (_error) {
// Return null to indicate we should try legacy endpoints
return null; return null;
} }
}; };
/** Pre-10.11 fallback: third-party intro-skipper / chapter-credits plugin endpoints. */ /**
* Fetches segments using legacy pre-10.11 endpoints
*/
const fetchLegacySegments = async ( const fetchLegacySegments = async (
itemId: string, itemId: string,
api: Api, api: Api,
): Promise<SegmentBuckets> => { ): Promise<{
const buckets = emptyBuckets(); introSegments: MediaTimeSegment[];
creditSegments: MediaTimeSegment[];
}> => {
const introSegments: MediaTimeSegment[] = [];
const creditSegments: MediaTimeSegment[] = [];
const [introRes, creditRes] = await Promise.allSettled([ try {
api.axiosInstance.get<IntroTimestamps>( const [introRes, creditRes] = await Promise.allSettled([
`${api.basePath}/Episode/${itemId}/IntroTimestamps`, api.axiosInstance.get<IntroTimestamps>(
{ headers: getAuthHeaders(api) }, `${api.basePath}/Episode/${itemId}/IntroTimestamps`,
), { headers: getAuthHeaders(api) },
api.axiosInstance.get<CreditTimestamps>( ),
`${api.basePath}/Episode/${itemId}/Timestamps`, api.axiosInstance.get<CreditTimestamps>(
{ headers: getAuthHeaders(api) }, `${api.basePath}/Episode/${itemId}/Timestamps`,
), { headers: getAuthHeaders(api) },
]); ),
]);
if (introRes.status === "fulfilled" && introRes.value.data.Valid) { if (introRes.status === "fulfilled" && introRes.value.data.Valid) {
buckets.introSegments.push({ introSegments.push({
startTime: introRes.value.data.IntroStart, startTime: introRes.value.data.IntroStart,
endTime: introRes.value.data.IntroEnd, endTime: introRes.value.data.IntroEnd,
text: "Intro", text: "Intro",
}); });
}
if (
creditRes.status === "fulfilled" &&
creditRes.value.data.Credits.Valid
) {
creditSegments.push({
startTime: creditRes.value.data.Credits.Start,
endTime: creditRes.value.data.Credits.End,
text: "Credits",
});
}
} catch (error) {
console.error("Failed to fetch legacy segments", error);
} }
if (creditRes.status === "fulfilled" && creditRes.value.data.Credits.Valid) { return { introSegments, creditSegments };
buckets.creditSegments.push({
startTime: creditRes.value.data.Credits.Start,
endTime: creditRes.value.data.Credits.End,
text: "Outro",
});
}
return buckets;
}; };
export const fetchAndParseSegments = async ( export const fetchAndParseSegments = async (
itemId: string, itemId: string,
api: Api, api: Api,
): Promise<SegmentBuckets> => { ): Promise<{
introSegments: MediaTimeSegment[];
creditSegments: MediaTimeSegment[];
}> => {
// Try new API first (Jellyfin 10.11+)
const newSegments = await fetchMediaSegments(itemId, api); const newSegments = await fetchMediaSegments(itemId, api);
return newSegments ?? fetchLegacySegments(itemId, api); if (newSegments) {
return newSegments;
}
// Fallback to legacy endpoints
return fetchLegacySegments(itemId, api);
}; };