Compare commits

..

1 Commits

Author SHA1 Message Date
Gauvain
947d2e4ff3 fix(hooks): correct useMemo dependency arrays
Two memos read values missing from their dependency arrays:
- TrackSheet: the MediaStreams filter uses streamType (add it).
- LibraryItemCard: the route memo uses api (add it).
Prevents stale memoized values when those inputs change (e.g. api after re-login).
2026-06-01 01:00:12 +02:00
105 changed files with 1411 additions and 10570 deletions

View File

@@ -45,4 +45,4 @@ This file previously contained facts about the codebase learned from past sessio
- **Streamystats components location**: Streamystats TV components are at `components/home/StreamystatsRecommendations.tv.tsx` and `components/home/StreamystatsPromotedWatchlists.tv.tsx`. The watchlist detail page (which shows items in a grid) is at `app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx`. _(2026-01-25)_ - **Streamystats components location**: Streamystats TV components are at `components/home/StreamystatsRecommendations.tv.tsx` and `components/home/StreamystatsPromotedWatchlists.tv.tsx`. The watchlist detail page (which shows items in a grid) is at `app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx`. _(2026-01-25)_
- **Platform-specific file suffix (.tv.tsx) does NOT work**: The `.tv.tsx` file suffix does NOT work for either pages or components in this project. Metro bundler doesn't resolve platform-specific suffixes. Instead, use `Platform.isTV` conditional rendering within a single file. For pages: check `Platform.isTV` at the top and return the TV component early. For components: create separate `MyComponent.tsx` and `TVMyComponent.tsx` files and use `Platform.isTV` to choose which to render. _(2026-01-26)_ - **Platform-specific file suffix (.tv.tsx) does NOT work**: The `.tv.tsx` file suffix does NOT work for either pages or components in this project. Metro bundler doesn't resolve platform-specific suffixes. Instead, use `Platform.isTV` conditional rendering within a single file. For pages: check `Platform.isTV` at the top and return the TV component early. For components: create separate `MyComponent.tsx` and `TVMyComponent.tsx` files and use `Platform.isTV` to choose which to render. _(2026-01-26)_

View File

@@ -1,25 +0,0 @@
# Custom EAS Build config for Android phone APK (downloadable artifact).
# Same bun-forcing flow as android-production.yml, but builds an APK
# (assembleRelease) instead of an AAB — for sideloading / GitHub artifact.
# Referenced from eas.json: build.production-apk.android.config
build:
name: Android phone APK (bun)
steps:
- eas/checkout
- run:
name: Install dependencies (bun, frozen)
command: bun install --frozen-lockfile
- run:
name: Prebuild (Android, bun)
command: bunx expo prebuild --platform android --no-install
- eas/configure_android_version
- eas/inject_android_credentials
- eas/run_gradle:
inputs:
command: :app:assembleRelease
- eas/find_and_upload_build_artifacts

View File

@@ -1,27 +0,0 @@
# Custom EAS Build config for Android TV APK (downloadable artifact).
# Same bun-forcing flow, with EXPO_TV=1 (set via the profile env in
# eas.json) so prebuild generates the TV variant. Builds an APK for
# sideloading onto Android TV devices.
# Referenced from eas.json: build.production-apk-tv.android.config
build:
name: Android TV APK (bun)
steps:
- eas/checkout
- run:
name: Install dependencies (bun, frozen)
command: bun install --frozen-lockfile
# EXPO_TV=1 comes from the profile env, so prebuild targets Android TV.
- run:
name: Prebuild (Android TV, bun)
command: bunx expo prebuild --platform android --no-install
- eas/configure_android_version
- eas/inject_android_credentials
- eas/run_gradle:
inputs:
command: :app:assembleRelease
- eas/find_and_upload_build_artifacts

View File

@@ -1,38 +0,0 @@
# Custom EAS Build config for Android (production AAB).
#
# Why this exists: EAS's managed build can't detect Bun's text lockfile
# (bun.lock) and falls back to yarn, which breaks our install. The managed
# steps `eas/install_node_modules` and `eas/prebuild` both use "the package
# manager detected based on your project", so we replace them with explicit
# `bun` commands. Everything else uses EAS's built-in functions so we still
# get remote versioning, credentials, and artifact upload.
#
# Referenced from eas.json: build.production.android.config = android-production.yml
build:
name: Android production (bun)
steps:
- eas/checkout
- run:
name: Install dependencies (bun, frozen)
command: bun install --frozen-lockfile
# android/ is gitignored, so generate native code fresh. --no-install
# because deps are already installed above; bunx keeps it on bun.
- run:
name: Prebuild (Android, bun)
command: bunx expo prebuild --platform android --no-install
# Applies the EAS-resolved remote versionCode/versionName (autoIncrement
# in eas.json) into the freshly prebuilt android/ project.
- eas/configure_android_version
# Injects the remote Android keystore / signing config.
- eas/inject_android_credentials
# Build the Play Store app bundle (.aab).
- eas/run_gradle:
inputs:
command: :app:bundleRelease
- eas/find_and_upload_build_artifacts

View File

@@ -1,44 +0,0 @@
# Custom EAS Build config for iOS + tvOS (App Store), forcing bun.
#
# Shared by both the iPhone profile (production) and the tvOS profile
# (production_tv). The profile decides the rest:
# - production_tv sets EXPO_TV=1 (env) so prebuild targets tvOS, and
# credentialsSource: local (EAS can't manage tvOS creds remotely).
# - production uses remote-managed iOS credentials.
#
# Like the Android configs, this replaces eas/install_node_modules and
# eas/prebuild (both auto-detect the wrong package manager) with explicit
# bun commands, and keeps EAS built-ins for credentials/version/fastlane.
build:
name: iOS/tvOS App Store (bun)
steps:
- eas/checkout
- run:
name: Install dependencies (bun, frozen)
command: bun install --frozen-lockfile
- eas/resolve_apple_team_id_from_credentials:
id: resolve_team
# android/ + ios/ are gitignored, so generate native code fresh.
# EXPO_TV (from the profile env) selects iPhone vs tvOS. --no-install
# skips JS + pod install; we install pods explicitly below with bun deps.
- run:
name: Prebuild (iOS/tvOS, bun)
command: bunx expo prebuild --platform ios --no-install
- run:
name: Install CocoaPods
working_directory: ./ios
command: pod install
- eas/configure_ios_credentials
- eas/configure_ios_version
- eas/generate_gymfile_from_template:
inputs:
credentials: ${ eas.job.secrets.buildCredentials }
- eas/run_fastlane
- eas/find_and_upload_build_artifacts

View File

@@ -1,14 +1,9 @@
name: 🚀 Release (EAS build + submit) name: 🚀 Release (EAS Build + Submit)
# On merge to main (gated by the `production` GitHub Environment approval), # Cloud EAS build + auto-submit for iOS, tvOS and Android on merge to main.
# build all targets on EAS in parallel via custom bun build configs: # A manual approval gate (the `production` GitHub Environment) pauses the run
# 1. iOS phone → App Store (auto-submit) # before any build/submit starts. Configure required reviewers on that
# 2. tvOS → App Store (auto-submit) # environment in repo Settings → Environments → production.
# 3. Android AAB → Google Play (auto-submit)
# 4. Android phone APK→ downloadable artifact
# 5. Android TV APK → downloadable artifact
# Note: EAS queues builds based on your plan's concurrency; parallel jobs
# here just submit them — EAS may still run them serially.
concurrency: concurrency:
group: release-${{ github.ref }} group: release-${{ github.ref }}
@@ -28,7 +23,7 @@ jobs:
- name: ✅ Release approved - name: ✅ Release approved
run: echo "Release approved for ${{ github.sha }}" run: echo "Release approved for ${{ github.sha }}"
build: release:
name: 🚀 ${{ matrix.name }} name: 🚀 ${{ matrix.name }}
needs: approve needs: approve
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@@ -41,25 +36,12 @@ jobs:
- name: 🍎 iOS - name: 🍎 iOS
platform: ios platform: ios
profile: production profile: production
submit: true
- name: 📺 tvOS - name: 📺 tvOS
platform: ios platform: ios
profile: production_tv profile: production_tv
submit: true - name: 🤖 Android
- name: 🤖 Android AAB
platform: android platform: android
profile: production profile: production
submit: true
- name: 🤖 Android APK
platform: android
profile: production-apk
submit: false
artifact_name: streamyfin-android-phone-apk
- name: 📺 Android TV APK
platform: android
profile: production-apk-tv
submit: false
artifact_name: streamyfin-android-tv-apk
steps: steps:
- name: 📥 Checkout code - name: 📥 Checkout code
@@ -94,8 +76,10 @@ jobs:
token: ${{ secrets.EXPO_TOKEN }} token: ${{ secrets.EXPO_TOKEN }}
eas-cache: true eas-cache: true
# tvOS uses credentialsSource: local — restore the gitignored # tvOS uses local credentials (EAS can't manage tvOS provisioning
# credentials.json + cert + provisioning profiles from secrets. # remotely, including the TopShelf extension target). Restore the
# gitignored credentials.json + cert + profiles from secrets so the
# cloud build can sign with `credentialsSource: local`.
- name: 🔐 Restore tvOS signing credentials - name: 🔐 Restore tvOS signing credentials
if: matrix.profile == 'production_tv' if: matrix.profile == 'production_tv'
env: env:
@@ -110,14 +94,10 @@ jobs:
echo "$TVOS_APP_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_tvOS_App_Store.mobileprovision echo "$TVOS_APP_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_tvOS_App_Store.mobileprovision
echo "$TVOS_TOPSHELF_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_TopShelf_tvOS_App_Store.mobileprovision echo "$TVOS_TOPSHELF_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_TopShelf_tvOS_App_Store.mobileprovision
# Android Play submit needs the Google Play service account JSON. # iOS + tvOS submit upload to App Store Connect with an ASC API key.
- name: 🔐 Restore Google Play service account # EAS reads it from EXPO_ASC_API_KEY_PATH / EXPO_ASC_KEY_ID /
if: matrix.platform == 'android' && matrix.submit # EXPO_ASC_ISSUER_ID (set on the build step below). Write the .p8,
env: # tolerating either raw-PEM or base64-encoded secret content.
GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json
# App Store Connect API key for iOS/tvOS submit (raw-PEM or base64).
- name: 🔐 Restore App Store Connect API key - name: 🔐 Restore App Store Connect API key
if: matrix.platform == 'ios' if: matrix.platform == 'ios'
env: env:
@@ -129,11 +109,18 @@ jobs:
printf '%s' "$APPLE_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/asc_api_key.p8" printf '%s' "$APPLE_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/asc_api_key.p8"
fi fi
# ── Submit builds: cloud build + auto-submit to the store ── # Android submit needs a Google Play service account JSON. eas.json's
# submit.production.android.serviceAccountKeyPath points at this file.
- name: 🔐 Restore Google Play service account
if: matrix.platform == 'android'
env:
GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json
- name: 🚀 Build & submit (${{ matrix.name }}) - name: 🚀 Build & submit (${{ matrix.name }})
if: matrix.submit
env: env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
# Consumed by eas submit for iOS/tvOS; ignored for Android.
EXPO_ASC_API_KEY_PATH: ${{ runner.temp }}/asc_api_key.p8 EXPO_ASC_API_KEY_PATH: ${{ runner.temp }}/asc_api_key.p8
EXPO_ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }} EXPO_ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
EXPO_ASC_ISSUER_ID: ${{ secrets.APPLE_KEY_ISSUER_ID }} EXPO_ASC_ISSUER_ID: ${{ secrets.APPLE_KEY_ISSUER_ID }}
@@ -142,75 +129,4 @@ jobs:
--platform ${{ matrix.platform }} \ --platform ${{ matrix.platform }} \
--profile ${{ matrix.profile }} \ --profile ${{ matrix.profile }} \
--auto-submit \ --auto-submit \
--non-interactive \ --non-interactive
--wait
# ── Artifact builds: cloud build, then download + upload the APK ──
- name: 🏗️ Build artifact (${{ matrix.name }})
if: ${{ !matrix.submit }}
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
eas build \
--platform ${{ matrix.platform }} \
--profile ${{ matrix.profile }} \
--non-interactive \
--wait \
--json > build-result.json
URL=$(node -e "const b=require('./build-result.json'); const x=Array.isArray(b)?b[0]:b; console.log(x.artifacts.applicationArchiveUrl)")
echo "Downloading artifact: $URL"
curl -fL "$URL" -o "${{ matrix.artifact_name }}.apk"
- name: 📤 Upload APK artifact (${{ matrix.name }})
if: ${{ !matrix.submit }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.artifact_name }}.apk
retention-days: 14
# Draft a GitHub Release with the two APKs attached. The tag comes from the
# merged-in app version (app.json → expo.version), NOT the auto-incremented
# build number — so cutting a release is a deliberate version bump via PR.
github-release:
name: 📦 Draft GitHub Release
needs: build
if: ${{ !cancelled() }}
runs-on: ubuntu-24.04
permissions:
contents: write
actions: read # required for `gh run download` to list/fetch this run's artifacts
steps:
- name: 📥 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
show-progress: false
- name: 📦 Download APK artifacts from this run
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p apks
gh run download ${{ github.run_id }} --name streamyfin-android-phone-apk --dir apks
gh run download ${{ github.run_id }} --name streamyfin-android-tv-apk --dir apks
ls -la apks
- name: 📝 Draft release (tag = app.json version, not auto-bumped)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION=$(node -e "console.log(require('./app.json').expo.version)")
TAG="v$VERSION"
echo "Release tag from merged app version: $TAG"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG exists — updating APK assets"
gh release upload "$TAG" apks/*.apk --clobber
else
echo "Creating draft release $TAG"
gh release create "$TAG" \
--draft \
--generate-notes \
--title "$TAG" \
apks/*.apk
fi

3
.gitignore vendored
View File

@@ -76,9 +76,6 @@ modules/background-downloader/android/build/*
# ios:unsigned-build Artifacts # ios:unsigned-build Artifacts
build/ build/
# but keep EAS custom build configs (the generic build/ rule above matches .eas/build/)
!.eas/build/
!.eas/build/**
.claude/ .claude/
.agents/skills/** .agents/skills/**
skills-lock.json skills-lock.json

View File

@@ -7,9 +7,6 @@ import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageSta
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast"); const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
const SyncPlayButtonComponent = Platform.isTV
? null
: require("@/components/syncplay/SyncPlayButton").SyncPlayButton;
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { HeaderBackButton } from "@/components/common/HeaderBackButton"; import { HeaderBackButton } from "@/components/common/HeaderBackButton";
@@ -36,7 +33,6 @@ export default function IndexLayout() {
{!Platform.isTV && ( {!Platform.isTV && (
<> <>
<Chromecast.Chromecast background='transparent' /> <Chromecast.Chromecast background='transparent' />
{SyncPlayButtonComponent && <SyncPlayButtonComponent />}
{user?.Policy?.IsAdministrator && <SessionsButton />} {user?.Policy?.IsAdministrator && <SessionsButton />}
<SettingsButton /> <SettingsButton />
</> </>

View File

@@ -59,19 +59,17 @@ function SettingsMobile() {
<QuickConnect className='mb-4' /> <QuickConnect className='mb-4' />
{Platform.OS !== "ios" && ( <View className='mb-4'>
<View className='mb-4'> <ListGroup title={t("pairing.pair_with_phone_title")}>
<ListGroup title={t("pairing.pair_with_phone_title")}> <ListItem
<ListItem onPress={() =>
onPress={() => router.push("/(auth)/(tabs)/(home)/companion-login")
router.push("/(auth)/(tabs)/(home)/companion-login") }
} title={t("pairing.pair_with_phone")}
title={t("pairing.pair_with_phone")} textColor='blue'
textColor='blue' />
/> </ListGroup>
</ListGroup> </View>
</View>
)}
<View className='mb-4'> <View className='mb-4'>
<AppLanguageSelector /> <AppLanguageSelector />

View File

@@ -114,7 +114,7 @@ export default function StreamystatsPage() {
}; };
const handleRefreshFromServer = useCallback(async () => { const handleRefreshFromServer = useCallback(async () => {
const newPluginSettings = await refreshStreamyfinPluginSettings(); const newPluginSettings = await refreshStreamyfinPluginSettings(true);
// Update local state with new values // Update local state with new values
const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || ""; const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || "";
setUrl(newUrl); setUrl(newUrl);

View File

@@ -166,7 +166,7 @@ export default function IndexLayout() {
open={dropdownOpen} open={dropdownOpen}
onOpenChange={setDropdownOpen} onOpenChange={setDropdownOpen}
trigger={ trigger={
<View> <View className='pl-1.5'>
<Ionicons <Ionicons
name='ellipsis-horizontal-outline' name='ellipsis-horizontal-outline'
size={24} size={24}

View File

@@ -40,8 +40,6 @@ const Layout = () => {
keyboardDismissMode='none' keyboardDismissMode='none'
screenOptions={{ screenOptions={{
tabBarBounces: true, tabBarBounces: true,
tabBarActiveTintColor: "#FFFFFF",
tabBarInactiveTintColor: "#9CA3AF",
tabBarLabelStyle: { tabBarLabelStyle: {
fontSize: TAB_LABEL_FONT_SIZE, fontSize: TAB_LABEL_FONT_SIZE,
fontWeight: "600", fontWeight: "600",

View File

@@ -31,7 +31,6 @@ import {
} from "@/components/video-player/controls/utils/playback-speed-settings"; } from "@/components/video-player/controls/utils/playback-speed-settings";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useHaptic } from "@/hooks/useHaptic"; import { useHaptic } from "@/hooks/useHaptic";
import { useKeepWebSocketAlive } from "@/hooks/useKeepWebSocketAlive";
import { useOrientation } from "@/hooks/useOrientation"; import { useOrientation } from "@/hooks/useOrientation";
import { usePlaybackManager } from "@/hooks/usePlaybackManager"; import { usePlaybackManager } from "@/hooks/usePlaybackManager";
import usePlaybackSpeed from "@/hooks/usePlaybackSpeed"; import usePlaybackSpeed from "@/hooks/usePlaybackSpeed";
@@ -50,8 +49,7 @@ import { DownloadedItem } from "@/providers/Downloads/types";
import { useInactivity } from "@/providers/InactivityProvider"; import { useInactivity } from "@/providers/InactivityProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { OfflineModeProvider } from "@/providers/OfflineModeProvider"; import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
import { useSyncPlay } from "@/providers/SyncPlay";
import type { PlayerControls } from "@/providers/SyncPlay/types";
import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles"; import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
@@ -79,11 +77,6 @@ export default function DirectPlayerPage() {
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false); const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
const [showControls, _setShowControls] = useState(true); const [showControls, _setShowControls] = useState(true);
const [isPipMode, setIsPipMode] = useState(false); const [isPipMode, setIsPipMode] = useState(false);
// Keep the global WebSocket open while in PiP so SyncPlay commands
// (and any other server pushes) keep flowing while iOS treats the
// app as backgrounded. See `WebSocketProvider.acquireKeepAlive`.
useKeepWebSocketAlive(isPipMode);
const [aspectRatio] = useState<"default" | "16:9" | "4:3" | "1:1" | "21:9">( const [aspectRatio] = useState<"default" | "16:9" | "4:3" | "1:1" | "21:9">(
"default", "default",
); );
@@ -135,7 +128,6 @@ export default function DirectPlayerPage() {
bitrateValue: bitrateValueStr, bitrateValue: bitrateValueStr,
offline: offlineStr, offline: offlineStr,
playbackPosition: playbackPositionFromUrl, playbackPosition: playbackPositionFromUrl,
syncPlay: syncPlayStr,
} = useLocalSearchParams<{ } = useLocalSearchParams<{
itemId: string; itemId: string;
audioIndex: string; audioIndex: string;
@@ -145,23 +137,9 @@ export default function DirectPlayerPage() {
offline: string; offline: string;
/** Playback position in ticks. */ /** Playback position in ticks. */
playbackPosition?: string; playbackPosition?: string;
/** Whether playback was initiated by SyncPlay */
syncPlay?: string;
}>(); }>();
// When opened via SyncPlay, don't auto-play - let SyncPlay commands control playback
const openedViaSyncPlay = syncPlayStr === "true";
const { lockOrientation, unlockOrientation } = useOrientation(); const { lockOrientation, unlockOrientation } = useOrientation();
// SyncPlay integration
const syncPlay = useSyncPlay();
const {
isEnabled: isSyncPlayEnabled,
controller: syncPlayController,
setPlayerControls,
notifyBuffering,
} = syncPlay;
const offline = offlineStr === "true"; const offline = offlineStr === "true";
// Audio index: use URL param if provided, otherwise use stored index for offline playback // Audio index: use URL param if provided, otherwise use stored index for offline playback
@@ -296,11 +274,6 @@ export default function DirectPlayerPage() {
}; };
if (itemId) { if (itemId) {
setItem(null);
setDownloadedItem(null);
// Clear the previous episode's stream so the loader gate stays closed
// until the new item's stream resolves (avoids a stale MPV source frame).
setStream(null);
fetchItemData(); fetchItemData();
} }
}, [itemId, offline, api, user?.Id]); }, [itemId, offline, api, user?.Id]);
@@ -343,12 +316,6 @@ export default function DirectPlayerPage() {
return null; return null;
} }
// Ensure item matches the current itemId to avoid race conditions
if (item.Id !== itemId) {
setStreamStatus({ isLoading: false, isError: false });
return null;
}
let result: Stream | null = null; let result: Stream | null = null;
if (offline && downloadedItem?.mediaSource) { if (offline && downloadedItem?.mediaSource) {
const url = downloadedItem.videoFilePath; const url = downloadedItem.videoFilePath;
@@ -421,7 +388,6 @@ export default function DirectPlayerPage() {
item, item,
user?.Id, user?.Id,
downloadedItem, downloadedItem,
offline,
]); ]);
useEffect(() => { useEffect(() => {
@@ -437,72 +403,8 @@ export default function DirectPlayerPage() {
reportPlaybackStart(); reportPlaybackStart();
}, [stream, api, offline]); }, [stream, api, offline]);
// SyncPlay: Connect player controls when video is ready
useEffect(() => {
if (!isVideoLoaded || !videoRef.current || offline) {
setPlayerControls(null);
return;
}
const controls: PlayerControls = {
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
seekTo: (positionMs: number) => {
const positionSec = positionMs / 1000;
console.log(
`PlayerControls.seekTo: ${positionMs}ms = ${positionSec}s, videoRef exists: ${!!videoRef.current}`,
);
videoRef.current?.seekTo(positionSec);
},
setSpeed: (speed: number) => videoRef.current?.setSpeed?.(speed),
getSpeed: () => currentPlaybackSpeed,
getCurrentPosition: () => progress.get(),
isPlaying: () => isPlaying,
isBuffering: () => isBuffering,
};
setPlayerControls(controls);
return () => {
setPlayerControls(null);
};
}, [
isVideoLoaded,
offline,
isPlaying,
isBuffering,
currentPlaybackSpeed,
progress,
setPlayerControls,
]);
// SyncPlay: Report buffering/ready state to server.
//
// CRITICAL: We must report `buffering` to the server *during* initial
// load (before `isVideoLoaded`), otherwise the server treats us as ready
// and proceeds without waiting for us. jellyfin-web reports this for
// free via the HTML5 video element's `waiting` event; for us, the
// initial load itself is the buffering window.
useEffect(() => {
if (!isSyncPlayEnabled) {
return;
}
const isLocallyReady = isVideoLoaded && !isBuffering;
// notifyBuffering routes through the debouncer in PlaybackCore so
// re-renders during a stall don't spam the server.
notifyBuffering(!isLocallyReady);
}, [isSyncPlayEnabled, isVideoLoaded, isBuffering, notifyBuffering]);
const togglePlay = async () => { const togglePlay = async () => {
lightHapticFeedback(); lightHapticFeedback();
// Route through SyncPlay when active
if (isSyncPlayEnabled && syncPlayController) {
syncPlayController.playPause();
return;
}
setIsPlaying(!isPlaying); setIsPlaying(!isPlaying);
if (isPlaying) { if (isPlaying) {
await videoRef.current?.pause(); await videoRef.current?.pause();
@@ -525,15 +427,21 @@ export default function DirectPlayerPage() {
if (!item?.Id || !stream?.sessionId || offline || !api) return; if (!item?.Id || !stream?.sessionId || offline || !api) return;
const currentTimeInTicks = msToTicks(progress.get()); const currentTimeInTicks = msToTicks(progress.get());
await getPlaystateApi(api).reportPlaybackStopped({ await getPlaystateApi(api).onPlaybackStopped({
playbackStopInfo: { itemId: item.Id,
ItemId: item.Id, mediaSourceId: mediaSourceId,
MediaSourceId: mediaSourceId, positionTicks: currentTimeInTicks,
PositionTicks: currentTimeInTicks, playSessionId: stream.sessionId,
PlaySessionId: stream.sessionId,
},
}); });
}, [api, item, mediaSourceId, stream, progress, offline]); }, [
api,
item,
mediaSourceId,
stream,
progress,
offline,
revalidateProgressCache,
]);
const stop = useCallback(() => { const stop = useCallback(() => {
// Update URL with final playback position before stopping // Update URL with final playback position before stopping
@@ -551,10 +459,9 @@ export default function DirectPlayerPage() {
useEffect(() => { useEffect(() => {
const beforeRemoveListener = navigation.addListener("beforeRemove", stop); const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
return () => { return () => {
reportPlaybackStopped();
beforeRemoveListener(); beforeRemoveListener();
}; };
}, [navigation, stop, reportPlaybackStopped]); }, [navigation, stop]);
const currentPlayStateInfo = useCallback((): const currentPlayStateInfo = useCallback(():
| PlaybackProgressInfo | PlaybackProgressInfo
@@ -731,12 +638,10 @@ export default function DirectPlayerPage() {
const startPos = ticksToSeconds(startTicks); const startPos = ticksToSeconds(startTicks);
// Build source config - headers only needed for online streaming // Build source config - headers only needed for online streaming
// When opened via SyncPlay, don't auto-play - SyncPlay commands control playback
const shouldAutoplay = !openedViaSyncPlay;
const source: MpvVideoSource = { const source: MpvVideoSource = {
url: stream.url, url: stream.url,
startPosition: startPos, startPosition: startPos,
autoplay: shouldAutoplay, autoplay: true,
initialSubtitleId, initialSubtitleId,
initialAudioId, initialAudioId,
// Pass cache/buffer settings from user preferences // Pass cache/buffer settings from user preferences
@@ -931,41 +836,6 @@ export default function DirectPlayerPage() {
[], [],
); );
// PiP playback controls. When SyncPlay is active, the native side
// is told to *delegate* these via `syncPlayDelegated`, so the OS
// play/pause/skip buttons emit these events instead of poking MPV
// directly. We route them through the SyncPlay controller so the
// server broadcasts a command to every group member (including us).
const _onPipPlayRequest = useCallback(() => {
if (isSyncPlayEnabled && syncPlayController) {
console.log("SyncPlay: PiP play → controller.playPause()");
syncPlayController.playPause();
}
}, [isSyncPlayEnabled, syncPlayController]);
const _onPipPauseRequest = useCallback(() => {
if (isSyncPlayEnabled && syncPlayController) {
console.log("SyncPlay: PiP pause → controller.playPause()");
syncPlayController.playPause();
}
}, [isSyncPlayEnabled, syncPlayController]);
const _onPipSkipRequest = useCallback(
(e: {
nativeEvent: { targetSeconds: number; intervalSeconds: number };
}) => {
if (!isSyncPlayEnabled || !syncPlayController) return;
const { targetSeconds } = e.nativeEvent;
// SyncPlay seek takes ticks (1 s = 10_000_000 ticks).
const ticks = Math.max(0, Math.round(targetSeconds * 10_000_000));
console.log(
`SyncPlay: PiP skip → controller.seek(${targetSeconds}s = ${ticks} ticks)`,
);
syncPlayController.seek(ticks);
},
[isSyncPlayEnabled, syncPlayController],
);
const [isMounted, setIsMounted] = useState(false); const [isMounted, setIsMounted] = useState(false);
// Add useEffect to handle mounting // Add useEffect to handle mounting
@@ -990,21 +860,10 @@ export default function DirectPlayerPage() {
videoRef.current?.pause?.(); videoRef.current?.pause?.();
}, []); }, []);
const seek = useCallback( const seek = useCallback((position: number) => {
(position: number) => { // MPV expects seconds, convert from ms
// Route through SyncPlay when active. `position` is in ms; the videoRef.current?.seekTo?.(position / 1000);
// controller takes ticks (1 ms = 10000 ticks). }, []);
if (isSyncPlayEnabled && syncPlayController) {
console.log("SyncPlay: seek requested via SyncPlay", position);
syncPlayController.seek(Math.round(position * 10000));
return;
}
// MPV expects seconds, convert from ms
videoRef.current?.seekTo?.(position / 1000);
},
[isSyncPlayEnabled, syncPlayController],
);
// TV audio track change handler // TV audio track change handler
const handleAudioIndexChange = useCallback( const handleAudioIndexChange = useCallback(
@@ -1396,10 +1255,6 @@ export default function DirectPlayerPage() {
onProgress={onProgress} onProgress={onProgress}
onPlaybackStateChange={onPlaybackStateChanged} onPlaybackStateChange={onPlaybackStateChanged}
onPictureInPictureChange={_onPictureInPictureChange} onPictureInPictureChange={_onPictureInPictureChange}
syncPlayDelegated={isSyncPlayEnabled}
onPipPlayRequest={_onPipPlayRequest}
onPipPauseRequest={_onPipPauseRequest}
onPipSkipRequest={_onPipSkipRequest}
onLoad={() => setIsVideoLoaded(true)} onLoad={() => setIsVideoLoaded(true)}
onError={(e: { nativeEvent: MpvOnErrorEventPayload }) => { onError={(e: { nativeEvent: MpvOnErrorEventPayload }) => {
console.error("Video Error:", e.nativeEvent); console.error("Video Error:", e.nativeEvent);

View File

@@ -25,7 +25,6 @@ import { MusicPlayerProvider } from "@/providers/MusicPlayerProvider";
import { NetworkStatusProvider } from "@/providers/NetworkStatusProvider"; import { NetworkStatusProvider } from "@/providers/NetworkStatusProvider";
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider"; import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
import { ServerUrlProvider } from "@/providers/ServerUrlProvider"; import { ServerUrlProvider } from "@/providers/ServerUrlProvider";
import { SyncPlayProvider } from "@/providers/SyncPlay";
import { WebSocketProvider } from "@/providers/WebSocketProvider"; import { WebSocketProvider } from "@/providers/WebSocketProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { import {
@@ -410,136 +409,133 @@ function Layout() {
<PlaySettingsProvider> <PlaySettingsProvider>
<LogProvider> <LogProvider>
<WebSocketProvider> <WebSocketProvider>
<SyncPlayProvider> <DownloadProvider>
<DownloadProvider> <MusicPlayerProvider>
<MusicPlayerProvider> <GlobalModalProvider>
<GlobalModalProvider> <BottomSheetModalProvider>
<BottomSheetModalProvider> <IntroSheetProvider>
<IntroSheetProvider> <ThemeProvider value={DarkTheme}>
<ThemeProvider value={DarkTheme}> <SystemBars style='light' hidden={false} />
<SystemBars style='light' hidden={false} /> <Stack initialRouteName='(auth)/(tabs)'>
<Stack initialRouteName='(auth)/(tabs)'> <Stack.Screen
<Stack.Screen name='(auth)/(tabs)'
name='(auth)/(tabs)' options={{
options={{ headerShown: false,
headerShown: false, title: "",
title: "", header: () => null,
header: () => null,
}}
/>
<Stack.Screen
name='(auth)/player'
options={{
headerShown: false,
title: "",
header: () => null,
}}
/>
<Stack.Screen
name='(auth)/now-playing'
options={{
headerShown: false,
presentation: "modal",
gestureEnabled: true,
}}
/>
<Stack.Screen
name='login'
options={{
headerShown: true,
title: "",
headerTransparent:
Platform.OS === "ios",
}}
/>
<Stack.Screen name='+not-found' />
<Stack.Screen
name='(auth)/tv-option-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-subtitle-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-request-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-season-select-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-series-season-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='tv-account-action-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='tv-account-select-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-user-switch-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
</Stack>
<Toaster
duration={4000}
toastOptions={{
style: {
backgroundColor: "#262626",
borderColor: "#363639",
borderWidth: 1,
},
titleStyle: {
color: "white",
},
}} }}
closeButton
/> />
{!Platform.isTV && <GlobalModal />} <Stack.Screen
</ThemeProvider> name='(auth)/player'
</IntroSheetProvider> options={{
</BottomSheetModalProvider> headerShown: false,
</GlobalModalProvider> title: "",
</MusicPlayerProvider> header: () => null,
</DownloadProvider> }}
</SyncPlayProvider> />
<Stack.Screen
name='(auth)/now-playing'
options={{
headerShown: false,
presentation: "modal",
gestureEnabled: true,
}}
/>
<Stack.Screen
name='login'
options={{
headerShown: true,
title: "",
headerTransparent: Platform.OS === "ios",
}}
/>
<Stack.Screen name='+not-found' />
<Stack.Screen
name='(auth)/tv-option-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-subtitle-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-request-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-season-select-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-series-season-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='tv-account-action-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='tv-account-select-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-user-switch-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
</Stack>
<Toaster
duration={4000}
toastOptions={{
style: {
backgroundColor: "#262626",
borderColor: "#363639",
borderWidth: 1,
},
titleStyle: {
color: "white",
},
}}
closeButton
/>
{!Platform.isTV && <GlobalModal />}
</ThemeProvider>
</IntroSheetProvider>
</BottomSheetModalProvider>
</GlobalModalProvider>
</MusicPlayerProvider>
</DownloadProvider>
</WebSocketProvider> </WebSocketProvider>
</LogProvider> </LogProvider>
</PlaySettingsProvider> </PlaySettingsProvider>

View File

@@ -1,5 +1,6 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 1,
"workspaces": { "workspaces": {
"": { "": {
"name": "streamyfin", "name": "streamyfin",

View File

@@ -1,7 +1,13 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
import React, { useEffect } from "react"; import React, { useEffect, useState } from "react";
import { Platform, StyleSheet, TouchableOpacity, View } from "react-native"; import {
type LayoutChangeEvent,
Platform,
StyleSheet,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useGlobalModal } from "@/providers/GlobalModalProvider"; import { useGlobalModal } from "@/providers/GlobalModalProvider";
@@ -211,6 +217,24 @@ const PlatformDropdownComponent = ({
}: PlatformDropdownProps) => { }: PlatformDropdownProps) => {
const { showModal, hideModal, isVisible } = useGlobalModal(); const { showModal, hideModal, isVisible } = useGlobalModal();
// @expo/ui's <Host> (SDK 55) fills its available space by default, and
// `matchContents` doesn't help here: it reports the native Menu's size via
// setStyleSize and overrides any explicit size. Instead we measure the
// trigger's intrinsic size in plain RN (off-layout) and pin it on the Host.
const [triggerSize, setTriggerSize] = useState<{
width: number;
height: number;
} | null>(null);
const handleMeasureTrigger = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout;
setTriggerSize((prev) =>
prev && prev.width === width && prev.height === height
? prev
: { width, height },
);
};
// Handle controlled open state for Android // Handle controlled open state for Android
useEffect(() => { useEffect(() => {
if (Platform.OS === "android" && controlledOpen === true) { if (Platform.OS === "android" && controlledOpen === true) {
@@ -241,11 +265,25 @@ const PlatformDropdownComponent = ({
}, [isVisible, controlledOpen, controlledOnOpenChange]); }, [isVisible, controlledOpen, controlledOnOpenChange]);
if (Platform.OS === "ios" && !Platform.isTV) { if (Platform.OS === "ios" && !Platform.isTV) {
// @expo/ui's <Host> can't size to content, so an in-flow invisible copy of // Pin the wrapper to the measured trigger size. @expo/ui's <Host> (SDK 55)
// the trigger sizes the wrapper while the Host overlays the real Menu. // fills its parent and reports its own size via setStyleSize, so it can't
// size itself to content. If the wrapper has no size, the Host's `flex: 1`
// height depends on the parent while the parent depends on the Host — a
// circular dependency that collapses to 0 for any selector nested more than
// one level deep (so only the first, shallowest dropdown stays visible).
// Giving the wrapper the measured size breaks the cycle; the Host then
// fills a concrete box.
return ( return (
<View> <View style={triggerSize ?? { opacity: 0 }}>
<View pointerEvents='none' aria-hidden style={{ opacity: 0 }}> {/* Hidden measurer: lays the trigger out off-flow to capture its
intrinsic size. Absolutely positioned WITHOUT right/bottom so it
sizes to the trigger's content rather than to its parent. */}
<View
style={{ position: "absolute", top: 0, left: 0, opacity: 0 }}
pointerEvents='none'
aria-hidden
onLayout={handleMeasureTrigger}
>
{trigger} {trigger}
</View> </View>
<Host style={[StyleSheet.absoluteFill, expoUIConfig?.hostStyle as any]}> <Host style={[StyleSheet.absoluteFill, expoUIConfig?.hostStyle as any]}>

View File

@@ -30,7 +30,6 @@ import { getDownloadedItemById } from "@/providers/Downloads/database";
import { useGlobalModal } from "@/providers/GlobalModalProvider"; import { useGlobalModal } from "@/providers/GlobalModalProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useOfflineMode } from "@/providers/OfflineModeProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { useSyncPlay } from "@/providers/SyncPlay/SyncPlayProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor"; import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl"; import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
@@ -68,14 +67,6 @@ export const PlayButton: React.FC<Props> = ({
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom); const user = useAtomValue(userAtom);
// SyncPlay: when enabled, we DO NOT navigate locally on play — we tell the
// server, which broadcasts a PlayQueue: NewPlaylist update to every group
// member (including us). Our `setStartPlaybackHandler` in SyncPlayProvider
// then performs the navigation uniformly for everyone, matching
// jellyfin-web's playbackManager intercept (Controller.play).
const { isEnabled: isSyncPlayEnabled, controller: syncPlayController } =
useSyncPlay();
// Use colors prop if provided, otherwise fallback to global atom // Use colors prop if provided, otherwise fallback to global atom
const effectiveColors = colors || globalColorAtom; const effectiveColors = colors || globalColorAtom;
@@ -103,37 +94,6 @@ export const PlayButton: React.FC<Props> = ({
const handleNormalPlayFlow = useCallback(async () => { const handleNormalPlayFlow = useCallback(async () => {
if (!item) return; if (!item) return;
// SyncPlay intercept: in a group, route playback through sthe server so
// every member gets the same PlayQueue: NewPlaylist update and navigates
// together. Skips local navigation and the Chromecast prompt entirely —
// SyncPlay + Chromecast isn't a supported combination yet, same as
// jellyfin-web.
if (isSyncPlayEnabled && syncPlayController && item.Id) {
try {
// Pass the full `item` (not just the ID) so the SyncPlay controller
// can run `translateItemsForPlayback` with full context — this is
// what jellyfin-web does, and it lets us expand Series / Season /
// BoxSet into real episode/track IDs before broadcasting the queue.
// Without expansion, receivers (jellyfin-web in particular) get
// container IDs they can't play and silently fail to open the
// player.
await syncPlayController.play({
items: [item],
ids: [item.Id],
startPositionTicks: item.UserData?.PlaybackPositionTicks ?? 0,
});
} catch (error) {
console.error("SyncPlay: failed to start group playback", error);
Alert.alert(
t("player.client_error"),
t("syncplay.failed_to_start", {
defaultValue: "Failed to start SyncPlay group playback",
}),
);
}
return;
}
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
itemId: item.Id!, itemId: item.Id!,
audioIndex: selectedOptions.audioIndex?.toString() ?? "", audioIndex: selectedOptions.audioIndex?.toString() ?? "",
@@ -330,8 +290,6 @@ export const PlayButton: React.FC<Props> = ({
goToPlayer, goToPlayer,
isOffline, isOffline,
t, t,
isSyncPlayEnabled,
syncPlayController,
]); ]);
const onPress = useCallback(async () => { const onPress = useCallback(async () => {

View File

@@ -26,7 +26,7 @@ export const TrackSheet: React.FC<Props> = ({
const streams = useMemo( const streams = useMemo(
() => source?.MediaStreams?.filter((x) => x.Type === streamType), () => source?.MediaStreams?.filter((x) => x.Type === streamType),
[source], [source, streamType],
); );
const selectedSteam = useMemo( const selectedSteam = useMemo(

View File

@@ -11,7 +11,6 @@ import { useTranslation } from "react-i18next";
import { FlatList, Modal, Pressable, StyleSheet, View } from "react-native"; import { FlatList, Modal, Pressable, StyleSheet, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
import { import {
type ChapterEntry, type ChapterEntry,
chapterStartsMs, chapterStartsMs,
@@ -39,7 +38,6 @@ function ChapterListComponent({
onClose, onClose,
}: ChapterListProps) { }: ChapterListProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const safeArea = useControlsSafeAreaInsets();
const listRef = useRef<FlatList<ChapterEntry>>(null); const listRef = useRef<FlatList<ChapterEntry>>(null);
const entries = useMemo(() => sortedChapters(chapters), [chapters]); const entries = useMemo(() => sortedChapters(chapters), [chapters]);
@@ -76,22 +74,9 @@ function ChapterListComponent({
transparent transparent
animationType='slide' animationType='slide'
onRequestClose={onClose} onRequestClose={onClose}
// iOS defaults <Modal> to portrait-only; without this it rotates the app
// back to portrait when opened from the landscape player. Android ignores it.
supportedOrientations={["portrait", "landscape"]}
> >
<Pressable onPress={onClose} style={styles.backdrop}> <Pressable onPress={onClose} style={styles.backdrop}>
<Pressable <Pressable onPress={(e) => e.stopPropagation()} style={styles.sheet}>
onPress={(e) => e.stopPropagation()}
style={[
styles.sheet,
{
marginLeft: safeArea.left,
marginRight: safeArea.right,
paddingBottom: safeArea.bottom,
},
]}
>
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.title}>{t("chapters.title")}</Text> <Text style={styles.title}>{t("chapters.title")}</Text>
<Pressable <Pressable
@@ -172,12 +157,14 @@ const styles = StyleSheet.create({
backdrop: { backdrop: {
flex: 1, flex: 1,
justifyContent: "flex-end", justifyContent: "flex-end",
backgroundColor: "rgba(0,0,0,0.6)",
}, },
sheet: { sheet: {
backgroundColor: Colors.background, backgroundColor: Colors.background,
borderTopLeftRadius: 16, borderTopLeftRadius: 16,
borderTopRightRadius: 16, borderTopRightRadius: 16,
maxHeight: "70%", maxHeight: "70%",
paddingBottom: 24,
}, },
header: { header: {
flexDirection: "row", flexDirection: "row",

View File

@@ -133,6 +133,7 @@ const HomeMobile = () => {
onPress={() => { onPress={() => {
router.push("/(auth)/downloads"); router.push("/(auth)/downloads");
}} }}
className='ml-1.5'
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
> >
<Feather <Feather

View File

@@ -37,20 +37,7 @@ export const ItemPeopleSections: React.FC<Props> = ({ item, ...props }) => {
return { ...item, People: people } as BaseItemDto; return { ...item, People: people } as BaseItemDto;
}, [item, people]); }, [item, people]);
// Jellyfin can list the same person several times (e.g. an actor also const topPeople = useMemo(() => people.slice(0, 3), [people]);
// credited as writer). Dedupe by Id so the same actor section isn't rendered
// twice and we still surface 3 distinct people.
const topPeople = useMemo(() => {
const seen = new Set<string>();
const unique: BaseItemPerson[] = [];
for (const person of people) {
if (!person.Id || seen.has(person.Id)) continue;
seen.add(person.Id);
unique.push(person);
if (unique.length >= 3) break;
}
return unique;
}, [people]);
const renderActorSection = useCallback( const renderActorSection = useCallback(
(person: BaseItemPerson, idx: number, total: number) => { (person: BaseItemPerson, idx: number, total: number) => {

View File

@@ -51,7 +51,7 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
api, api,
item: library, item: library,
}), }),
[library], [api, library],
); );
const itemType = useMemo(() => { const itemType = useMemo(() => {

View File

@@ -1,6 +1,6 @@
import { t } from "i18next"; import { t } from "i18next";
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { Platform, ScrollView, View } from "react-native"; import { ScrollView, View } from "react-native";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useScaledTVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
@@ -107,7 +107,7 @@ export const TVAddServerForm: React.FC<TVAddServerFormProps> = ({
</View> </View>
{/* Pair with Phone */} {/* Pair with Phone */}
{Platform.OS !== "ios" && onStartPairing && ( {onStartPairing && (
<View> <View>
<Button <Button
onPress={onStartPairing} onPress={onStartPairing}

View File

@@ -401,6 +401,10 @@ export const TVJellyseerrSearchResults: React.FC<
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const hasMovies = movieResults && movieResults.length > 0;
const hasTv = tvResults && tvResults.length > 0;
const hasPersons = personResults && personResults.length > 0;
if (loading) { if (loading) {
return null; return null;
} }
@@ -427,26 +431,22 @@ export const TVJellyseerrSearchResults: React.FC<
return ( return (
<View> <View>
{/* No section requests `hasTVPreferredFocus`: the native search field
keeps focus while typing, otherwise the first result would re-grab
focus on every keystroke as results re-render. The user navigates
down to the grid manually. */}
<TVJellyseerrMovieSection <TVJellyseerrMovieSection
title={t("search.request_movies")} title={t("search.request_movies")}
items={movieResults} items={movieResults}
isFirstSection={false} isFirstSection={hasMovies}
onItemPress={onMoviePress} onItemPress={onMoviePress}
/> />
<TVJellyseerrTvSection <TVJellyseerrTvSection
title={t("search.request_series")} title={t("search.request_series")}
items={tvResults} items={tvResults}
isFirstSection={false} isFirstSection={!hasMovies && hasTv}
onItemPress={onTvPress} onItemPress={onTvPress}
/> />
<TVJellyseerrPersonSection <TVJellyseerrPersonSection
title={t("search.actors")} title={t("search.actors")}
items={personResults} items={personResults}
isFirstSection={false} isFirstSection={!hasMovies && !hasTv && hasPersons}
onItemPress={onPersonPress} onItemPress={onPersonPress}
/> />
</View> </View>

View File

@@ -235,13 +235,10 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
module). It renders the native search bar + grid keyboard and module). It renders the native search bar + grid keyboard and
forwards typed text into the existing query pipeline via setSearch; forwards typed text into the existing query pipeline via setSearch;
our own results grid renders below. */} our own results grid renders below. */}
{/* No horizontal margin here: the native tvOS search bar centers itself
and renders a trailing "Hold to Dictate in <Language>" hint. Extra
margins squeeze the bar's width and clip that trailing hint, so let
the native view span the full width and own its own insets. */}
<View <View
style={{ style={{
marginBottom: 24, marginBottom: 24,
marginHorizontal: HORIZONTAL_PADDING,
height: SEARCH_AREA_HEIGHT, height: SEARCH_AREA_HEIGHT,
}} }}
> >
@@ -283,17 +280,13 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
{/* Library Search Results */} {/* Library Search Results */}
{isLibraryMode && !loading && ( {isLibraryMode && !loading && (
<View style={{ gap: SECTION_GAP }}> <View style={{ gap: SECTION_GAP }}>
{sections.map((section) => ( {sections.map((section, index) => (
<TVSearchSection <TVSearchSection
key={section.key} key={section.key}
title={section.title} title={section.title}
items={section.items!} items={section.items!}
orientation={section.orientation || "vertical"} orientation={section.orientation || "vertical"}
// Never auto-focus a result. The native search field owns focus isFirstSection={index === 0}
// while typing; `hasTVPreferredFocus` here would re-grab focus on
// every keystroke as results re-render. User navigates down to the
// grid manually.
isFirstSection={false}
onItemPress={onItemPress} onItemPress={onItemPress}
onItemLongPress={onItemLongPress} onItemLongPress={onItemLongPress}
imageUrlGetter={ imageUrlGetter={

View File

@@ -297,12 +297,12 @@ export const TVSearchSection: React.FC<TVSearchSectionProps> = ({
removeClippedSubviews={false} removeClippedSubviews={false}
getItemLayout={getItemLayout} getItemLayout={getItemLayout}
style={{ overflow: "visible" }} style={{ overflow: "visible" }}
// Edge padding via contentContainerStyle, NOT contentInset+contentOffset. contentInset={{
// contentOffset only applies on initial mount; since this FlatList is left: edgePadding,
// reused across searches (stable key), a second search left the inset right: edgePadding,
// without the offset and the grid snapped flush to the left edge. }}
contentOffset={{ x: -edgePadding, y: 0 }}
contentContainerStyle={{ contentContainerStyle={{
paddingHorizontal: edgePadding,
paddingVertical: SCALE_PADDING, paddingVertical: SCALE_PADDING,
}} }}
/> />

View File

@@ -31,12 +31,8 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
}) => { }) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const router = useRouter();
const isOffline = useOfflineMode(); const isOffline = useOfflineMode();
// Read the live (cached) downloads DB inside the query rather than the const router = useRouter();
// provider's downloadedItems snapshot, so refetches after
// updateDownloadedItem() reflect the latest state instead of a stale
// refreshKey-gated snapshot. getAllDownloadedItems() is cached, so this stays cheap.
const { getDownloadedItems } = useDownload(); const { getDownloadedItems } = useDownload();
const scrollRef = useRef<HorizontalScrollRef>(null); const scrollRef = useRef<HorizontalScrollRef>(null);
@@ -104,7 +100,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
onPress={() => { onPress={() => {
router.setParams({ id: _item.Id }); router.setParams({ id: _item.Id });
}} }}
className={`flex flex-col w-44 className={`flex flex-col w-44
${item?.Id === _item.Id ? "" : "opacity-50"} ${item?.Id === _item.Id ? "" : "opacity-50"}
`} `}
> >

View File

@@ -229,7 +229,7 @@ export const LibraryOptionsSheet: React.FC<Props> = ({
/> />
</OptionGroup> </OptionGroup>
<OptionGroup title={t("library.options.options_title")}> <OptionGroup title='Options'>
<ToggleItem <ToggleItem
label={t("library.options.show_titles")} label={t("library.options.show_titles")}
value={settings.showTitles} value={settings.showTitles}

View File

@@ -196,10 +196,7 @@ export const OtherSettings: React.FC = () => {
} }
/> />
</ListItem> </ListItem>
<ListItem <ListItem title={t("home.settings.other.max_auto_play_episode_count")}>
title={t("home.settings.other.max_auto_play_episode_count")}
disabled={pluginSettings?.maxAutoPlayEpisodeCount?.locked}
>
<PlatformDropdown <PlatformDropdown
groups={autoPlayEpisodeOptions} groups={autoPlayEpisodeOptions}
trigger={ trigger={

View File

@@ -229,10 +229,7 @@ export const PlaybackControlsSettings: React.FC = () => {
<ListItem <ListItem
title={t("home.settings.other.max_auto_play_episode_count")} title={t("home.settings.other.max_auto_play_episode_count")}
disabled={ disabled={!settings.autoPlayNextEpisode}
!settings.autoPlayNextEpisode ||
pluginSettings?.maxAutoPlayEpisodeCount?.locked
}
> >
<PlatformDropdown <PlatformDropdown
groups={autoPlayEpisodeOptions} groups={autoPlayEpisodeOptions}

View File

@@ -1,235 +0,0 @@
/**
* GroupSelectionMenu
*
* Content rendered inside the SyncPlay bottom sheet (the sheet itself is
* owned by SyncPlayButton). Calls `onClose` after successful actions to
* dismiss the parent sheet.
*/
import { Ionicons } from "@expo/vector-icons";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { useSyncPlay } from "@/providers/SyncPlay";
import type { GroupInfoDto } from "@/providers/SyncPlay/types";
interface GroupSelectionMenuProps {
onClose: () => void;
}
export function GroupSelectionMenu({ onClose }: GroupSelectionMenuProps) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const {
isEnabled,
groupInfo,
canCreateGroups,
joinGroup,
createGroup,
leaveGroup,
getGroups,
} = useSyncPlay();
const [groups, setGroups] = useState<GroupInfoDto[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isCreating, setIsCreating] = useState(false);
useEffect(() => {
let cancelled = false;
(async () => {
setIsLoading(true);
try {
const fetchedGroups = await getGroups();
if (!cancelled) {
setGroups(fetchedGroups);
}
} catch (error) {
console.error("Failed to fetch groups", error);
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
})();
return () => {
cancelled = true;
};
}, [getGroups]);
const handleJoinGroup = useCallback(
async (groupId: string) => {
try {
await joinGroup(groupId);
onClose();
} catch (error) {
console.error("Failed to join group", error);
}
},
[joinGroup, onClose],
);
const handleCreateGroup = useCallback(async () => {
setIsCreating(true);
try {
await createGroup();
onClose();
} catch (error) {
console.error("Failed to create group", error);
} finally {
setIsCreating(false);
}
}, [createGroup, onClose]);
const handleLeaveGroup = useCallback(async () => {
try {
await leaveGroup();
onClose();
} catch (error) {
console.error("Failed to leave group", error);
}
}, [leaveGroup, onClose]);
const containerStyle = {
paddingLeft: Math.max(16, insets.left),
paddingRight: Math.max(16, insets.right),
paddingBottom: Math.max(16, insets.bottom),
paddingTop: 8,
};
if (isEnabled && groupInfo) {
return (
<View style={containerStyle}>
<View className='mb-4'>
<View className='flex-row items-center mb-2'>
<Ionicons name='people' size={24} color='#00a4dc' />
<Text className='font-bold text-xl text-neutral-100 ml-2'>
{t("syncplay.title")}
</Text>
</View>
<Text className='text-neutral-400'>{t("syncplay.my_group")}</Text>
</View>
<View className='bg-neutral-800 rounded-xl p-4 mb-4'>
<View className='flex-row items-center justify-between mb-3'>
<Text className='text-neutral-100 font-semibold text-lg'>
{groupInfo.GroupName}
</Text>
<View className='bg-[#00a4dc] px-2 py-1 rounded'>
<Text className='text-white text-xs font-medium'>
{groupInfo.State}
</Text>
</View>
</View>
{groupInfo.Participants && groupInfo.Participants.length > 0 && (
<View className='flex-row items-center'>
<Ionicons name='person' size={16} color='#9ca3af' />
<Text className='text-neutral-400 ml-2'>
{groupInfo.Participants.length} {t("syncplay.members")}
</Text>
</View>
)}
</View>
<Button onPress={handleLeaveGroup} color='red'>
<View className='flex-row items-center justify-center'>
<Ionicons name='exit-outline' size={20} color='white' />
<Text className='text-white font-semibold ml-2'>
{t("syncplay.leave_group")}
</Text>
</View>
</Button>
</View>
);
}
return (
<View style={containerStyle}>
<View className='mb-4'>
<View className='flex-row items-center mb-2'>
<Ionicons name='people-outline' size={24} color='white' />
<Text className='font-bold text-xl text-neutral-100 ml-2'>
{t("syncplay.title")}
</Text>
</View>
<Text className='text-neutral-400'>{t("syncplay.join_group")}</Text>
</View>
{isLoading && (
<View className='py-8 items-center'>
<ActivityIndicator color='#00a4dc' />
</View>
)}
{!isLoading && groups.length > 0 && (
<View className='mb-4'>
<Text className='text-neutral-400 text-sm mb-2 ml-1'>
{t("syncplay.available_groups")}
</Text>
<View className='bg-neutral-800 rounded-xl overflow-hidden'>
{groups.map((group, index) => (
<TouchableOpacity
key={group.GroupId ?? index}
onPress={() => group.GroupId && handleJoinGroup(group.GroupId)}
className={`flex-row items-center p-4 ${
index < groups.length - 1 ? "border-b border-neutral-700" : ""
}`}
>
<View className='w-10 h-10 bg-[#00a4dc]/20 rounded-full items-center justify-center mr-3'>
<Ionicons name='people' size={20} color='#00a4dc' />
</View>
<View className='flex-1'>
<Text className='text-neutral-100 font-medium'>
{group.GroupName}
</Text>
<Text className='text-neutral-500 text-sm'>
{group.Participants?.length ?? 0} {t("syncplay.members")} {" "}
{group.State}
</Text>
</View>
<Ionicons name='chevron-forward' size={20} color='#9ca3af' />
</TouchableOpacity>
))}
</View>
</View>
)}
{!isLoading && groups.length === 0 && (
<View className='bg-neutral-800/50 rounded-xl p-6 mb-4 items-center'>
<Ionicons name='people-outline' size={40} color='#6b7280' />
<Text className='text-neutral-400 text-center mt-3'>
{t("syncplay.available_groups")}: 0{"\n"}
{t("syncplay.create_new_group")}
</Text>
</View>
)}
{canCreateGroups && (
<Button
onPress={handleCreateGroup}
color='purple'
disabled={isCreating}
>
<View className='flex-row items-center justify-center'>
{isCreating ? (
<ActivityIndicator size='small' color='white' />
) : (
<>
<Ionicons name='add' size={20} color='white' />
<Text className='text-white font-semibold ml-2'>
{t("syncplay.create_new_group")}
</Text>
</>
)}
</View>
</Button>
)}
</View>
);
}

View File

@@ -1,206 +0,0 @@
/**
* SyncPlayActionIcon
*
* In-button SyncPlay status indicator — drops into the player's
* play/pause button slot and replaces the normal play/pause/loader
* graphic while SyncPlay is mid-transition. Mirrors jellyfin-web's
* `#syncPlayIcon` element (see `showIcon()` in
* `jellyfin-web/src/controllers/playback/video/index.js`).
*
* Icon vocabulary (matched 1:1 with jellyfin-web's `showIcon` switch):
*
* action primary secondary pulse spin
* --------------- ------------- ----------------- ---------- ----
* schedule-play sync play (centered) infinite yes
* unpause play-circle — one-shot no
* pause pause-circle — one-shot no
* seek refresh — infinite no
* buffering clock — infinite no
* wait-pause clock pause (shifted) infinite no
* wait-unpause clock play (shifted) infinite no
*
* Material → Ionicons mapping used here:
* sync → sync, schedule → time-outline, update → refresh-outline,
* play_arrow → play, pause → pause,
* play_circle_outline → play-circle-outline,
* pause_circle_outline → pause-circle-outline.
*
* When no SyncPlay action is active the component renders `fallback`
* so callers can keep the normal play/pause/loader graphic.
*/
import { Ionicons } from "@expo/vector-icons";
import { type ReactNode, useEffect } from "react";
import { StyleSheet, View } from "react-native";
import Animated, {
cancelAnimation,
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withSequence,
withTiming,
} from "react-native-reanimated";
import type { SyncPlayOsdAction } from "@/providers/SyncPlay";
import { useSyncPlay } from "@/providers/SyncPlay";
// SyncPlay cyan (matches jellyfin-web's `.syncPlayIconCircle` color)
const SYNC_PLAY_COLOR = "#00a4dc";
type IoniconName = keyof typeof Ionicons.glyphMap;
type SecondaryPosition = "centered" | "shifted";
interface SecondaryIcon {
icon: IoniconName;
position: SecondaryPosition;
}
interface OsdConfig {
/** Primary icon — fills the available size. */
icon: IoniconName;
/** Optional smaller overlay (~42% size). */
secondary?: SecondaryIcon;
/** Wrapper-level scale animation. */
pulse: "infinite" | "oneshot";
/** Rotate the primary icon continuously (secondary stays still). */
spin?: boolean;
}
const CONFIG: Record<SyncPlayOsdAction, OsdConfig> = {
"schedule-play": {
icon: "sync",
secondary: { icon: "play", position: "centered" },
pulse: "infinite",
spin: true,
},
unpause: { icon: "play-circle-outline", pulse: "oneshot" },
pause: { icon: "pause-circle-outline", pulse: "oneshot" },
seek: { icon: "refresh-outline", pulse: "infinite" },
buffering: { icon: "time-outline", pulse: "infinite" },
"wait-pause": {
icon: "time-outline",
secondary: { icon: "pause", position: "shifted" },
pulse: "infinite",
},
"wait-unpause": {
icon: "time-outline",
secondary: { icon: "play", position: "shifted" },
pulse: "infinite",
},
};
interface SyncPlayActionIconProps {
size: number;
color?: string;
/** Rendered when no SyncPlay action is active. */
fallback?: ReactNode;
}
export function SyncPlayActionIcon({
size,
color = SYNC_PLAY_COLOR,
fallback = null,
}: SyncPlayActionIconProps) {
const { osdAction } = useSyncPlay();
const rotation = useSharedValue(0);
const scale = useSharedValue(1);
useEffect(() => {
cancelAnimation(rotation);
cancelAnimation(scale);
rotation.value = 0;
scale.value = 1;
if (!osdAction) return;
const config = CONFIG[osdAction];
if (config.spin) {
rotation.value = withRepeat(
withTiming(360, { duration: 1200, easing: Easing.linear }),
-1,
false,
);
}
if (config.pulse === "infinite") {
scale.value = withRepeat(
withSequence(
withTiming(1.1, {
duration: 700,
easing: Easing.inOut(Easing.quad),
}),
withTiming(0.95, {
duration: 700,
easing: Easing.inOut(Easing.quad),
}),
),
-1,
true,
);
} else {
// one-shot: single scale flash; the provider clears the action
// ~1500ms later (transient OSD) so the icon then unmounts.
scale.value = withSequence(
withTiming(1.2, { duration: 220, easing: Easing.out(Easing.quad) }),
withTiming(1, { duration: 220, easing: Easing.inOut(Easing.quad) }),
);
}
}, [osdAction, rotation, scale]);
const pulseStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const spinStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }],
}));
if (!osdAction) return <>{fallback}</>;
const config = CONFIG[osdAction];
const secondarySize = Math.round(size * 0.42);
// centered: geometric middle of the primary (e.g. play arrow inside
// the spinning `sync` ring for schedule-play).
// shifted: bottom-right corner (e.g. play/pause badge on the clock
// for wait-unpause / wait-pause).
const secondaryPosStyle =
config.secondary?.position === "centered"
? {
top: (size - secondarySize) / 2,
left: (size - secondarySize) / 2,
}
: { bottom: 0, right: 0 };
return (
<Animated.View style={pulseStyle}>
<View style={{ width: size, height: size }}>
<Animated.View style={[StyleSheet.absoluteFill, spinStyle]}>
<Ionicons name={config.icon} size={size} color={color} />
</Animated.View>
{config.secondary && (
<View
pointerEvents='none'
style={[styles.secondary, secondaryPosStyle]}
>
<Ionicons
name={config.secondary.icon}
size={secondarySize}
color={color}
/>
</View>
)}
</View>
</Animated.View>
);
}
const styles = StyleSheet.create({
secondary: {
position: "absolute",
},
});

View File

@@ -1,97 +0,0 @@
/**
* SyncPlayButton
*
* Header button for accessing SyncPlay functionality.
* Shows group status and opens the group selection sheet.
*
* Uses the @expo/ui drop-in BottomSheetModal (SwiftUI sheet on iOS, Jetpack
* Compose ModalBottomSheet on Android). Because it presents natively, it
* works correctly even when triggered from `headerRight` — no portal or
* provider context is required (unlike @gorhom/bottom-sheet, which fails
* silently from detached UINavigationItem subtrees).
*
* Safe to import statically: this whole module is lazy-required only on
* non-TV platforms by app/(auth)/(tabs)/(home)/_layout.tsx.
*/
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@expo/ui/community/bottom-sheet";
import { Ionicons } from "@expo/vector-icons";
import { useCallback, useRef } from "react";
import { Platform, View } from "react-native";
import { Pressable } from "react-native-gesture-handler";
import { useCastDevice } from "react-native-google-cast";
import { toast } from "sonner-native";
import { useNetworkStatus } from "@/providers/NetworkStatusProvider";
import { useSyncPlay } from "@/providers/SyncPlay";
import { GroupSelectionMenu } from "./GroupSelectionMenu";
interface SyncPlayButtonProps {
size?: number;
}
export function SyncPlayButton({ size = 22 }: SyncPlayButtonProps) {
const { isEnabled, canJoinGroups } = useSyncPlay();
const { isConnected } = useNetworkStatus();
const castDevice = useCastDevice();
const sheetRef = useRef<BottomSheetMethods>(null);
const isCasting = !!castDevice;
const handlePress = useCallback(() => {
if (isCasting) {
toast("SyncPlay not available while casting");
return;
}
sheetRef.current?.present();
}, [isCasting]);
const handleDismiss = useCallback(() => {
sheetRef.current?.dismiss();
}, []);
if (Platform.isTV) return null;
if (!canJoinGroups) return null;
if (!isConnected) return null;
const iconColor = isCasting ? "#6b7280" : isEnabled ? "#00a4dc" : "white";
return (
<>
<Pressable
className='mr-4'
onPress={handlePress}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<View className='relative'>
<Ionicons
name={isEnabled ? "people" : "people-outline"}
size={size}
color={iconColor}
/>
{isEnabled && !isCasting && (
<View
className='absolute -top-0.5 -right-0.5 w-2.5 h-2.5 rounded-full bg-[#00a4dc]'
style={{
borderWidth: 1,
borderColor: "#171717",
}}
/>
)}
</View>
</Pressable>
<BottomSheetModal
ref={sheetRef}
snapPoints={Platform.OS === "android" ? ["100%"] : ["60%"]}
enablePanDownToClose
>
<BottomSheetView>
<GroupSelectionMenu onClose={handleDismiss} />
</BottomSheetView>
</BottomSheetModal>
</>
);
}

View File

@@ -1,53 +0,0 @@
/**
* SyncPlaySpinner
*
* Compact rotating SyncPlay icon shown in place of the play/pause button
* while a play/pause command is in flight to the server (the "schedule-play"
* indicator from jellyfin-web).
*/
import { Ionicons } from "@expo/vector-icons";
import { useEffect } from "react";
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated";
// SyncPlay cyan color (matches jellyfin-web)
const SYNC_PLAY_COLOR = "#00a4dc";
interface SyncPlaySpinnerProps {
size: number;
color?: string;
}
export function SyncPlaySpinner({
size,
color = SYNC_PLAY_COLOR,
}: SyncPlaySpinnerProps) {
const rotation = useSharedValue(0);
useEffect(() => {
rotation.value = withRepeat(
withTiming(360, {
duration: 1200,
easing: Easing.linear,
}),
-1,
false,
);
}, [rotation]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }],
}));
return (
<Animated.View style={animatedStyle}>
<Ionicons name='sync' size={size} color={color} />
</Animated.View>
);
}

View File

@@ -1,8 +0,0 @@
/**
* SyncPlay UI Components
*/
export { GroupSelectionMenu } from "./GroupSelectionMenu";
export { SyncPlayActionIcon } from "./SyncPlayActionIcon";
export { SyncPlayButton } from "./SyncPlayButton";
export { SyncPlaySpinner } from "./SyncPlaySpinner";

View File

@@ -8,10 +8,10 @@ import { useTranslation } from "react-i18next";
import { Pressable, View } from "react-native"; import { Pressable, View } from "react-native";
import { Slider } from "react-native-awesome-slider"; import { Slider } from "react-native-awesome-slider";
import { type SharedValue } from "react-native-reanimated"; import { type SharedValue } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ChapterList } from "@/components/chapters/ChapterList"; import { ChapterList } from "@/components/chapters/ChapterList";
import { ChapterTicks } from "@/components/chapters/ChapterTicks"; import { ChapterTicks } from "@/components/chapters/ChapterTicks";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { chapterMarkers, chapterNameAt } from "@/utils/chapters"; import { chapterMarkers, chapterNameAt } from "@/utils/chapters";
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton"; import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
@@ -75,6 +75,9 @@ interface BottomControlsProps {
minutes: number; minutes: number;
seconds: number; seconds: number;
}; };
// Chapter props
chapterPositions?: number[];
} }
export const BottomControls: FC<BottomControlsProps> = ({ export const BottomControls: FC<BottomControlsProps> = ({
@@ -108,10 +111,11 @@ export const BottomControls: FC<BottomControlsProps> = ({
trickPlayUrl, trickPlayUrl,
trickplayInfo, trickplayInfo,
time, time,
chapterPositions = [],
}) => { }) => {
const { settings } = useSettings(); const { settings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useControlsSafeAreaInsets(); const insets = useSafeAreaInsets();
const [chapterListVisible, setChapterListVisible] = useState(false); const [chapterListVisible, setChapterListVisible] = useState(false);
// Only expose chapter UI when there are at least two real markers. // Only expose chapter UI when there are at least two real markers.
@@ -142,9 +146,13 @@ export const BottomControls: FC<BottomControlsProps> = ({
style={[ style={[
{ {
position: "absolute", position: "absolute",
right: insets.right, right:
left: insets.left, (settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
bottom: Math.max(insets.bottom - 17, 0), left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
bottom:
(settings?.safeAreaInControlsEnabled ?? true)
? Math.max(insets.bottom - 17, 0)
: 0,
}, },
]} ]}
className={"flex flex-col px-2"} className={"flex flex-col px-2"}
@@ -180,6 +188,17 @@ export const BottomControls: FC<BottomControlsProps> = ({
) : null} ) : null}
</View> </View>
<View className='flex flex-row items-center space-x-2 shrink-0'> <View className='flex flex-row items-center space-x-2 shrink-0'>
{hasChapters && (
<Pressable
onPress={() => setChapterListVisible(true)}
hitSlop={10}
className='justify-center mr-4'
accessibilityRole='button'
accessibilityLabel={t("chapters.open")}
>
<Ionicons name='bookmarks' size={24} color='white' />
</Pressable>
)}
<SkipButton <SkipButton
showButton={showSkipButton} showButton={showSkipButton}
onPress={skipIntro} onPress={skipIntro}
@@ -211,17 +230,6 @@ export const BottomControls: FC<BottomControlsProps> = ({
onPress={handleNextEpisodeManual} onPress={handleNextEpisodeManual}
/> />
)} )}
{hasChapters && (
<Pressable
onPress={() => setChapterListVisible(true)}
hitSlop={10}
className='justify-center ml-4'
accessibilityRole='button'
accessibilityLabel={t("chapters.open")}
>
<Ionicons name='bookmarks' size={24} color='white' />
</Pressable>
)}
</View> </View>
</View> </View>
<View <View

View File

@@ -1,10 +1,9 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import type { FC } from "react"; import type { FC } from "react";
import { Platform, TouchableOpacity, View } from "react-native"; import { Platform, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader"; import { Loader } from "@/components/Loader";
import { SyncPlayActionIcon } from "@/components/syncplay/SyncPlayActionIcon";
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import AudioSlider from "./AudioSlider"; import AudioSlider from "./AudioSlider";
import BrightnessSlider from "./BrightnessSlider"; import BrightnessSlider from "./BrightnessSlider";
@@ -43,15 +42,15 @@ export const CenterControls: FC<CenterControlsProps> = ({
goToNextChapter, goToNextChapter,
}) => { }) => {
const { settings } = useSettings(); const { settings } = useSettings();
const insets = useControlsSafeAreaInsets(); const insets = useSafeAreaInsets();
return ( return (
<View <View
style={{ style={{
position: "absolute", position: "absolute",
top: "50%", top: "50%",
left: insets.left, left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
right: insets.right, right: (settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
@@ -122,20 +121,15 @@ export const CenterControls: FC<CenterControlsProps> = ({
<View style={Platform.isTV ? { flex: 1, alignItems: "center" } : {}}> <View style={Platform.isTV ? { flex: 1, alignItems: "center" } : {}}>
<TouchableOpacity onPress={togglePlay}> <TouchableOpacity onPress={togglePlay}>
<SyncPlayActionIcon {!isBuffering ? (
size={ICON_SIZES.CENTER} <Ionicons
fallback={ name={isPlaying ? "pause" : "play"}
!isBuffering ? ( size={ICON_SIZES.CENTER}
<Ionicons color='white'
name={isPlaying ? "pause" : "play"} />
size={ICON_SIZES.CENTER} ) : (
color='white' <Loader size={"large"} />
/> )}
) : (
<Loader size={"large"} />
)
}
/>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View File

@@ -219,6 +219,7 @@ export const Controls: FC<Props> = ({
hasNextChapter, hasNextChapter,
goToPreviousChapter, goToPreviousChapter,
goToNextChapter, goToNextChapter,
chapterPositions,
} = useChapterNavigation({ } = useChapterNavigation({
chapters: item.Chapters, chapters: item.Chapters,
progress, progress,
@@ -365,9 +366,7 @@ export const Controls: FC<Props> = ({
{ applyLanguagePreferences: true }, { applyLanguagePreferences: true },
); );
// Use setParams instead of replace to avoid unmounting/remounting the player, const queryParams = new URLSearchParams({
// which would create a new MPV native view and crash with "mp_initialize already initialized".
router.setParams({
...(offline && { offline: "true" }), ...(offline && { offline: "true" }),
itemId: item.Id ?? "", itemId: item.Id ?? "",
audioIndex: defaultAudioIndex?.toString() ?? "", audioIndex: defaultAudioIndex?.toString() ?? "",
@@ -376,17 +375,11 @@ export const Controls: FC<Props> = ({
bitrateValue: bitrateValue?.toString(), bitrateValue: bitrateValue?.toString(),
playbackPosition: playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "", item.UserData?.PlaybackPositionTicks?.toString() ?? "",
}); }).toString();
router.replace(`player/direct-player?${queryParams}` as any);
}, },
[ [settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router],
settings,
subtitleIndex,
audioIndex,
mediaSource,
bitrateValue,
router,
offline,
],
); );
const goToPreviousItem = useCallback(() => { const goToPreviousItem = useCallback(() => {
@@ -592,6 +585,7 @@ export const Controls: FC<Props> = ({
trickPlayUrl={trickPlayUrl} trickPlayUrl={trickPlayUrl}
trickplayInfo={trickplayInfo} trickplayInfo={trickplayInfo}
time={isSliding || showRemoteBubble ? time : remoteTime} time={isSliding || showRemoteBubble ? time : remoteTime}
chapterPositions={chapterPositions}
/> />
</Animated.View> </Animated.View>
</> </>

View File

@@ -1254,7 +1254,7 @@ export const Controls: FC<Props> = ({
<Text <Text
style={[styles.endsAtText, { fontSize: typography.callout }]} style={[styles.endsAtText, { fontSize: typography.callout }]}
> >
{t("player.ends_at", { time: getFinishTime() })} {t("player.ends_at")} {getFinishTime()}
</Text> </Text>
</View> </View>
)} )}
@@ -1448,7 +1448,7 @@ export const Controls: FC<Props> = ({
<Text <Text
style={[styles.endsAtText, { fontSize: typography.callout }]} style={[styles.endsAtText, { fontSize: typography.callout }]}
> >
{t("player.ends_at", { time: getFinishTime() })} {t("player.ends_at")} {getFinishTime()}
</Text> </Text>
</View> </View>
)} )}

View File

@@ -5,6 +5,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { import {
HorizontalScroll, HorizontalScroll,
@@ -16,10 +17,10 @@ import {
SeasonDropdown, SeasonDropdown,
type SeasonIndexState, type SeasonIndexState,
} from "@/components/series/SeasonDropdown"; } from "@/components/series/SeasonDropdown";
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
import { useDownload } from "@/providers/DownloadProvider"; 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 { import {
getDownloadedEpisodesForSeason, getDownloadedEpisodesForSeason,
getDownloadedSeasonNumbers, getDownloadedSeasonNumbers,
@@ -45,7 +46,8 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
scrollViewRef.current?.scrollToIndex(index, 100); scrollViewRef.current?.scrollToIndex(index, 100);
}; };
const isOffline = useOfflineMode(); const isOffline = useOfflineMode();
const insets = useControlsSafeAreaInsets(); const { settings } = useSettings();
const insets = useSafeAreaInsets();
// Set the initial season index // Set the initial season index
useEffect(() => { useEffect(() => {
@@ -57,11 +59,6 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
} }
}, []); }, []);
// Read the live (cached) downloads DB inside the query rather than the
// provider's downloadedItems snapshot. The snapshot only refreshes on the
// provider refreshKey, so after updateDownloadedItem() invalidates
// ["episodes"]/["seasons"] (e.g. progress/played writes) the refetch would
// return stale data. getAllDownloadedItems() is cached, so this stays cheap.
const { getDownloadedItems } = useDownload(); const { getDownloadedItems } = useDownload();
const seasonIndex = seasonIndexState[item.ParentId ?? ""]; const seasonIndex = seasonIndexState[item.ParentId ?? ""];
@@ -185,9 +182,12 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
backgroundColor: "black", backgroundColor: "black",
height: "100%", height: "100%",
width: "100%", width: "100%",
paddingTop: insets.top, paddingTop:
paddingLeft: insets.left, (settings?.safeAreaInControlsEnabled ?? true) ? insets.top : 0,
paddingRight: insets.right, paddingLeft:
(settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
paddingRight:
(settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
}} }}
> >
<View <View

View File

@@ -5,11 +5,12 @@ import type {
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import { type FC, useCallback, useState } from "react"; import { type FC, useCallback, useState } from "react";
import { Platform, TouchableOpacity, View } from "react-native"; import { Platform, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
import { useHaptic } from "@/hooks/useHaptic"; import { useHaptic } from "@/hooks/useHaptic";
import { useOrientation } from "@/hooks/useOrientation"; import { useOrientation } from "@/hooks/useOrientation";
import { OrientationLock } from "@/packages/expo-screen-orientation"; import { OrientationLock } from "@/packages/expo-screen-orientation";
import { useSettings } from "@/utils/atoms/settings";
import { HEADER_LAYOUT, ICON_SIZES } from "./constants"; import { HEADER_LAYOUT, ICON_SIZES } from "./constants";
import DropdownView from "./dropdown/DropdownView"; import DropdownView from "./dropdown/DropdownView";
import { PlaybackSpeedScope } from "./utils/playback-speed-settings"; import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
@@ -57,8 +58,9 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
showTechnicalInfo = false, showTechnicalInfo = false,
onToggleTechnicalInfo, onToggleTechnicalInfo,
}) => { }) => {
const { settings } = useSettings();
const router = useRouter(); const router = useRouter();
const insets = useControlsSafeAreaInsets(); const insets = useSafeAreaInsets();
const lightHapticFeedback = useHaptic("light"); const lightHapticFeedback = useHaptic("light");
const { orientation, lockOrientation } = useOrientation(); const { orientation, lockOrientation } = useOrientation();
const [isTogglingOrientation, setIsTogglingOrientation] = useState(false); const [isTogglingOrientation, setIsTogglingOrientation] = useState(false);
@@ -97,9 +99,10 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
style={[ style={[
{ {
position: "absolute", position: "absolute",
top: insets.top, top: (settings?.safeAreaInControlsEnabled ?? true) ? insets.top : 0,
left: insets.left, left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
right: insets.right, right:
(settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
padding: HEADER_LAYOUT.CONTAINER_PADDING, padding: HEADER_LAYOUT.CONTAINER_PADDING,
}, },
]} ]}

View File

@@ -16,8 +16,8 @@ import Animated, {
} from "react-native-reanimated"; } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useScaledTVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
import type { TechnicalInfo } from "@/modules/mpv-player"; import type { TechnicalInfo } from "@/modules/mpv-player";
import { useSettings } from "@/utils/atoms/settings";
import { HEADER_LAYOUT } from "./constants"; import { HEADER_LAYOUT } from "./constants";
type PlayMethod = "DirectPlay" | "DirectStream" | "Transcode"; type PlayMethod = "DirectPlay" | "DirectStream" | "Transcode";
@@ -184,8 +184,8 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
currentAudioIndex, currentAudioIndex,
}) => { }) => {
const typography = useScaledTVTypography(); const typography = useScaledTVTypography();
const { settings } = useSettings();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const safeInsets = useControlsSafeAreaInsets();
const [info, setInfo] = useState<TechnicalInfo | null>(null); const [info, setInfo] = useState<TechnicalInfo | null>(null);
const opacity = useSharedValue(0); const opacity = useSharedValue(0);
@@ -268,8 +268,14 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
left: Math.max(insets.left, 48) + 20, left: Math.max(insets.left, 48) + 20,
} }
: { : {
top: safeInsets.top + HEADER_LAYOUT.CONTAINER_PADDING + 4, top:
left: safeInsets.left + HEADER_LAYOUT.CONTAINER_PADDING + 20, (settings?.safeAreaInControlsEnabled ?? true)
? insets.top + HEADER_LAYOUT.CONTAINER_PADDING + 4
: HEADER_LAYOUT.CONTAINER_PADDING + 4,
left:
(settings?.safeAreaInControlsEnabled ?? true)
? insets.left + HEADER_LAYOUT.CONTAINER_PADDING + 20
: HEADER_LAYOUT.CONTAINER_PADDING + 20,
}; };
const textStyle = Platform.isTV const textStyle = Platform.isTV

View File

@@ -56,11 +56,7 @@
"environment": "production", "environment": "production",
"autoIncrement": true, "autoIncrement": true,
"android": { "android": {
"image": "latest", "image": "latest"
"config": "android-production.yml"
},
"ios": {
"config": "ios-production.yml"
} }
}, },
"production-apk": { "production-apk": {
@@ -69,8 +65,7 @@
"autoIncrement": true, "autoIncrement": true,
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest", "image": "latest"
"config": "android-production-apk.yml"
} }
}, },
"production-apk-tv": { "production-apk-tv": {
@@ -79,8 +74,7 @@
"autoIncrement": true, "autoIncrement": true,
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest", "image": "latest"
"config": "android-production-tv.yml"
}, },
"env": { "env": {
"EXPO_TV": "1" "EXPO_TV": "1"
@@ -94,8 +88,7 @@
"EXPO_TV": "1" "EXPO_TV": "1"
}, },
"ios": { "ios": {
"credentialsSource": "local", "credentialsSource": "local"
"config": "ios-production.yml"
} }
} }
}, },

View File

@@ -1,18 +0,0 @@
import {
type EdgeInsets,
useSafeAreaInsets,
} from "react-native-safe-area-context";
import { useSettings } from "@/utils/atoms/settings";
const ZERO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
/**
* Returns safe-area insets to apply to in-player controls, honoring the
* `safeAreaInControlsEnabled` user setting. When the setting is disabled,
* returns zero insets so controls can sit flush against the screen edges.
*/
export const useControlsSafeAreaInsets = (): EdgeInsets => {
const { settings } = useSettings();
const insets = useSafeAreaInsets();
return settings.safeAreaInControlsEnabled ? insets : ZERO_INSETS;
};

View File

@@ -1,4 +1,3 @@
import { File, Paths } from "expo-file-system";
import { useCallback } from "react"; import { useCallback } from "react";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
@@ -13,28 +12,36 @@ const useImageStorage = () => {
} }
}, []); }, []);
/**
* expo-file-system instead of fetch+Blob+FileReader: the latter silently
* resolves to an empty payload under RN's New Architecture.
*/
const image2Base64 = useCallback(async (url?: string | null) => { const image2Base64 = useCallback(async (url?: string | null) => {
if (!url) return null; if (!url) return null;
const tmpFile = new File( let blob: Blob;
Paths.cache,
`img-${Date.now()}-${Math.random().toString(36).slice(2)}.jpg`,
);
try { try {
const downloaded = await File.downloadFileAsync(url, tmpFile, { // Fetch the data from the URL
idempotent: true, const response = await fetch(url);
}); blob = await response.blob();
return await downloaded.base64();
} catch (error) { } catch (error) {
console.warn("Error fetching image:", error); console.warn("Error fetching image:", error);
return null; return null;
} finally {
if (tmpFile.exists) tmpFile.delete();
} }
// Create a FileReader instance
const reader = new FileReader();
// Convert blob to base64
return new Promise<string>((resolve, reject) => {
reader.onloadend = () => {
if (typeof reader.result === "string") {
// Extract the base64 string (remove the data URL prefix)
const base64 = reader.result.split(",")[1];
resolve(base64);
} else {
reject(new Error("Failed to convert image to base64"));
}
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}, []); }, []);
const saveImage = useCallback( const saveImage = useCallback(

View File

@@ -1,22 +0,0 @@
import { useEffect } from "react";
import { useWebSocketContext } from "@/providers/WebSocketProvider";
/**
* While `active` is true, hold a keep-alive token on the global
* WebSocket so it is NOT closed when the app moves to
* background/inactive. Releases automatically when `active` flips
* false or the component unmounts.
*
* Used by the video player while in Picture-in-Picture so SyncPlay
* commands (and any other server pushes) keep flowing while the OS
* thinks the app is backgrounded.
*/
export function useKeepWebSocketAlive(active: boolean): void {
const { acquireKeepAlive } = useWebSocketContext();
useEffect(() => {
if (!active) return;
const release = acquireKeepAlive();
return release;
}, [active, acquireKeepAlive]);
}

View File

@@ -109,35 +109,30 @@ export const usePlaybackManager = ({
staleTime: 0, staleTime: 0,
}); });
/**
* Derive prev/next from the current item's real position in the adjacent
* list rather than from the array length. `getEpisodes({ adjacentTo })` does
* not guarantee a fixed [prev, current, next] shape — at the first/last
* episode it can still return the current item as the first/last entry — so
* length-based indexing wrongly surfaces the current episode as "previous".
*/
const currentIndex = useMemo(
() => adjacentItems?.findIndex((e) => e.Id === item?.Id) ?? -1,
[adjacentItems, item],
);
/** A neighbour is only navigable if it has an actual media file (not a
* "Virtual"/missing episode placeholder, e.g. an absent Special). */
const isNavigable = (episode?: BaseItemDto | null): episode is BaseItemDto =>
!!episode && episode.Id !== item?.Id && episode.LocationType !== "Virtual";
const previousItem = useMemo(() => { const previousItem = useMemo(() => {
if (!adjacentItems || currentIndex <= 0) return null; if (!adjacentItems || adjacentItems.length <= 1) {
const candidate = adjacentItems[currentIndex - 1]; return null;
return isNavigable(candidate) ? candidate : null; }
}, [adjacentItems, currentIndex, item]);
if (adjacentItems.length === 2) {
return adjacentItems[0].Id === item?.Id ? null : adjacentItems[0];
}
return adjacentItems[0];
}, [adjacentItems, item]);
/** The next item in the series */ /** The next item in the series */
const nextItem = useMemo(() => { const nextItem = useMemo(() => {
if (!adjacentItems || currentIndex < 0) return null; if (!adjacentItems || adjacentItems.length <= 1) {
const candidate = adjacentItems[currentIndex + 1]; return null;
return isNavigable(candidate) ? candidate : null; }
}, [adjacentItems, currentIndex, item]);
if (adjacentItems.length === 2) {
return adjacentItems[1].Id === item?.Id ? null : adjacentItems[1];
}
return adjacentItems[2];
}, [adjacentItems, item]);
/** /**
* Reports playback progress. * Reports playback progress.

View File

@@ -2,7 +2,6 @@ import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Alert } from "react-native"; import { Alert } from "react-native";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useSyncPlay } from "@/providers/SyncPlay/SyncPlayProvider";
import { useWebSocketContext } from "@/providers/WebSocketProvider"; import { useWebSocketContext } from "@/providers/WebSocketProvider";
interface UseWebSocketProps { interface UseWebSocketProps {
@@ -81,9 +80,9 @@ export const useWebSocket = ({
playTrailers, playTrailers,
}: UseWebSocketProps) => { }: UseWebSocketProps) => {
const router = useRouter(); const router = useRouter();
const { lastMessage, clearLastMessage } = useWebSocketContext(); const { lastMessage } = useWebSocketContext();
const { t } = useTranslation(); const { t } = useTranslation();
const { isEnabled: isSyncPlayEnabled } = useSyncPlay(); const { clearLastMessage } = useWebSocketContext();
useEffect(() => { useEffect(() => {
if (!lastMessage) return; if (!lastMessage) return;
@@ -97,25 +96,6 @@ export const useWebSocket = ({
| Record<string, string> | Record<string, string>
| undefined; // Arguments are Dictionary<string, string> | undefined; // Arguments are Dictionary<string, string>
// Skip playback commands when SyncPlay is enabled - SyncPlay handles these
const isSyncPlayCommand =
lastMessage.MessageType === "SyncPlayCommand" ||
lastMessage.MessageType === "SyncPlayGroupUpdate";
const isPlaybackCommand = [
"PlayPause",
"Pause",
"Unpause",
"Stop",
"Seek",
"NextTrack",
"PreviousTrack",
].includes(command ?? "");
if (isSyncPlayEnabled && (isSyncPlayCommand || isPlaybackCommand)) {
console.log(`Command ~ ${command} - skipping, SyncPlay handles playback`);
return;
}
if (command === "PlayPause") { if (command === "PlayPause") {
console.log("Command ~ PlayPause"); console.log("Command ~ PlayPause");
togglePlay(); togglePlay();

View File

@@ -1,20 +1,46 @@
apply plugin: 'expo-module-gradle-plugin' plugins {
id 'com.android.library'
id 'kotlin-android'
}
group = 'expo.modules.backgrounddownloader' group = 'expo.modules.backgrounddownloader'
version = '1.0.0' version = '1.0.0'
expoModule { def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
canBePublished false def kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
}
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useDefaultAndroidSdkVersions()
useCoreDependencies()
useExpoPublishing()
android { android {
namespace "expo.modules.backgrounddownloader" namespace "expo.modules.backgrounddownloader"
defaultConfig {
versionCode 1 compileOptions {
versionName "1.0.0" sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
lintOptions {
abortOnError false
} }
} }
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "com.squareup.okhttp3:okhttp:4.12.0"
} }
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = "17"
}
}

View File

@@ -50,13 +50,6 @@ class MpvPlayerModule : Module() {
// No-op on Android - media session integration would require MediaSessionCompat // No-op on Android - media session integration would require MediaSessionCompat
} }
// When true, PiP play/pause/skip controls emit JS events
// instead of driving MPV directly, so the host app can route
// through SyncPlay (server -> group broadcast -> all clients).
Prop("syncPlayDelegated") { view: MpvPlayerView, delegated: Boolean ->
view.syncPlayDelegated = delegated
}
// Async function to play video // Async function to play video
AsyncFunction("play") { view: MpvPlayerView -> AsyncFunction("play") { view: MpvPlayerView ->
view.play() view.play()
@@ -205,7 +198,7 @@ class MpvPlayerModule : Module() {
} }
// Defines events that the view can send to JavaScript // Defines events that the view can send to JavaScript
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange", "onPipPlayRequest", "onPipPauseRequest", "onPipSkipRequest") Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange")
} }
} }
} }

View File

@@ -47,16 +47,6 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
val onError by EventDispatcher() val onError by EventDispatcher()
val onTracksReady by EventDispatcher() val onTracksReady by EventDispatcher()
val onPictureInPictureChange by EventDispatcher() val onPictureInPictureChange by EventDispatcher()
// SyncPlay: when `syncPlayDelegated == true`, PiP playback controls
// (play / pause / skip) emit these events instead of driving MPV
// directly, so JS can route the action through the SyncPlay
// controller (server -> group broadcast -> all clients). Default
// behavior (non-SyncPlay) is unchanged.
val onPipPlayRequest by EventDispatcher()
val onPipPauseRequest by EventDispatcher()
val onPipSkipRequest by EventDispatcher()
var syncPlayDelegated: Boolean = false
private var textureView: TextureView private var textureView: TextureView
private var renderer: MPVLayerRenderer? = null private var renderer: MPVLayerRenderer? = null
@@ -95,32 +85,14 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
pipController?.setPlayerView(textureView) pipController?.setPlayerView(textureView)
pipController?.delegate = object : PiPController.Delegate { pipController?.delegate = object : PiPController.Delegate {
override fun onPlay() { override fun onPlay() {
if (syncPlayDelegated) {
onPipPlayRequest(mapOf<String, Any>())
return
}
play() play()
} }
override fun onPause() { override fun onPause() {
if (syncPlayDelegated) {
onPipPauseRequest(mapOf<String, Any>())
return
}
pause() pause()
} }
override fun onSeekBy(seconds: Double) { override fun onSeekBy(seconds: Double) {
if (syncPlayDelegated) {
val target = (cachedPosition + seconds).coerceAtLeast(0.0)
onPipSkipRequest(
mapOf(
"targetSeconds" to target,
"intervalSeconds" to seconds
)
)
return
}
seekBy(seconds) seekBy(seconds)
} }

View File

@@ -65,13 +65,6 @@ public class MpvPlayerModule: Module {
} }
} }
// When true, PiP play/pause/skip controls emit JS events instead
// of driving MPV directly, so the host app can route through
// SyncPlay (server -> group broadcast -> all clients).
Prop("syncPlayDelegated") { (view: MpvPlayerView, delegated: Bool) in
view.syncPlayDelegated = delegated
}
// Async function to play video // Async function to play video
AsyncFunction("play") { (view: MpvPlayerView) in AsyncFunction("play") { (view: MpvPlayerView) in
view.play() view.play()
@@ -220,7 +213,7 @@ public class MpvPlayerModule: Module {
} }
// Defines events that the view can send to JavaScript // Defines events that the view can send to JavaScript
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange", "onPipPlayRequest", "onPipPauseRequest", "onPipSkipRequest") Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady")
} }
} }
} }

View File

@@ -61,17 +61,6 @@ class MpvPlayerView: ExpoView {
let onProgress = EventDispatcher() let onProgress = EventDispatcher()
let onError = EventDispatcher() let onError = EventDispatcher()
let onTracksReady = EventDispatcher() let onTracksReady = EventDispatcher()
let onPictureInPictureChange = EventDispatcher()
// SyncPlay: when `syncPlayDelegated == true`, PiP playback controls
// (play / pause / skip) emit these events instead of driving MPV
// directly, so JS can route the action through the SyncPlay
// controller (server group broadcast all clients). Default
// behavior (non-SyncPlay) is unchanged.
let onPipPlayRequest = EventDispatcher()
let onPipPauseRequest = EventDispatcher()
let onPipSkipRequest = EventDispatcher()
var syncPlayDelegated: Bool = false
private var currentURL: URL? private var currentURL: URL?
private var cachedPosition: Double = 0 private var cachedPosition: Double = 0
@@ -92,6 +81,7 @@ class MpvPlayerView: ExpoView {
private func setupView() { private func setupView() {
clipsToBounds = true clipsToBounds = true
backgroundColor = .black backgroundColor = .black
configureAudioSession()
videoContainer = UIView() videoContainer = UIView()
videoContainer.translatesAutoresizingMaskIntoConstraints = false videoContainer.translatesAutoresizingMaskIntoConstraints = false
@@ -151,26 +141,21 @@ class MpvPlayerView: ExpoView {
CATransaction.commit() CATransaction.commit()
} }
// MARK: - Audio Session & Notifications
private func configureAudioSession() { private func configureAudioSession() {
let session = AVAudioSession.sharedInstance() let audioSession = AVAudioSession.sharedInstance()
do { do {
try session.setCategory(.playback, mode: .moviePlayback, policy: .longFormAudio, options: []) try audioSession.setCategory(
try session.setActive(true) .playback,
mode: .moviePlayback,
policy: .longFormAudio,
options: []
)
try audioSession.setActive(true)
} catch { } catch {
print("Failed to configure audio session: \(error)") print("Failed to configure audio session: \(error)")
} }
} }
// MARK: - Audio Session & Notifications
/// Deactivate the session AND reset the category `setActive(false)` alone
/// leaves `.playback`/`.longFormAudio` on the shared singleton, so any later
/// reactivation (foreground, route change, other modules) re-steals audio.
private func tearDownAudioSession() {
let session = AVAudioSession.sharedInstance()
try? session.setActive(false, options: .notifyOthersOnDeactivation)
try? session.setCategory(.ambient, mode: .default, options: [.mixWithOthers])
}
private func setupNotifications() { private func setupNotifications() {
// Handle audio session interruptions (e.g., incoming calls, other apps playing audio) // Handle audio session interruptions (e.g., incoming calls, other apps playing audio)
@@ -285,7 +270,6 @@ class MpvPlayerView: ExpoView {
func play() { func play() {
intendedPlayState = true intendedPlayState = true
configureAudioSession()
setupRemoteCommands() setupRemoteCommands()
renderer?.play() renderer?.play()
pipController?.setPlaybackRate(1.0) pipController?.setPlaybackRate(1.0)
@@ -456,7 +440,6 @@ class MpvPlayerView: ExpoView {
renderer?.stop() renderer?.stop()
displayLayer.removeFromSuperlayer() displayLayer.removeFromSuperlayer()
clearNowPlayingInfo() clearNowPlayingInfo()
tearDownAudioSession()
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
} }
@@ -536,7 +519,9 @@ extension MpvPlayerView: MPVLayerRendererDelegate {
} }
func renderer(_: MPVLayerRenderer, didSelectAudioOutput audioOutput: String) { func renderer(_: MPVLayerRenderer, didSelectAudioOutput audioOutput: String) {
print("[MPV] Audio output ready (\(audioOutput)), syncing Now Playing") // Audio output is now active - this is the right time to activate audio session and set Now Playing
print("[MPV] Audio output ready (\(audioOutput)), activating audio session and syncing Now Playing")
nowPlayingManager.activateAudioSession()
syncNowPlaying(isPlaying: !isPaused()) syncNowPlaying(isPlaying: !isPaused())
} }
} }
@@ -648,9 +633,6 @@ extension MpvPlayerView: PiPControllerDelegate {
print("PiP did start: \(didStartPictureInPicture)") print("PiP did start: \(didStartPictureInPicture)")
// Ensure current time is synced when PiP starts // Ensure current time is synced when PiP starts
pipController?.setCurrentTimeFromSeconds(cachedPosition, duration: cachedDuration) pipController?.setCurrentTimeFromSeconds(cachedPosition, duration: cachedDuration)
// Notify JS of the actual PiP active state. `didStartPictureInPicture`
// is `false` when AVKit reports a failure to start, so reflect that.
onPictureInPictureChange(["isActive": didStartPictureInPicture])
} }
func pipController(_ controller: PiPController, willStopPictureInPicture: Bool) { func pipController(_ controller: PiPController, willStopPictureInPicture: Bool) {
@@ -669,9 +651,6 @@ extension MpvPlayerView: PiPControllerDelegate {
if _isZoomedToFill { if _isZoomedToFill {
displayLayer.videoGravity = .resizeAspectFill displayLayer.videoGravity = .resizeAspectFill
} }
// Notify JS that PiP has fully stopped so the controls overlay can
// be re-mounted when the user returns to full screen.
onPictureInPictureChange(["isActive": false])
} }
func pipController(_ controller: PiPController, restoreUserInterfaceForPictureInPictureStop completionHandler: @escaping (Bool) -> Void) { func pipController(_ controller: PiPController, restoreUserInterfaceForPictureInPictureStop completionHandler: @escaping (Bool) -> Void) {
@@ -681,12 +660,6 @@ extension MpvPlayerView: PiPControllerDelegate {
func pipControllerPlay(_ controller: PiPController) { func pipControllerPlay(_ controller: PiPController) {
print("PiP play requested") print("PiP play requested")
if syncPlayDelegated {
// Let JS route through SyncPlay. We deliberately do NOT touch
// MPV here; the WS command coming back will drive playback.
onPipPlayRequest([:])
return
}
intendedPlayState = true intendedPlayState = true
renderer?.play() renderer?.play()
pipController?.setPlaybackRate(1.0) pipController?.setPlaybackRate(1.0)
@@ -694,10 +667,6 @@ extension MpvPlayerView: PiPControllerDelegate {
func pipControllerPause(_ controller: PiPController) { func pipControllerPause(_ controller: PiPController) {
print("PiP pause requested") print("PiP pause requested")
if syncPlayDelegated {
onPipPauseRequest([:])
return
}
intendedPlayState = false intendedPlayState = false
renderer?.pausePlayback() renderer?.pausePlayback()
pipController?.setPlaybackRate(0.0) pipController?.setPlaybackRate(0.0)
@@ -707,16 +676,6 @@ extension MpvPlayerView: PiPControllerDelegate {
let seconds = CMTimeGetSeconds(interval) let seconds = CMTimeGetSeconds(interval)
print("PiP skip by interval: \(seconds)") print("PiP skip by interval: \(seconds)")
let target = max(0, cachedPosition + seconds) let target = max(0, cachedPosition + seconds)
if syncPlayDelegated {
// `targetSeconds` lets JS convert to ticks and call
// syncPlayController.seek(). `intervalSeconds` is also sent
// for telemetry / debug.
onPipSkipRequest([
"targetSeconds": target,
"intervalSeconds": seconds
])
return
}
seekTo(position: target) seekTo(position: target)
} }

View File

@@ -29,21 +29,6 @@ export type OnPictureInPictureChangePayload = {
isActive: boolean; isActive: boolean;
}; };
/**
* Emitted when the user taps a PiP playback control while the view
* was rendered with `syncPlayDelegated`. The host app should route
* the action through the SyncPlay controller instead of acting
* locally.
*/
export type OnPipPlayRequestPayload = Record<string, never>;
export type OnPipPauseRequestPayload = Record<string, never>;
export type OnPipSkipRequestPayload = {
/** Absolute target position the user wants to seek to, in seconds. */
targetSeconds: number;
/** Skip interval requested by the OS (signed seconds). Debug only. */
intervalSeconds: number;
};
export type NowPlayingMetadata = { export type NowPlayingMetadata = {
title?: string; title?: string;
artist?: string; artist?: string;
@@ -99,18 +84,6 @@ export type MpvPlayerViewProps = {
onPictureInPictureChange?: (event: { onPictureInPictureChange?: (event: {
nativeEvent: OnPictureInPictureChangePayload; nativeEvent: OnPictureInPictureChangePayload;
}) => void; }) => void;
/**
* When true, PiP play/pause/skip controls emit the corresponding
* `onPipPlayRequest` / `onPipPauseRequest` / `onPipSkipRequest`
* events instead of driving MPV directly. Used to route PiP control
* actions through SyncPlay.
*/
syncPlayDelegated?: boolean;
onPipPlayRequest?: (event: { nativeEvent: OnPipPlayRequestPayload }) => void;
onPipPauseRequest?: (event: {
nativeEvent: OnPipPauseRequestPayload;
}) => void;
onPipSkipRequest?: (event: { nativeEvent: OnPipSkipRequestPayload }) => void;
}; };
export interface MpvPlayerViewRef { export interface MpvPlayerViewRef {

View File

@@ -1,13 +1,11 @@
import { useTranslation } from "react-i18next";
import { MpvPlayerViewProps } from "./MpvPlayer.types"; import { MpvPlayerViewProps } from "./MpvPlayer.types";
export default function MpvPlayerView(props: MpvPlayerViewProps) { export default function MpvPlayerView(props: MpvPlayerViewProps) {
const url = props.source?.url ?? ""; const url = props.source?.url ?? "";
const { t } = useTranslation();
return ( return (
<div> <div>
<iframe <iframe
title={t("player.mpv_player_title")} title='MPV Player'
style={{ flex: 1 }} style={{ flex: 1 }}
src={url} src={url}
onLoad={() => props.onLoad?.({ nativeEvent: { url } })} onLoad={() => props.onLoad?.({ nativeEvent: { url } })}

View File

@@ -4,16 +4,9 @@ const { withEntitlementsPlist } = require("expo/config-plugins");
* Expo config plugin to add User Management entitlement for tvOS profile linking * Expo config plugin to add User Management entitlement for tvOS profile linking
*/ */
const withTVUserManagement = (config) => { const withTVUserManagement = (config) => {
// Only add for tvOS builds. The entitlement is restricted by Apple and must
// be present in the provisioning profile, so injecting it into mobile builds
// breaks signing ("Entitlement ... not found and could not be included in
// profile"). The entitlement is only needed for tvOS
// TVUserManager.currentUserIdentifier.
if (process.env.EXPO_TV !== "1") {
return config;
}
return withEntitlementsPlist(config, (config) => { return withEntitlementsPlist(config, (config) => {
// Only add for tvOS builds (check if building for TV)
// The entitlement is needed for TVUserManager.currentUserIdentifier to work
config.modResults["com.apple.developer.user-management"] = [ config.modResults["com.apple.developer.user-management"] = [
"runs-as-current-user", "runs-as-current-user",
]; ];

View File

@@ -4,68 +4,28 @@ import type { DownloadedItem, DownloadsDatabase } from "./types";
const DOWNLOADS_DATABASE_KEY = "downloads.v2.json"; const DOWNLOADS_DATABASE_KEY = "downloads.v2.json";
// Performance optimization: Cache the parsed database to avoid repeated JSON.parse calls
let cachedDb: DownloadsDatabase | null = null;
let cacheVersion = 0;
// Performance optimization: Cache the flattened items array
let cachedItems: DownloadedItem[] | null = null;
let itemsCacheVersion = -1;
// Performance optimization: Index for O(1) item lookups by ID
let itemIndex: Map<string, DownloadedItem> | null = null;
let indexCacheVersion = -1;
/** /**
* Get the downloads database from storage * Get the downloads database from storage
* PERFORMANCE: Caches the parsed database to avoid repeated JSON.parse calls.
* NOTE: Returns the shared cached instance — do NOT mutate it directly. Go
* through addDownloadedItem/updateDownloadedItem/removeDownloadedItem so
* saveDownloadsDatabase() runs and the derived caches stay consistent.
*/ */
export function getDownloadsDatabase(): DownloadsDatabase { export function getDownloadsDatabase(): DownloadsDatabase {
// Return cached database if available
if (cachedDb !== null) {
return cachedDb;
}
// Parse from storage and cache the result
const file = storage.getString(DOWNLOADS_DATABASE_KEY); const file = storage.getString(DOWNLOADS_DATABASE_KEY);
if (file) { if (file) {
cachedDb = JSON.parse(file) as DownloadsDatabase; return JSON.parse(file) as DownloadsDatabase;
return cachedDb;
} }
return { movies: {}, series: {}, other: {} };
const emptyDb = { movies: {}, series: {}, other: {} };
cachedDb = emptyDb;
return emptyDb;
} }
/** /**
* Save the downloads database to storage * Save the downloads database to storage
* PERFORMANCE: Updates cache and invalidates derived caches
*/ */
export function saveDownloadsDatabase(db: DownloadsDatabase): void { export function saveDownloadsDatabase(db: DownloadsDatabase): void {
storage.set(DOWNLOADS_DATABASE_KEY, JSON.stringify(db)); storage.set(DOWNLOADS_DATABASE_KEY, JSON.stringify(db));
// Update the cache with the new database
cachedDb = db;
// Invalidate derived caches (items array and index)
cachedItems = null;
itemIndex = null;
cacheVersion++;
} }
/** /**
* Get all downloaded items as a flat array * Get all downloaded items as a flat array
* PERFORMANCE: Caches the flattened array to avoid rebuilding on every call
*/ */
export function getAllDownloadedItems(): DownloadedItem[] { export function getAllDownloadedItems(): DownloadedItem[] {
// Return cached items if available and up-to-date
if (cachedItems !== null && itemsCacheVersion === cacheVersion) {
return cachedItems;
}
// Build the items array from the database
const db = getDownloadsDatabase(); const db = getDownloadsDatabase();
const items: DownloadedItem[] = []; const items: DownloadedItem[] = [];
@@ -87,41 +47,34 @@ export function getAllDownloadedItems(): DownloadedItem[] {
} }
} }
// Cache the result
cachedItems = items;
itemsCacheVersion = cacheVersion;
return items; return items;
} }
/** /**
* Build or refresh the item index for O(1) lookups * Get a downloaded item by its ID
*/ */
function ensureItemIndex(): void { export function getDownloadedItemById(id: string): DownloadedItem | undefined {
if (itemIndex !== null && indexCacheVersion === cacheVersion) { const db = getDownloadsDatabase();
return; // Index is up-to-date
if (db.movies[id]) {
return db.movies[id];
} }
// Build new index from all items for (const series of Object.values(db.series)) {
itemIndex = new Map<string, DownloadedItem>(); for (const season of Object.values(series.seasons)) {
const items = getAllDownloadedItems(); for (const episode of Object.values(season.episodes)) {
if (episode.item.Id === id) {
for (const item of items) { return episode;
if (item.item.Id) { }
itemIndex.set(item.item.Id, item); }
} }
} }
indexCacheVersion = cacheVersion; if (db.other?.[id]) {
} return db.other[id];
}
/** return undefined;
* Get a downloaded item by its ID
* PERFORMANCE: Uses O(1) index lookup instead of O(n²) iteration
*/
export function getDownloadedItemById(id: string): DownloadedItem | undefined {
ensureItemIndex();
return itemIndex!.get(id);
} }
/** /**
@@ -268,5 +221,4 @@ export function updateDownloadedItem(
*/ */
export function clearAllDownloadedItems(): void { export function clearAllDownloadedItems(): void {
saveDownloadsDatabase({ movies: {}, series: {}, other: {} }); saveDownloadsDatabase({ movies: {}, series: {}, other: {} });
// saveDownloadsDatabase already invalidates caches
} }

View File

@@ -619,54 +619,44 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setUser(storedUser); setUser(storedUser);
} }
// Dismiss splash screen with cached data immediately, const response = await getUserApi(apiInstance).getCurrentUser();
// fetch fresh user data in the background setUser(response.data);
setInitialLoaded(true);
try { // Migrate current session to secure storage if not already saved
const response = await getUserApi(apiInstance).getCurrentUser(); if (storedUser?.Id && storedUser?.Name) {
setUser(response.data); const existingCredential = await getAccountCredential(
serverUrl,
// Migrate current session to secure storage if not already saved storedUser.Id,
if (storedUser?.Id && storedUser?.Name) { );
const existingCredential = await getAccountCredential( if (!existingCredential) {
await saveAccountCredential({
serverUrl, serverUrl,
storedUser.Id, serverName: "",
); token,
if (!existingCredential) { userId: storedUser.Id,
await saveAccountCredential({ username: storedUser.Name,
serverUrl, savedAt: Date.now(),
serverName: "", securityType: "none",
token, primaryImageTag: response.data.PrimaryImageTag ?? undefined,
userId: storedUser.Id, });
username: storedUser.Name, } else if (
savedAt: Date.now(), response.data.PrimaryImageTag !==
securityType: "none", existingCredential.primaryImageTag
primaryImageTag: response.data.PrimaryImageTag ?? undefined, ) {
}); // Update image tag if it has changed
} else if ( addAccountToServer(serverUrl, existingCredential.serverName, {
response.data.PrimaryImageTag !== userId: existingCredential.userId,
existingCredential.primaryImageTag username: existingCredential.username,
) { securityType: existingCredential.securityType,
// Update image tag if it has changed savedAt: existingCredential.savedAt,
addAccountToServer(serverUrl, existingCredential.serverName, { primaryImageTag: response.data.PrimaryImageTag ?? undefined,
userId: existingCredential.userId, });
username: existingCredential.username,
securityType: existingCredential.securityType,
savedAt: existingCredential.savedAt,
primaryImageTag: response.data.PrimaryImageTag ?? undefined,
});
}
} }
} catch (e) {
// Background fetch failed — app already rendered with cached data
console.warn("Background user fetch failed, using cached data:", e);
} }
} else {
setInitialLoaded(true);
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally {
setInitialLoaded(true); setInitialLoaded(true);
} }
}; };

View File

@@ -1,165 +0,0 @@
/**
* SyncPlay Controller — public playback API exposed to consumers.
*
* Methods are fire-and-forget by design: SyncPlay HTTP responses don't
* carry useful info (the real state arrives via WebSocket broadcast).
* Wrap calls in try/catch so transient network errors don't reach the UI.
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api";
import type { SyncPlayManager } from "./Manager";
import {
getItemsForPlayback,
type TranslateOptions,
translateItemsForPlayback,
} from "./transport/queueTranslation";
export interface PlayOptions extends TranslateOptions {
items?: BaseItemDto[];
ids?: string[];
startIndex?: number;
startPositionTicks?: number;
}
export class Controller {
private manager!: SyncPlayManager;
init(manager: SyncPlayManager): void {
this.manager = manager;
}
/** Toggle play/pause for the whole group. */
playPause(): void {
if (this.manager.isPlaying()) {
this.pause();
} else {
this.unpause();
}
}
/** Resume the group's playback. */
unpause(): void {
this.manager.markPendingPlaybackCommand("Unpause");
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlayUnpause();
} catch (error) {
console.error("SyncPlay Controller.unpause failed", error);
}
}
/** Pause the group's playback. */
pause(): void {
this.manager.markPendingPlaybackCommand("Pause");
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlayPause();
} catch (error) {
console.error("SyncPlay Controller.pause failed", error);
}
// Pause locally too so the user sees instant feedback.
this.manager.getPlayerWrapper().localPause();
}
/** Seek the group's playback. `positionTicks` is in ticks (1ms = 10000 ticks). */
seek(positionTicks: number): void {
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlaySeek({
seekRequestDto: { PositionTicks: positionTicks },
});
} catch (error) {
console.error("SyncPlay Controller.seek failed", error);
}
}
/**
* Start playback in the group. Expands containers (Series, Season,
* BoxSet, Playlist, single Episode w/ autoplay) into the real
* playable queue before broadcasting.
*
* Resolves once the SetNewQueue request completes; the server then
* broadcasts a PlayQueue update and Play command to every member.
*/
async play(options: PlayOptions): Promise<void> {
const api = this.manager.getApiClient();
const sendPlayRequest = async (items: BaseItemDto[]) => {
const queue = items
.map((item) => item.Id)
.filter((id): id is string => typeof id === "string");
await getSyncPlayApi(api).syncPlaySetNewQueue({
playRequestDto: {
PlayingQueue: queue,
PlayingItemPosition: options.startIndex ?? 0,
StartPositionTicks: options.startPositionTicks ?? 0,
},
});
};
try {
const sourceItems = options.items
? options.items
: await getItemsForPlayback(api, options.ids ?? []);
const items = await translateItemsForPlayback(api, sourceItems, options);
await sendPlayRequest(items);
} catch (error) {
console.error("SyncPlay Controller.play failed", error);
throw error;
}
}
/** Stop the group's playback. */
stop(): void {
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlayStop();
} catch (error) {
console.error("SyncPlay Controller.stop failed", error);
}
}
/** Jump to the next item in the group's queue. */
nextItem(): void {
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlayNextItem({
nextItemRequestDto: {
PlaylistItemId: this.manager
.getQueueCore()
.getCurrentPlaylistItemId(),
} as unknown as Parameters<
ReturnType<typeof getSyncPlayApi>["syncPlayNextItem"]
>[0]["nextItemRequestDto"],
});
} catch (error) {
console.error("SyncPlay Controller.nextItem failed", error);
}
}
/** Jump to the previous item in the group's queue. */
previousItem(): void {
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlayPreviousItem({
previousItemRequestDto: {
PlaylistItemId: this.manager
.getQueueCore()
.getCurrentPlaylistItemId(),
} as unknown as Parameters<
ReturnType<typeof getSyncPlayApi>["syncPlayPreviousItem"]
>[0]["previousItemRequestDto"],
});
} catch (error) {
console.error("SyncPlay Controller.previousItem failed", error);
}
}
/** Jump to a specific item in the queue by playlist item id. */
setCurrentPlaylistItem(playlistItemId: string): void {
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlaySetPlaylistItem({
setPlaylistItemRequestDto: { PlaylistItemId: playlistItemId },
});
} catch (error) {
console.error("SyncPlay Controller.setCurrentPlaylistItem failed", error);
}
}
}
export default Controller;

View File

@@ -1,93 +0,0 @@
/**
* Per-instance event emitter — replaces jellyfin-web's global `Events.trigger`
* bus. Listeners that throw are caught and logged so one bad listener can't
* break the rest.
*/
import { WaitForEventDefaultTimeout } from "./constants";
export class EventEmitter {
private listeners: Map<string, Set<(...args: unknown[]) => void>> = new Map();
on(event: string, callback: (...args: unknown[]) => void): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
}
off(event: string, callback: (...args: unknown[]) => void): void {
this.listeners.get(event)?.delete(callback);
}
emit(event: string, ...args: unknown[]): void {
this.listeners.get(event)?.forEach((callback) => {
try {
callback(...args);
} catch (error) {
console.error(
`SyncPlay EventEmitter: handler for "${event}" threw`,
error,
);
}
});
}
removeAllListeners(event?: string): void {
if (event) {
this.listeners.delete(event);
} else {
this.listeners.clear();
}
}
}
/**
* Resolve on the next emission of `event`, or reject after `timeoutMs`
* (or any event in `rejectEventTypes`). Cleans up every listener.
*/
export function waitForEventOnce(
emitter: EventEmitter,
event: string,
timeoutMs: number = WaitForEventDefaultTimeout,
rejectEventTypes?: string[],
): Promise<unknown[]> {
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | null = null;
const clearAll = () => {
emitter.off(event, handler);
if (timer) clearTimeout(timer);
if (Array.isArray(rejectEventTypes)) {
for (const eventName of rejectEventTypes) {
emitter.off(eventName, rejectCallback);
}
}
};
const handler = (...args: unknown[]) => {
clearAll();
resolve(args);
};
const rejectCallback = (...args: unknown[]) => {
clearAll();
reject(args[0] ?? new Error("rejected"));
};
if (timeoutMs) {
timer = setTimeout(() => {
clearAll();
reject(new Error("Timed out."));
}, timeoutMs);
}
emitter.on(event, handler);
if (Array.isArray(rejectEventTypes)) {
for (const eventName of rejectEventTypes) {
emitter.on(eventName, rejectCallback);
}
}
});
}

View File

@@ -1,383 +0,0 @@
/**
* SyncPlayManager — central orchestrator for a SyncPlay session.
*
* Owns the three "cores" (TimeSync, PlaybackCore, QueueCore) and the
* PlayerWrapper, and routes WebSocket events between them.
*
* Lifecycle:
* constructor → init() → (joinGroup → group-state-change "Idle"+) →
* group-state-change "Playing" → group-state-change "Paused" → ...
* → (leaveGroup) → destroy()
*
* Events emitted (provider listens):
* - `group-info-update` `(GroupInfoDto | null)`
* - `group-state-change` `(state: string, oldState: string)`
* - `enabled` `(isEnabled: boolean)`
* - `play-state-change` `(isFollowing: boolean)`
* - `playbackstart` / `playbackerror` — from PlayerWrapper hooks
* - `osd` `(action: SyncPlayOsdAction)`
* - `toast` `(messageKey: string)`
*
* The manager exposes a per-instance `EventEmitter` rather than upstream
* `Events.on(manager, ...)` — replaces the global Events bus pattern.
*/
import type { Api } from "@jellyfin/sdk";
import { Controller } from "./Controller";
import { PlaybackCore } from "./cores/PlaybackCore";
import { QueueCore } from "./cores/QueueCore";
import { TimeSync } from "./cores/TimeSync";
import { EventEmitter } from "./EventEmitter";
import { PendingPlaybackTracker } from "./player/PendingPlaybackTracker";
import { PlayerWrapper } from "./player/PlayerWrapper";
import { reconcileToGroupOnAttach } from "./player/reconcileToGroupOnAttach";
import type {
GroupInfoDto,
GroupUpdate,
PlayerControls,
PlayQueueUpdate,
SendCommand,
} from "./types";
/** Raw WebSocket message data shapes (already unwrapped by the hook). */
export class SyncPlayManager extends EventEmitter {
private apiClient: Api;
private playerWrapper: PlayerWrapper;
private timeSync: TimeSync;
private playbackCore: PlaybackCore;
private queueCore: QueueCore;
private pendingPlaybackTracker: PendingPlaybackTracker;
private controller: Controller;
/** Current group info. `null` when not in a group. */
private groupInfo: GroupInfoDto | null = null;
/** Is SyncPlay actively enabled (i.e., we're in a group)? */
private syncPlayEnabledAtPlayer = false;
/** Are we mirroring the group's commands locally? */
private followingGroupPlayback = true;
constructor(api: Api) {
super();
this.apiClient = api;
this.playerWrapper = new PlayerWrapper();
this.timeSync = new TimeSync(api);
this.playbackCore = new PlaybackCore();
this.queueCore = new QueueCore();
this.pendingPlaybackTracker = new PendingPlaybackTracker();
this.controller = new Controller();
}
/** Wire up cores. Called once after construction. */
init(): void {
this.playbackCore.init(this);
this.queueCore.init(this);
this.controller.init(this);
// Forward PlaybackCore OSD events to provider listeners.
this.playbackCore.on("osd", (...args) => {
this.emit("osd", ...args);
});
// Bridge optimistic pending Pause/Unpause → React state.
this.pendingPlaybackTracker.setChangeHandler((cmd) => {
this.emit("pending-playback-change", cmd);
});
this.timeSync.startPing();
}
/** Public controller for callers. */
getController(): Controller {
return this.controller;
}
/** Called by SyncPlayProvider when the user switches Jellyfin servers. */
updateApiClient(api: Api): void {
this.apiClient = api;
this.timeSync.updateApiClient(api);
}
getApiClient(): Api {
return this.apiClient;
}
getPlayerWrapper(): PlayerWrapper {
return this.playerWrapper;
}
getTimeSync(): TimeSync {
return this.timeSync;
}
getPlaybackCore(): PlaybackCore {
return this.playbackCore;
}
getQueueCore(): QueueCore {
return this.queueCore;
}
getPendingPlaybackTracker(): PendingPlaybackTracker {
return this.pendingPlaybackTracker;
}
// ===========================================================================
// WebSocket message handlers (called by useSyncPlayWebSocket)
// ===========================================================================
/**
* Handle a `SyncPlayGroupUpdate` WebSocket message.
*
* Cast: the SDK's `GroupUpdate.Type` union is narrower than what the
* server actually emits (it omits `SyncPlayIsDisabled`, `GroupUpdate`,
* `CreateGroupDenied`, `JoinGroupDenied`). Wire format is the source
* of truth here.
*/
processGroupUpdate(rawUpdate: GroupUpdate): void {
if (!rawUpdate) {
console.warn("SyncPlay processGroupUpdate: empty update");
return;
}
const update = rawUpdate as unknown as {
Type: string;
Data: unknown;
};
switch (update.Type) {
case "PlayQueue":
this.queueCore.updatePlayQueue(
this.apiClient,
update.Data as unknown as PlayQueueUpdate,
);
break;
case "UserJoined":
case "UserLeft":
// Group membership notifications — current group will follow
// via GroupUpdate, but emit a toast for friendliness.
this.emit("toast", `MessageSyncPlay${update.Type}`, update.Data);
break;
case "GroupJoined": {
this.groupInfo = update.Data as GroupInfoDto;
this.enableSyncPlay(this.groupInfo);
this.emit("group-update", this.groupInfo);
this.emit("toast", "MessageSyncPlayGroupJoined");
break;
}
case "GroupLeft":
case "NotInGroup":
case "SyncPlayIsDisabled": {
const previousState = this.groupInfo?.State;
this.groupInfo = null;
this.disableSyncPlay();
this.emit("group-update", null);
if (update.Type === "GroupLeft") {
this.emit("toast", "MessageSyncPlayGroupLeft");
}
if (previousState) {
this.emit("group-state-change", "Idle", previousState);
}
break;
}
case "GroupUpdate": {
const previousState = this.groupInfo?.State;
this.groupInfo = update.Data as GroupInfoDto;
this.emit("group-update", this.groupInfo);
const newState = this.groupInfo.State;
if (newState && newState !== previousState) {
this.emit("group-state-change", newState, previousState ?? "Idle");
}
break;
}
case "StateUpdate": {
const stateData = update.Data as {
State?: string;
PreviousState?: string;
Reason?: string;
};
const newState = stateData.State ?? "Idle";
const previousState = stateData.PreviousState ?? "Idle";
const reason = stateData.Reason;
if (this.groupInfo) {
this.groupInfo.State = newState as GroupInfoDto["State"];
this.emit("group-update", this.groupInfo);
}
this.emit("group-state-change", newState, previousState, reason);
// Server signals "Playing" or "Paused" → clear any in-flight
// optimistic tap state.
if (newState === "Playing" || newState === "Paused") {
this.pendingPlaybackTracker.clear();
}
break;
}
case "CreateGroupDenied":
this.emit("toast", "MessageSyncPlayCreateGroupDenied");
break;
case "JoinGroupDenied":
this.emit("toast", "MessageSyncPlayJoinGroupDenied");
break;
case "LibraryAccessDenied":
this.emit("toast", "MessageSyncPlayLibraryAccessDenied");
break;
case "GroupDoesNotExist":
this.emit("toast", "MessageSyncPlayGroupDoesNotExist");
break;
default:
console.warn("SyncPlay processGroupUpdate: unknown type", update.Type);
break;
}
}
/** Handle a `SyncPlayCommand` WebSocket message. */
processCommand(command: SendCommand): void {
if (!command) {
console.warn("SyncPlay processCommand: empty command");
return;
}
this.playbackCore.applyCommand(command);
// Server told us the new playing state — clear optimistic UI.
if (command.Command === "Unpause" || command.Command === "Pause") {
this.pendingPlaybackTracker.clear();
}
}
// ===========================================================================
// Enable / disable SyncPlay
// ===========================================================================
private enableSyncPlay(_group: GroupInfoDto): void {
if (this.syncPlayEnabledAtPlayer) return;
this.syncPlayEnabledAtPlayer = true;
this.followingGroupPlayback = true;
this.timeSync.forceUpdate();
this.emit("enabled", true);
this.emit("play-state-change", true);
}
private disableSyncPlay(): void {
if (!this.syncPlayEnabledAtPlayer) return;
this.syncPlayEnabledAtPlayer = false;
this.followingGroupPlayback = false;
this.playbackCore.clearScheduledCommand();
this.queueCore.clear();
this.pendingPlaybackTracker.clear();
this.emit("enabled", false);
this.emit("play-state-change", false);
}
/**
* Resume following group playback after the user temporarily took
* local control (e.g. scrubbed the seek bar).
*/
async followGroupPlayback(_api: Api): Promise<void> {
this.followingGroupPlayback = true;
this.emit("play-state-change", true);
}
/** Stop following group playback (e.g., user takes local control). */
haltGroupPlayback(_api: Api): void {
this.followingGroupPlayback = false;
this.emit("play-state-change", false);
}
isFollowingGroupPlayback(): boolean {
return this.followingGroupPlayback;
}
isSyncPlayEnabled(): boolean {
return this.syncPlayEnabledAtPlayer;
}
// ===========================================================================
// Player attach + provider bridges
// ===========================================================================
/**
* Bind the RN player controls.
* Called from the player screen's `useEffect`. Triggers a reconcile
* if a group is active and the player is late-arriving.
*/
setPlayerControls(controls: PlayerControls | null): void {
this.playerWrapper.bindToControls(controls);
if (controls && this.syncPlayEnabledAtPlayer) {
const lastCommand = this.playbackCore.getLastCommand();
reconcileToGroupOnAttach(controls, lastCommand, (local) =>
this.timeSync.localDateToRemote(local),
);
}
}
/** Player-side notify hook: media is ready to play. */
notifyReady(): void {
this.emit("playbackstart");
if (this.syncPlayEnabledAtPlayer) {
this.playbackCore.onReady(this.apiClient);
}
}
/** Player-side notify hook: buffering state changed. */
notifyBuffering(isBuffering: boolean): void {
if (!this.syncPlayEnabledAtPlayer) return;
if (isBuffering) {
this.playbackCore.onBuffering(this.apiClient);
} else {
this.playbackCore.onReady(this.apiClient);
}
}
/** Player-side notify hook: local playback started. */
notifyPlaybackStart(): void {
this.emit("playbackstart");
if (this.syncPlayEnabledAtPlayer) {
this.playbackCore.onPlaybackStart(this.apiClient);
}
}
// ===========================================================================
// Pending playback (optimistic UI for play/pause taps)
// ===========================================================================
/** Called by Controller before sending an Unpause/Pause request. */
markPendingPlaybackCommand(command: "Unpause" | "Pause"): void {
this.pendingPlaybackTracker.mark(command);
}
/** Is the group currently playing? Used by Controller.playPause. */
isPlaying(): boolean {
const pending = this.pendingPlaybackTracker.get();
if (pending === "Unpause") return true;
if (pending === "Pause") return false;
return this.groupInfo?.State === "Playing";
}
/** Group info for consumers. */
getGroupInfo(): GroupInfoDto | null {
return this.groupInfo;
}
/** Last playback command (for QueueCore.startPlayback resumption). */
getLastPlaybackCommand(): SendCommand | null {
return this.playbackCore.getLastCommand();
}
// ===========================================================================
// Teardown
// ===========================================================================
destroy(): void {
this.timeSync.destroy();
this.playbackCore.destroy();
this.queueCore.destroy();
this.playerWrapper.bindToControls(null);
this.removeAllListeners();
}
}
export default SyncPlayManager;

View File

@@ -1,527 +0,0 @@
/**
* SyncPlayProvider — React glue around `SyncPlayManager`.
*
* Responsibilities:
* - Manager lifecycle (construct on api change, destroy on unmount)
* - React mirrors of manager state (`isEnabled`, `groupInfo`,
* `pendingPlaybackCommand`) so components re-render
* - Navigation handlers wired into `PlayerWrapper.localPlay` /
* `localSetCurrentPlaylistItem` — these are what jellyfin-web does
* synchronously via `playbackManager.play`; on RN they navigate
* to the player screen instead
* - AppState foreground re-join (we may miss broadcasts while
* suspended)
*
* External API surface (`useSyncPlay`) is stable; components don't
* change when the internals do.
*/
import { getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api";
import { useAtomValue } from "jotai";
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { AppState, type AppStateStatus } from "react-native";
import { toast } from "sonner-native";
import { useAppRouter } from "@/hooks/useAppRouter";
import { useKeepWebSocketAlive } from "@/hooks/useKeepWebSocketAlive";
import i18n from "@/i18n";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useWebSocketContext } from "@/providers/WebSocketProvider";
import type { Controller as SyncPlayController } from "./Controller";
import { SyncPlayManager } from "./Manager";
import { useSyncPlayWebSocket } from "./transport/useSyncPlayWebSocket";
import type { GroupInfoDto, PlayerControls, SyncPlayOsdAction } from "./types";
interface SyncPlayContextValue {
isEnabled: boolean;
groupInfo: GroupInfoDto | null;
canJoinGroups: boolean;
canCreateGroups: boolean;
joinGroup: (groupId: string) => Promise<void>;
createGroup: (groupName?: string) => Promise<void>;
leaveGroup: () => Promise<void>;
getGroups: () => Promise<GroupInfoDto[]>;
controller: SyncPlayController | null;
setPlayerControls: (controls: PlayerControls | null) => void;
notifyReady: () => void;
notifyBuffering: (isBuffering: boolean) => void;
notifyPlaybackStart: () => void;
pendingPlaybackCommand: "Unpause" | "Pause" | null;
/**
* Current SyncPlay OSD overlay state. Drives the animated icon over the
* video that mirrors jellyfin-web's `#syncPlayIcon`. `null` means hidden.
*/
osdAction: SyncPlayOsdAction | null;
}
const SyncPlayContext = createContext<SyncPlayContextValue | null>(null);
interface SyncPlayProviderProps {
children: ReactNode;
}
export function SyncPlayProvider({ children }: SyncPlayProviderProps) {
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const router = useAppRouter();
const { isConnected: isWsConnected } = useWebSocketContext();
const [manager, setManager] = useState<SyncPlayManager | null>(null);
const isNavigatingToPlayerRef = useRef(false);
const [isEnabled, setIsEnabled] = useState(false);
const [groupInfo, setGroupInfo] = useState<GroupInfoDto | null>(null);
const [pendingPlaybackCommand, setPendingPlaybackCommand] = useState<
"Unpause" | "Pause" | null
>(null);
// While in a SyncPlay group, hold a keep-alive token on the global
// WebSocket so backgrounding the app does NOT cleanly close the
// socket. A clean close is interpreted by the Jellyfin server as
// leaving the group and is broadcast to every other member as
// "<user> has left the group". Keeping the socket open across a
// short suspend lets us stay in the group while quickly switching
// apps; if the OS eventually tears the TCP connection down anyway,
// the app-foreground rejoin effect below will pull us back in.
useKeepWebSocketAlive(isEnabled);
const [osdAction, setOsdAction] = useState<SyncPlayOsdAction | null>(null);
const osdTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
/**
* Set the OSD overlay action.
*
* `transient` mirrors jellyfin-web's `iconVisibilityTime = 1500` for the
* Unpause / Pause / Seek command-confirmation flashes. Persistent actions
* (schedule-play, buffering, wait-*) stay until cleared by a state
* transition or a subsequent call with `null`.
*/
const showOsd = useCallback(
(action: SyncPlayOsdAction | null, transient = false) => {
if (osdTimeoutRef.current) {
clearTimeout(osdTimeoutRef.current);
osdTimeoutRef.current = null;
}
setOsdAction(action);
if (transient && action !== null) {
osdTimeoutRef.current = setTimeout(() => {
osdTimeoutRef.current = null;
setOsdAction((cur) => (cur === action ? null : cur));
}, 1500);
}
},
[],
);
// Pending play/pause tap → optimistic schedule-play overlay (unless another
// overlay reason has already taken precedence).
useEffect(() => {
if (pendingPlaybackCommand) {
setOsdAction((cur) => cur ?? "schedule-play");
} else {
setOsdAction((cur) => (cur === "schedule-play" ? null : cur));
}
}, [pendingPlaybackCommand]);
// Clear the OSD auto-expire timeout on unmount.
useEffect(() => {
return () => {
if (osdTimeoutRef.current) {
clearTimeout(osdTimeoutRef.current);
osdTimeoutRef.current = null;
}
};
}, []);
const canJoinGroups = useMemo(() => {
const access = user?.Policy?.SyncPlayAccess;
return access !== "None" && access !== undefined;
}, [user?.Policy?.SyncPlayAccess]);
const canCreateGroups = useMemo(
() => user?.Policy?.SyncPlayAccess === "CreateAndJoinGroups",
[user?.Policy?.SyncPlayAccess],
);
// Latch: `true` once we've fired the per-attach `playbackstart` event.
const playbackStartFiredRef = useRef(false);
// ---------------------------------------------------------------------------
// Manager lifecycle
// ---------------------------------------------------------------------------
useEffect(() => {
if (!api) return;
const mgr = new SyncPlayManager(api);
mgr.init();
setManager(mgr);
const playerWrapper = mgr.getPlayerWrapper();
// localPlay → navigate to direct-player with syncPlay=true
playerWrapper.setLocalPlayHandler((options) => {
const itemId = options.ids[0];
if (!itemId) {
console.warn("SyncPlay: localPlay called with no ids");
return;
}
if (isNavigatingToPlayerRef.current) {
console.debug("SyncPlay: already navigating to player");
return;
}
isNavigatingToPlayerRef.current = true;
toast(i18n.t("syncplay.joining_playback"));
const queryParams = new URLSearchParams({
itemId,
playbackPosition: String(options.startPositionTicks ?? 0),
syncPlay: "true",
}).toString();
router.push(`/player/direct-player?${queryParams}`);
setTimeout(() => {
isNavigatingToPlayerRef.current = false;
}, 2000);
});
// localSetCurrentPlaylistItem → navigate to the new playlist item
playerWrapper.setLocalSetCurrentItemHandler((playlistItemId) => {
if (!playlistItemId) return;
const queueCore = mgr.getQueueCore();
const target = queueCore
.getPlaylist()
.find((i) => i.PlaylistItemId === playlistItemId);
const itemId = target?.Id;
if (!itemId) {
console.warn(
"SyncPlay: localSetCurrentPlaylistItem — item not in playlist",
playlistItemId,
);
return;
}
if (isNavigatingToPlayerRef.current) return;
isNavigatingToPlayerRef.current = true;
const queryParams = new URLSearchParams({
itemId,
playbackPosition: String(queueCore.getStartPositionTicks()),
syncPlay: "true",
}).toString();
router.push(`/player/direct-player?${queryParams}`);
setTimeout(() => {
isNavigatingToPlayerRef.current = false;
}, 2000);
});
mgr.on("enabled", (...args: unknown[]) => {
const enabled = args[0] as boolean;
setIsEnabled(enabled);
if (!enabled) setGroupInfo(null);
});
mgr.on("group-update", (...args: unknown[]) => {
setGroupInfo((args[0] as GroupInfoDto | null | undefined) ?? null);
});
mgr.on("pending-playback-change", (...args: unknown[]) => {
setPendingPlaybackCommand(args[0] as "Unpause" | "Pause" | null);
});
// group-state-change → on "Waiting", pause locally so we don't drift
// ahead of the group while the server is reconciling buffering/seek
// state. Position resync is *only* done from the explicit Pause /
// Unpause / Seek SendCommands that follow (`PlaybackCore.applyCommand`
// → `scheduleUnpause` etc.) — those commands carry the canonical
// `PositionTicks` for the action's `When`. The old code here also
// called `wrapper.localSeek(lastCommand.PositionTicks)`, but
// `lastCommand` is the *previous* Pause/Unpause and can be many
// seconds stale, so it rewound the user every time someone else
// buffered. Don't put a seek back here.
mgr.on("group-state-change", (...args: unknown[]) => {
const state = args[0] as string | undefined;
const reason = args[2] as string | undefined;
const wrapper = mgr.getPlayerWrapper();
if (!wrapper.isPlaybackActive()) return;
if (state === "Waiting") {
wrapper.localPause();
}
// Drive the persistent OSD overlay from (state, reason).
// Mirrors jellyfin-web's `group-state-update` → `showIcon` mapping.
if (state === "Waiting") {
if (reason === "Buffer") showOsd("buffering");
else if (reason === "Unpause") showOsd("wait-unpause");
else if (reason === "Pause") showOsd("wait-pause");
else if (reason === "Seek") showOsd("seek");
} else if (state === "Playing" || state === "Paused") {
// Stable state — clear any persistent overlay; transient flashes
// come from the `osd` event below and self-expire.
setOsdAction((cur) => {
if (
cur === "schedule-play" ||
cur === "buffering" ||
cur === "wait-pause" ||
cur === "wait-unpause"
) {
return null;
}
return cur;
});
}
});
// PlaybackCore-emitted OSD events. Transient ones auto-expire in 1.5s.
mgr.on("osd", (...args: unknown[]) => {
const action = args[0] as SyncPlayOsdAction;
const transient =
action === "unpause" || action === "pause" || action === "seek";
showOsd(action, transient);
});
mgr.on("toast", (...args: unknown[]) => {
const key = args[0] as string;
const arg = args[1] as string | undefined;
const message = arg
? i18n.t(`syncplay.toasts.${key}`, { user: arg })
: i18n.t(`syncplay.toasts.${key}`);
toast(message);
});
return () => {
mgr.destroy();
setManager(null);
};
}, [api, router]);
// Initial join race: once `enabled` flips true, snapshot the current group.
useEffect(() => {
if (isEnabled && manager) {
setGroupInfo(manager.getGroupInfo());
}
}, [isEnabled, manager]);
// Wire WebSocket messages → manager
useSyncPlayWebSocket(manager);
// ---------------------------------------------------------------------------
// Group management
// ---------------------------------------------------------------------------
const getGroups = useCallback(async (): Promise<GroupInfoDto[]> => {
if (!api) return [];
try {
const response = await getSyncPlayApi(api).syncPlayGetGroups();
return (response.data as unknown as GroupInfoDto[]) ?? [];
} catch (error) {
console.error("SyncPlay: failed to get groups", error);
return [];
}
}, [api]);
const joinGroup = useCallback(
async (groupId: string): Promise<void> => {
if (!api) return;
try {
await getSyncPlayApi(api).syncPlayJoinGroup({
joinGroupRequestDto: { GroupId: groupId },
});
} catch (error) {
console.error("SyncPlay: failed to join group", error);
throw error;
}
},
[api],
);
const createGroup = useCallback(
async (groupName?: string): Promise<void> => {
if (!api || !user) return;
const name = groupName || `${user.Name}'s Group`;
try {
await getSyncPlayApi(api).syncPlayCreateGroup({
newGroupRequestDto: { GroupName: name },
});
} catch (error) {
console.error("SyncPlay: failed to create group", error);
throw error;
}
},
[api, user],
);
const leaveGroup = useCallback(async (): Promise<void> => {
if (!api) return;
try {
await getSyncPlayApi(api).syncPlayLeaveGroup();
} catch (error) {
console.error("SyncPlay: failed to leave group", error);
throw error;
}
}, [api]);
// ---------------------------------------------------------------------------
// App foreground re-join (idempotent; gets us a fresh GroupJoined snapshot)
// ---------------------------------------------------------------------------
const lastGroupIdRef = useRef<string | null>(null);
useEffect(() => {
lastGroupIdRef.current = groupInfo?.GroupId ?? null;
}, [groupInfo?.GroupId]);
// Track whether the WebSocket got torn down while the app was
// backgrounded. If it survived (keep-alive worked), the server
// still has us in the group and we must NOT call JoinGroup again —
// doing so would trigger a redundant "X joined the group" broadcast
// to every other member every time we briefly leave the app.
const wsClosedWhileBackgroundedRef = useRef(false);
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
useEffect(() => {
if (!isWsConnected && appStateRef.current !== "active") {
wsClosedWhileBackgroundedRef.current = true;
}
}, [isWsConnected]);
useEffect(() => {
if (!api) return;
const subscription = AppState.addEventListener("change", (nextAppState) => {
const previousAppState = appStateRef.current;
appStateRef.current = nextAppState;
const becameActive =
(previousAppState === "background" ||
previousAppState === "inactive") &&
nextAppState === "active";
if (!becameActive) return;
const groupId = lastGroupIdRef.current;
if (!groupId) return;
// Happy path: keep-alive held the socket open across the
// suspend. Server still considers us a member — nothing to do.
if (!wsClosedWhileBackgroundedRef.current) {
console.log(
"SyncPlay: app foregrounded with WS still alive, skipping rejoin",
);
return;
}
wsClosedWhileBackgroundedRef.current = false;
// Small delay so the WebSocket has a moment to reconnect.
setTimeout(() => {
console.log(
`SyncPlay: app foregrounded after WS drop, rejoining group ${groupId}`,
);
getSyncPlayApi(api)
.syncPlayJoinGroup({ joinGroupRequestDto: { GroupId: groupId } })
.catch((error) => {
console.error("SyncPlay: failed to rejoin group", error);
});
}, 1000);
});
return () => subscription.remove();
}, [api]);
// ---------------------------------------------------------------------------
// Player attach bridges
// ---------------------------------------------------------------------------
const setPlayerControls = useCallback(
(controls: PlayerControls | null) => {
// Reset the playbackstart latch on each new attach.
playbackStartFiredRef.current = false;
manager?.setPlayerControls(controls);
},
[manager],
);
const notifyReady = useCallback(() => {
manager?.notifyReady();
}, [manager]);
const notifyBuffering = useCallback(
(isBuffering: boolean) => {
manager?.notifyBuffering(isBuffering);
if (!isBuffering && !playbackStartFiredRef.current) {
playbackStartFiredRef.current = true;
manager?.notifyPlaybackStart();
}
},
[manager],
);
const notifyPlaybackStart = useCallback(() => {
manager?.notifyPlaybackStart();
}, [manager]);
// ---------------------------------------------------------------------------
// Context value
// ---------------------------------------------------------------------------
const contextValue: SyncPlayContextValue = useMemo(
() => ({
isEnabled,
groupInfo,
canJoinGroups,
canCreateGroups,
joinGroup,
createGroup,
leaveGroup,
getGroups,
controller: manager?.getController() ?? null,
setPlayerControls,
notifyReady,
notifyBuffering,
notifyPlaybackStart,
pendingPlaybackCommand,
osdAction,
}),
[
isEnabled,
groupInfo,
canJoinGroups,
canCreateGroups,
joinGroup,
createGroup,
leaveGroup,
getGroups,
manager,
setPlayerControls,
notifyReady,
notifyBuffering,
notifyPlaybackStart,
pendingPlaybackCommand,
osdAction,
],
);
return (
<SyncPlayContext.Provider value={contextValue}>
{children}
</SyncPlayContext.Provider>
);
}
export function useSyncPlay(): SyncPlayContextValue {
const context = useContext(SyncPlayContext);
if (!context) {
throw new Error("useSyncPlay must be used within a SyncPlayProvider");
}
return context;
}

View File

@@ -1,23 +0,0 @@
/**
* Constants — shared timing/threshold values used across SyncPlay files.
* Kept separate from `types.ts` because these are implementation tuning
* values, not the public protocol/types surface.
*/
import { TicksPerMillisecond } from "./types";
export { TicksPerMillisecond };
/** Default timeout for `waitForEventOnce` (matches jellyfin-web). */
export const WaitForEventDefaultTimeout = 30000;
/** Short-lived timeout for player events (matches jellyfin-web). */
export const WaitForPlayerEventTimeout = 500;
export function ticksToMs(ticks: number): number {
return ticks / TicksPerMillisecond;
}
export function msToTicks(ms: number): number {
return Math.round(ms * TicksPerMillisecond);
}

View File

@@ -1,378 +0,0 @@
/**
* SyncPlay PlaybackCore — schedules unpauses/pauses/seeks/stops to fire
* at the precise group-wide moment and keeps the player drift-corrected.
*
* Design choices that diverge from jellyfin-web:
* - **No SpeedToSync**. Our RN players' `setPlaybackSpeed` is unreliable
* across platforms (mpv/VLC/expo-video each behave differently for
* fractional speeds). We always seek to catch up.
* - **No `MessageSyncPlayDuplicateMedia` detection**. Web's detection
* used HTML element identity; on RN we don't have a stable handle
* and the false-positive rate would be much higher than the value.
* - **No syncMethod / showSyncIcon**. We don't surface the sync
* technique to the UI.
*/
import type { Api } from "@jellyfin/sdk";
import { getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api";
import {
TicksPerMillisecond,
ticksToMs,
WaitForPlayerEventTimeout,
} from "../constants";
import { EventEmitter, waitForEventOnce } from "../EventEmitter";
import type { SyncPlayManager } from "../Manager";
import { type SendCommand, SYNC_PLAY_TUNING } from "../types";
export class PlaybackCore extends EventEmitter {
private manager!: SyncPlayManager;
private lastCommand: SendCommand | null = null;
private scheduledCommand: ReturnType<typeof setTimeout> | null = null;
init(manager: SyncPlayManager): void {
this.manager = manager;
}
/** Local "playback started" hook — fires the initial Ready request. */
onPlaybackStart(apiClient: Api): void {
try {
const playerWrapper = this.manager.getPlayerWrapper();
const positionMs = playerWrapper.currentTime();
const positionTicks = Math.round(positionMs * TicksPerMillisecond);
const isPlaying = playerWrapper.isPlaying();
const playlistItemId =
this.manager.getQueueCore().getCurrentPlaylistItemId() ?? undefined;
const now = this.manager.getTimeSync().localDateToRemote(new Date());
getSyncPlayApi(apiClient).syncPlayReady({
readyRequestDto: {
When: now.toISOString(),
PositionTicks: positionTicks,
IsPlaying: isPlaying,
PlaylistItemId: playlistItemId,
},
});
} catch (error) {
console.error("SyncPlay onPlaybackStart:", error);
}
}
/** Local pause → tell the server. */
onPause(apiClient: Api): void {
try {
getSyncPlayApi(apiClient).syncPlayPause();
} catch (error) {
console.error("SyncPlay onPause:", error);
}
}
/** Local unpause → tell the server. */
onUnpause(apiClient: Api): void {
try {
getSyncPlayApi(apiClient).syncPlayUnpause();
} catch (error) {
console.error("SyncPlay onUnpause:", error);
}
}
/** Local "ready" hook — server uses this to know we've finished buffering. */
onReady(apiClient: Api): void {
this.sendBufferingRequest(apiClient, false);
}
/** Local "buffering" hook — server uses this to (optionally) pause the group. */
onBuffering(apiClient: Api): void {
this.sendBufferingRequest(apiClient, true);
}
/** Send a Ready or Buffering request. */
sendBufferingRequest(apiClient: Api, isBuffering: boolean): void {
const playerWrapper = this.manager.getPlayerWrapper();
const positionMs = playerWrapper.currentTime();
const positionTicks = Math.round(positionMs * TicksPerMillisecond);
const isPlaying = playerWrapper.isPlaying();
const playlistItemId =
this.manager.getQueueCore().getCurrentPlaylistItemId() ?? undefined;
const now = this.manager.getTimeSync().localDateToRemote(new Date());
try {
if (isBuffering) {
getSyncPlayApi(apiClient).syncPlayBuffering({
bufferRequestDto: {
When: now.toISOString(),
PositionTicks: positionTicks,
IsPlaying: isPlaying,
PlaylistItemId: playlistItemId,
},
});
} else {
getSyncPlayApi(apiClient).syncPlayReady({
readyRequestDto: {
When: now.toISOString(),
PositionTicks: positionTicks,
IsPlaying: isPlaying,
PlaylistItemId: playlistItemId,
},
});
}
} catch (error) {
console.error("SyncPlay sendBufferingRequest:", error);
}
}
/**
* Apply a group command (Unpause, Pause, Stop, Seek). Times the
* execution to fire at the group-wide instant the server selected.
*/
applyCommand(command: SendCommand): void {
(command as unknown as { EmittedAt: Date }).EmittedAt = new Date(
command.EmittedAt as unknown as string,
);
(command as unknown as { When: Date }).When = new Date(
command.When as unknown as string,
);
if (
this.lastCommand &&
((
this.lastCommand as unknown as { EmittedAt: Date }
).EmittedAt.getTime() >
(command as unknown as { EmittedAt: Date }).EmittedAt.getTime() ||
(this.lastCommand as unknown as { When: Date }).When.getTime() >
(command as unknown as { When: Date }).When.getTime())
) {
// NOTE: strict `>` (not `>=`) on EmittedAt — Jellyfin's server timestamps
// commands at sub-ms precision but JS `Date` truncates to ms, so two
// commands emitted within the same millisecond would otherwise be
// rejected as "outdated" and silently dropped. This produced an
// unbreakable pause/unpause loop where every fresh command was
// discarded. Matches jellyfin-web's check in
// `web/src/plugins/syncPlay/core/Manager.js`.
console.debug(
"SyncPlay applyCommand: dropping outdated command",
command,
);
return;
}
this.lastCommand = command;
if (!this.manager.isFollowingGroupPlayback()) {
console.debug(
"SyncPlay applyCommand: dropping command (not following playback)",
command,
);
return;
}
const playerWrapper = this.manager.getPlayerWrapper();
if (!playerWrapper.isPlaybackActive()) {
console.debug(
"SyncPlay applyCommand: dropping command (playback not active)",
command,
);
return;
}
const enqueuedAt = new Date();
const remoteEnqueuedAt = this.manager
.getTimeSync()
.localDateToRemote(enqueuedAt);
const localCommandWhen = this.manager
.getTimeSync()
.remoteDateToLocal(command.When as unknown as Date);
switch (command.Command) {
case "Unpause":
this.scheduleUnpause(localCommandWhen, command.PositionTicks ?? 0);
this.emit("osd", "unpause");
break;
case "Pause":
this.schedulePause(localCommandWhen, command.PositionTicks ?? 0);
this.emit("osd", "pause");
break;
case "Stop":
this.scheduleStop(localCommandWhen);
break;
case "Seek":
this.scheduleSeek(localCommandWhen, command.PositionTicks ?? 0);
this.emit("osd", "seek");
break;
default:
console.warn("SyncPlay applyCommand: unknown command", command);
break;
}
if (
(command as unknown as { When: Date }).When.getTime() <
remoteEnqueuedAt.getTime()
) {
console.debug(
"SyncPlay applyCommand: command was scheduled for the past",
command,
);
}
}
/** Pre-Unpause hook: emit a wait OSD while we wait for the moment. */
scheduleUnpause(when: Date, positionTicks: number): void {
this.clearScheduledCommand();
const now = Date.now();
const playAtTime = when.getTime();
const currentPositionMs = this.manager.getPlayerWrapper().currentTime();
const currentPositionTicks = Math.round(
currentPositionMs * TicksPerMillisecond,
);
if (playAtTime > now) {
// Future: seek now, then play at the right moment.
this.localSeek(positionTicks);
this.scheduledCommand = setTimeout(() => {
this.localUnpause();
// After playback resumes, the player position will need a
// small bump to land on the group target. waitForPlayerEvent
// is best-effort.
waitForEventOnce(
this.manager,
"unpause",
WaitForPlayerEventTimeout,
).catch(() => undefined);
}, playAtTime - now);
this.emit("osd", "wait-unpause");
} else {
// Past: catch up now.
const targetMs = ticksToMs(positionTicks);
const delayMs = now - playAtTime;
this.localSeek(Math.round((targetMs + delayMs) * TicksPerMillisecond));
this.localUnpause();
void currentPositionTicks;
}
}
schedulePause(when: Date, positionTicks: number): void {
this.clearScheduledCommand();
const now = Date.now();
const pauseAtTime = when.getTime();
const callback = () => {
this.localUnpause();
this.localSeek(positionTicks);
this.localPause();
};
if (pauseAtTime > now) {
this.scheduledCommand = setTimeout(callback, pauseAtTime - now);
this.emit("osd", "wait-pause");
} else {
callback();
}
}
scheduleStop(when: Date): void {
this.clearScheduledCommand();
const now = Date.now();
const stopAtTime = when.getTime();
if (stopAtTime > now) {
this.scheduledCommand = setTimeout(() => {
this.localStop();
}, stopAtTime - now);
} else {
this.localStop();
}
}
scheduleSeek(when: Date, positionTicks: number): void {
this.applyCommand({
...this.lastCommand!,
Command: "Pause",
PositionTicks: positionTicks,
When: when as unknown as string,
EmittedAt: new Date().toISOString(),
});
}
clearScheduledCommand(): void {
if (this.scheduledCommand) {
clearTimeout(this.scheduledCommand);
this.scheduledCommand = null;
}
}
// -- local player ops ------------------------------------------------------
localUnpause(): void {
this.manager.getPlayerWrapper().localUnpause();
}
localPause(): void {
this.manager.getPlayerWrapper().localPause();
}
localSeek(positionTicks: number): void {
this.manager.getPlayerWrapper().localSeek(positionTicks);
}
localStop(): void {
this.manager.getPlayerWrapper().localStop();
}
// -- queries ---------------------------------------------------------------
getLastCommand(): SendCommand | null {
return this.lastCommand;
}
/**
* Estimate where the group should be in ticks, given a known
* starting position and the time the position was valid at.
*/
estimateCurrentTicks(positionTicks: number, when: Date): number {
const lastCommand = this.lastCommand;
if (!lastCommand) return positionTicks;
const remoteNow = this.manager.getTimeSync().localDateToRemote(new Date());
const elapsedMs = remoteNow.getTime() - when.getTime();
if (lastCommand.Command === "Unpause") {
return positionTicks + elapsedMs * TicksPerMillisecond;
}
return positionTicks;
}
/**
* Drift correction tick — called on every player time update. Skips
* to the group's expected position if drift exceeds the threshold.
* SpeedToSync is intentionally not implemented (see file header).
*/
syncPlaybackTime(): void {
const lastCommand = this.lastCommand;
if (lastCommand?.Command !== "Unpause") return;
const playerWrapper = this.manager.getPlayerWrapper();
if (!playerWrapper.isPlaying()) return;
const currentMs = playerWrapper.currentTime();
const expectedTicks = this.estimateCurrentTicks(
lastCommand.PositionTicks ?? 0,
lastCommand.When as unknown as Date,
);
const expectedMs = ticksToMs(expectedTicks);
const driftMs = Math.abs(currentMs - expectedMs);
if (driftMs > SYNC_PLAY_TUNING.minDelaySkipToSync) {
console.log(
`SyncPlay syncPlaybackTime: drift ${driftMs.toFixed(
0,
)}ms exceeds threshold, seeking to ${expectedMs.toFixed(0)}ms`,
);
this.localSeek(expectedTicks);
}
}
// -- teardown --------------------------------------------------------------
destroy(): void {
this.clearScheduledCommand();
this.lastCommand = null;
this.removeAllListeners();
}
}
export default PlaybackCore;

View File

@@ -1,332 +0,0 @@
/**
* SyncPlay QueueCore — tracks the group's playlist.
*
* Responsibilities:
* - Handle `PlayQueue` group updates (NewPlaylist, SetCurrentItem,
* NextItem, PreviousItem, RemoveItems, etc.)
* - Resolve the server's flat list of ItemIds into full `BaseItemDto`s
* (with PlaylistItemId glued on for SyncPlay requests)
* - Expose `currentPlaylistItemId` — required by every SyncPlay
* request (Ready, Buffering, Seek) so the server can ignore stale
* ones from before the playlist moved
* - On NewPlaylist, ask the server we're ready by sending a Buffering
* request after the local player emits `playbackstart`
*/
import type { Api } from "@jellyfin/sdk";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api";
import { TicksPerMillisecond, WaitForEventDefaultTimeout } from "../constants";
import { EventEmitter, waitForEventOnce } from "../EventEmitter";
import type { SyncPlayManager } from "../Manager";
import {
getItemsForPlayback,
translateItemsForPlayback,
} from "../transport/queueTranslation";
import type {
PlayQueueUpdate,
PlayQueueUpdateReason,
SyncPlayQueueItem,
} from "../types";
export class QueueCore extends EventEmitter {
private manager!: SyncPlayManager;
private lastPlayQueueUpdate: PlayQueueUpdate | null = null;
/** Playable items with `PlaylistItemId` glued on. */
private playlist: BaseItemDto[] = [];
init(manager: SyncPlayManager): void {
this.manager = manager;
}
/** Handle a PlayQueue group update from the server. */
updatePlayQueue(apiClient: Api, newPlayQueue: PlayQueueUpdate): void {
(newPlayQueue as unknown as { LastUpdate: Date }).LastUpdate = new Date(
newPlayQueue.LastUpdate as unknown as string,
);
if (
(newPlayQueue.LastUpdate as unknown as Date).getTime() <=
this.getLastUpdateTime()
) {
console.debug("SyncPlay updatePlayQueue: ignoring old update");
return;
}
this.onPlayQueueUpdate(apiClient, newPlayQueue)
.then(() => {
if (
(newPlayQueue.LastUpdate as unknown as Date).getTime() <
this.getLastUpdateTime()
) {
console.warn("SyncPlay updatePlayQueue: trying to apply old update");
return;
}
const reason = newPlayQueue.Reason as PlayQueueUpdateReason;
switch (reason) {
case "NewPlaylist": {
if (!this.manager.isFollowingGroupPlayback()) {
this.manager.followGroupPlayback(apiClient).then(() => {
this.startPlayback(apiClient);
});
} else {
this.startPlayback(apiClient);
}
break;
}
case "SetCurrentItem":
case "NextItem":
case "PreviousItem": {
const playlistItemId = this.getCurrentPlaylistItemId();
this.setCurrentPlaylistItem(apiClient, playlistItemId);
break;
}
case "RemoveItems":
case "MoveItem":
case "Queue":
case "QueueNext":
case "RepeatMode":
case "ShuffleMode":
// Video-focused: we don't expose repeat/shuffle/queue mutation
// controls in the RN UI yet, so these reasons just update our
// local snapshot (already done by onPlayQueueUpdate) without
// triggering any local action.
break;
default:
console.warn(
"SyncPlay updatePlayQueue: unknown reason",
newPlayQueue.Reason,
);
break;
}
})
.catch((error) => {
console.warn("SyncPlay updatePlayQueue:", error);
});
}
/** Apply a play-queue update to local state. */
async onPlayQueueUpdate(
apiClient: Api,
playQueueUpdate: PlayQueueUpdate,
): Promise<void> {
const itemIds = (playQueueUpdate.Playlist ?? [])
.map((queueItem: SyncPlayQueueItem) => queueItem.ItemId)
.filter((id): id is string => typeof id === "string");
if (!itemIds.length) {
this.lastPlayQueueUpdate = playQueueUpdate;
this.playlist = [];
return;
}
const fetched = await getItemsForPlayback(apiClient, itemIds);
const items = await translateItemsForPlayback(apiClient, fetched, {
ids: itemIds,
});
if (
this.lastPlayQueueUpdate &&
(playQueueUpdate.LastUpdate as unknown as Date).getTime() <=
this.getLastUpdateTime()
) {
throw new Error("Trying to apply old update");
}
// Glue PlaylistItemId from the server's playlist entries onto each
// resolved item. The server-assigned IDs are what every SyncPlay
// request needs to identify the queue slot.
const playlistItems = playQueueUpdate.Playlist ?? [];
for (let i = 0; i < items.length && i < playlistItems.length; i++) {
items[i].PlaylistItemId = playlistItems[i].PlaylistItemId;
}
this.lastPlayQueueUpdate = playQueueUpdate;
this.playlist = items;
}
/**
* Send a Ready request once the local player begins playback. The
* server uses this to wait until every member is buffered before
* issuing the next Unpause.
*
* On timeout (player never starts), halt group playback so the rest
* of the group can proceed without us.
*/
scheduleReadyRequestOnPlaybackStart(apiClient: Api, origin: string): void {
waitForEventOnce(
this.manager,
"playbackstart",
WaitForEventDefaultTimeout,
["playbackerror"],
)
.then(() => {
console.debug(
"SyncPlay scheduleReadyRequestOnPlaybackStart: notifying server",
);
const playerWrapper = this.manager.getPlayerWrapper();
playerWrapper.localPause();
const currentPosition = playerWrapper.currentTime();
const currentPositionTicks = Math.round(
currentPosition * TicksPerMillisecond,
);
const isPlaying = playerWrapper.isPlaying();
const now = this.manager.getTimeSync().localDateToRemote(new Date());
try {
getSyncPlayApi(apiClient).syncPlayReady({
readyRequestDto: {
When: now.toISOString(),
PositionTicks: currentPositionTicks,
IsPlaying: isPlaying,
PlaylistItemId: this.getCurrentPlaylistItemId() ?? undefined,
},
});
} catch (error) {
console.error("SyncPlay syncPlayReady failed", error);
}
})
.catch((error) => {
console.error(
"Timed out waiting for 'playbackstart' event!",
origin,
error,
);
if (!this.manager.isSyncPlayEnabled()) {
this.manager.emit("toast", "MessageSyncPlayErrorMedia");
}
this.manager.haltGroupPlayback(apiClient);
});
}
/** Start local playback by navigating to the player screen for the current item. */
startPlayback(apiClient: Api): void {
if (!this.manager.isFollowingGroupPlayback()) {
console.debug("SyncPlay startPlayback: ignoring, not following playback");
return;
}
if (this.isPlaylistEmpty()) {
console.debug("SyncPlay startPlayback: empty playlist");
return;
}
// Estimate where to start playback from. Prefer the last playback
// command if newer than the queue update (playback ticks change
// more often than queue position).
const playbackCommand = this.manager.getLastPlaybackCommand();
let startPositionTicks = 0;
if (
playbackCommand &&
(
playbackCommand as unknown as { EmittedAt: Date }
).EmittedAt?.getTime() >= this.getLastUpdateTime()
) {
startPositionTicks = this.manager
.getPlaybackCore()
.estimateCurrentTicks(
playbackCommand.PositionTicks ?? 0,
(playbackCommand as unknown as { When: Date }).When,
);
} else {
startPositionTicks = this.manager
.getPlaybackCore()
.estimateCurrentTicks(
this.getStartPositionTicks(),
(this.getLastUpdate() ?? new Date()) as Date,
);
}
const serverId = apiClient.deviceInfo?.id ?? "";
this.scheduleReadyRequestOnPlaybackStart(apiClient, "startPlayback");
this.manager
.getPlayerWrapper()
.localPlay({
ids: this.getPlaylistAsItemIds(),
startPositionTicks,
startIndex: this.getCurrentPlaylistIndex(),
serverId,
})
.catch((error: unknown) => {
console.error("SyncPlay startPlayback: localPlay failed", error);
this.manager.emit("toast", "MessageSyncPlayErrorMedia");
});
}
/** Navigate to a specific item in the queue. */
setCurrentPlaylistItem(apiClient: Api, playlistItemId: string | null): void {
if (!this.manager.isFollowingGroupPlayback()) {
console.debug(
"SyncPlay setCurrentPlaylistItem: ignoring, not following playback",
);
return;
}
this.scheduleReadyRequestOnPlaybackStart(
apiClient,
"setCurrentPlaylistItem",
);
this.manager.getPlayerWrapper().localSetCurrentPlaylistItem(playlistItemId);
}
// -- getters ---------------------------------------------------------------
getCurrentPlaylistIndex(): number {
return this.lastPlayQueueUpdate?.PlayingItemIndex ?? -1;
}
getCurrentPlaylistItemId(): string | null {
if (!this.lastPlayQueueUpdate) return null;
const index = this.lastPlayQueueUpdate.PlayingItemIndex ?? -1;
if (index === -1) return null;
return this.playlist[index]?.PlaylistItemId ?? null;
}
getPlaylist(): BaseItemDto[] {
return this.playlist.slice(0);
}
isPlaylistEmpty(): boolean {
return this.playlist.length === 0;
}
getLastUpdate(): Date | null {
if (!this.lastPlayQueueUpdate) return null;
return this.lastPlayQueueUpdate.LastUpdate as unknown as Date;
}
getLastUpdateTime(): number {
if (!this.lastPlayQueueUpdate) return 0;
return (this.lastPlayQueueUpdate.LastUpdate as unknown as Date).getTime();
}
getStartPositionTicks(): number {
return this.lastPlayQueueUpdate?.StartPositionTicks ?? 0;
}
getPlaylistAsItemIds(): (string | undefined)[] {
if (!this.lastPlayQueueUpdate) return [];
return (this.lastPlayQueueUpdate.Playlist ?? []).map((q) => q.ItemId);
}
// -- teardown --------------------------------------------------------------
/** Clear cached playlist. Called on group disable so a re-join starts clean. */
clear(): void {
this.lastPlayQueueUpdate = null;
this.playlist = [];
}
destroy(): void {
this.clear();
this.removeAllListeners();
}
}
export default QueueCore;

View File

@@ -1,220 +0,0 @@
/**
* TimeSync — NTP-style time synchronisation with the Jellyfin server.
*
* Merged port of jellyfin-web's `core/timeSync/{TimeSync,TimeSyncServer,
* TimeSyncCore}.js` — three classes that exist on web because the
* abstract layer supports syncing against other group members, not just
* the server. RN only syncs against the server, so it's one class.
*
* Algorithm: repeatedly time a round-trip request to `getUtcTime`,
* compute `offset = ((requestReceived - requestSent) + (responseSent -
* responseReceived)) / 2`, keep the minimum-delay measurement out of
* the last 8. This is the standard NTP outlier-rejection trick — the
* measurement with the shortest delay is the most accurate because
* less network jitter could have skewed the timestamps.
*
* Polling: greedy mode at 1s intervals for the first 3 pings to warm
* up the offset, then low-profile at 60s intervals for steady-state.
* `forceUpdate()` resets to greedy mode (called on group join).
*/
import type { Api } from "@jellyfin/sdk";
import { getTimeSyncApi } from "@jellyfin/sdk/lib/utils/api";
import { EventEmitter } from "../EventEmitter";
const NumberOfTrackedMeasurements = 8;
const PollingIntervalGreedy = 1000; // ms
const PollingIntervalLowProfile = 60000; // ms
const GreedyPingCount = 3;
class Measurement {
requestSent: number;
requestReceived: number;
responseSent: number;
responseReceived: number;
constructor(
requestSent: Date,
requestReceived: Date,
responseSent: Date,
responseReceived: Date,
) {
this.requestSent = requestSent.getTime();
this.requestReceived = requestReceived.getTime();
this.responseSent = responseSent.getTime();
this.responseReceived = responseReceived.getTime();
}
/** Time offset (ms): positive means server clock is ahead of ours. */
getOffset(): number {
return (
(this.requestReceived -
this.requestSent +
(this.responseSent - this.responseReceived)) /
2
);
}
/** Round-trip delay (ms), excluding server processing. */
getDelay(): number {
return (
this.responseReceived -
this.requestSent -
(this.responseSent - this.requestReceived)
);
}
/** One-way ping (ms). */
getPing(): number {
return this.getDelay() / 2;
}
}
/**
* Tracks the offset between this client's clock and the Jellyfin server's
* clock, and exposes conversions between local and remote Dates.
*
* Listeners:
* - `"update"` (timeOffset: number, ping: number) — fires on every
* successful ping. Errors are logged but not emitted; consumers
* should treat absence of updates as transient.
*/
export class TimeSync extends EventEmitter {
private api: Api;
private pingStop = true;
private pollingInterval = PollingIntervalGreedy;
private poller: ReturnType<typeof setTimeout> | null = null;
private pings = 0;
private measurement: Measurement | null = null;
private measurements: Measurement[] = [];
constructor(api: Api) {
super();
this.api = api;
}
/** Called when the user switches Jellyfin servers. */
updateApiClient(api: Api): void {
this.api = api;
}
/** Whether we've completed at least one successful measurement. */
isReady(): boolean {
return !!this.measurement;
}
/** Current best-estimate time offset (ms). */
getTimeOffset(): number {
return this.measurement ? this.measurement.getOffset() : 0;
}
/** Current best-estimate one-way ping (ms). */
getPing(): number {
return this.measurement ? this.measurement.getPing() : 0;
}
/** Convert a server-time Date to local time. */
remoteDateToLocal(remote: Date): Date {
return new Date(remote.getTime() - this.getTimeOffset());
}
/** Convert a local Date to server time. */
localDateToRemote(local: Date): Date {
return new Date(local.getTime() + this.getTimeOffset());
}
/** Start polling. Idempotent. */
startPing(): void {
this.pingStop = false;
this.scheduleNextPing();
}
/** Stop polling. Idempotent. */
stopPing(): void {
this.pingStop = true;
if (this.poller) {
clearTimeout(this.poller);
this.poller = null;
}
}
/** Reset to greedy polling and force a fresh measurement immediately. */
forceUpdate(): void {
this.stopPing();
this.pollingInterval = PollingIntervalGreedy;
this.pings = 0;
this.startPing();
}
/** Drop all measurements. Used on group leave. */
resetMeasurements(): void {
this.measurement = null;
this.measurements = [];
}
/** Full teardown on provider unmount. */
destroy(): void {
this.stopPing();
this.resetMeasurements();
this.removeAllListeners();
}
private scheduleNextPing(): void {
if (this.poller || this.pingStop) return;
this.poller = setTimeout(() => {
this.poller = null;
this.requestPing()
.then((result) => this.onPingResponse(result))
.catch((error) => {
console.error("SyncPlay TimeSync: ping failed", error);
})
.finally(() => this.scheduleNextPing());
}, this.pollingInterval);
}
private async requestPing() {
const requestSent = new Date();
const response = await getTimeSyncApi(this.api).getUtcTime();
const responseReceived = new Date();
const data = response.data;
const requestReceived = new Date(data.RequestReceptionTime as string);
const responseSent = new Date(data.ResponseTransmissionTime as string);
return { requestSent, requestReceived, responseSent, responseReceived };
}
private onPingResponse(result: {
requestSent: Date;
requestReceived: Date;
responseSent: Date;
responseReceived: Date;
}): void {
const measurement = new Measurement(
result.requestSent,
result.requestReceived,
result.responseSent,
result.responseReceived,
);
this.measurements.push(measurement);
if (this.measurements.length > NumberOfTrackedMeasurements) {
this.measurements.shift();
}
// Outlier rejection: pick the measurement with the shortest delay.
const sorted = [...this.measurements].sort(
(a, b) => a.getDelay() - b.getDelay(),
);
this.measurement = sorted[0];
// Throttle once we've warmed up.
if (this.pings >= GreedyPingCount) {
this.pollingInterval = PollingIntervalLowProfile;
} else {
this.pings++;
}
this.emit("update", this.getTimeOffset(), this.getPing());
}
}
export default TimeSync;

View File

@@ -1,13 +0,0 @@
/**
* SyncPlay — public exports.
*
* Only what external consumers (components, hooks, screens) need.
* Internal modules (PlaybackCore, QueueCore, TimeSync, PlayerWrapper,
* queueTranslation, EventEmitter, etc.) stay package-private.
*/
export { Controller as SyncPlayController } from "./Controller";
export { msToTicks, ticksToMs } from "./constants";
export { SyncPlayManager } from "./Manager";
export { SyncPlayProvider, useSyncPlay } from "./SyncPlayProvider";
export * from "./types";

View File

@@ -1,58 +0,0 @@
/**
* PendingPlaybackTracker — tracks an in-flight `Unpause` / `Pause` request
* that we've sent to the server but haven't seen echoed back via
* `SyncPlayCommand`.
*
* Drives three things:
* 1. Drop duplicate rapid taps
* 2. Provide an optimistic-UI hint for the in-flight state
* 3. Override "current play state" when deciding pause-vs-unpause
* for the next tap
*
* Auto-clears after `pendingPlaybackTimeoutMs` so a lost broadcast
* doesn't freeze the UI forever.
*/
import { SYNC_PLAY_TUNING } from "../types";
export class PendingPlaybackTracker {
private command: "Unpause" | "Pause" | null = null;
private timeout: ReturnType<typeof setTimeout> | null = null;
private onChange: ((cmd: "Unpause" | "Pause" | null) => void) | null = null;
setChangeHandler(
handler: ((cmd: "Unpause" | "Pause" | null) => void) | null,
): void {
this.onChange = handler;
}
get(): "Unpause" | "Pause" | null {
return this.command;
}
mark(command: "Unpause" | "Pause"): void {
this.command = command;
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
console.debug(
"SyncPlay PendingPlaybackTracker: timed out waiting for broadcast",
command,
);
this.command = null;
this.timeout = null;
this.onChange?.(null);
}, SYNC_PLAY_TUNING.pendingPlaybackTimeoutMs);
this.onChange?.(command);
}
clear(): void {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (this.command !== null) {
this.command = null;
this.onChange?.(null);
}
}
}

View File

@@ -1,87 +0,0 @@
/**
* PlayerWrapper — adapter between jellyfin's tick-based playerWrapper API
* and our millisecond-based `PlayerControls`. Methods that have no RN
* analog (queue mutation hooks) delegate to provider-supplied handlers
* which navigate to the player screen.
*/
import { TicksPerMillisecond } from "../constants";
import type { PlayerControls } from "../types";
/** Options passed to `playerWrapper.localPlay` — provider navigates to the player screen. */
export interface LocalPlayOptions {
ids: (string | undefined)[];
startPositionTicks: number;
startIndex: number;
serverId?: string;
}
export class PlayerWrapper {
private controls: PlayerControls | null = null;
private localPlayHandler: ((options: LocalPlayOptions) => void) | null = null;
private setCurrentItemHandler:
| ((playlistItemId: string | null) => void)
| null = null;
/** Attach / detach the underlying player. */
bindToControls(controls: PlayerControls | null): void {
this.controls = controls;
}
/** Provider wires this to navigate to the player screen. */
setLocalPlayHandler(handler: ((options: LocalPlayOptions) => void) | null) {
this.localPlayHandler = handler;
}
/** Provider wires this to navigate to a different queue item. */
setLocalSetCurrentItemHandler(
handler: ((playlistItemId: string | null) => void) | null,
) {
this.setCurrentItemHandler = handler;
}
localUnpause(): void {
this.controls?.play();
}
localPause(): void {
this.controls?.pause();
}
/** Upstream takes ticks; RN's `seekTo` takes ms. */
localSeek(positionTicks: number): void {
this.controls?.seekTo(positionTicks / TicksPerMillisecond);
}
/** RN: pause instead of teardown — leaving the player screen is the navigator's job. */
localStop(): void {
this.controls?.pause();
}
/** Position in ms. */
currentTime(): number {
return this.controls?.getCurrentPosition() ?? 0;
}
isPlaying(): boolean {
return this.controls?.isPlaying() ?? false;
}
isPlaybackActive(): boolean {
return this.controls !== null;
}
/** RN never runs as a remote-managed player. */
isRemote(): boolean {
return false;
}
localPlay(options: LocalPlayOptions): Promise<void> {
this.localPlayHandler?.(options);
return Promise.resolve();
}
localSetCurrentPlaylistItem(playlistItemId: string | null): void {
this.setCurrentItemHandler?.(playlistItemId);
}
}

View File

@@ -1,64 +0,0 @@
/**
* bufferingDebouncer — wrap an `isBuffering: boolean → void` notify callback
* with three RN-only guards. Web gets these for free from HTML `waiting`/
* `canplay`; our `PlayerControls` exposes state (not events) and the React
* effect that polls it can fire many times per second.
*
* - **dedup**: drop redundant calls when state hasn't changed
* - **debounce buffering→true**: only escalate after the threshold;
* going back to ready cancels the pending escalation
* - **coalesce inflight**: serialize concurrent sends
*
* Returns `{ notify, dispose }`.
*/
import { SYNC_PLAY_TUNING } from "../types";
export function createBufferingDebouncer(
send: (isBuffering: boolean) => Promise<void>,
) {
let lastSent: boolean | null = null;
let inflight: Promise<void> | null = null;
let pendingTimeout: ReturnType<typeof setTimeout> | null = null;
const flush = async (isBuffering: boolean) => {
if (lastSent === isBuffering) return;
if (inflight) {
try {
await inflight;
} catch {
// ignore — used only for ordering
}
if (lastSent === isBuffering) return;
}
lastSent = isBuffering;
inflight = send(isBuffering).finally(() => {
inflight = null;
});
return inflight;
};
return {
notify(isBuffering: boolean): void {
if (pendingTimeout) {
clearTimeout(pendingTimeout);
pendingTimeout = null;
}
if (!isBuffering) {
// Ready always fires immediately.
void flush(false);
return;
}
pendingTimeout = setTimeout(() => {
pendingTimeout = null;
void flush(true);
}, SYNC_PLAY_TUNING.minBufferingThresholdMs);
},
dispose(): void {
if (pendingTimeout) {
clearTimeout(pendingTimeout);
pendingTimeout = null;
}
},
};
}

View File

@@ -1,58 +0,0 @@
/**
* reconcileToGroupOnAttach — estimate the group's current position from
* the last play/pause broadcast and seek the freshly-attached player
* there if drift exceeds the threshold.
*
* Web's player binds at group-join, so this race doesn't exist there.
* On RN the player mounts in a separate route after the join, so
* commands arrive before controls attach. Without this, the player
* resumes from its local position and is silently behind the group.
*/
import { TicksPerMillisecond } from "../constants";
import {
type PlayerControls,
type SendCommand,
SYNC_PLAY_TUNING,
} from "../types";
export function reconcileToGroupOnAttach(
controls: PlayerControls,
lastCommand: SendCommand | null,
localToRemote: (local: Date) => Date,
): void {
if (
!lastCommand ||
(lastCommand.Command !== "Unpause" && lastCommand.Command !== "Pause") ||
!lastCommand.When ||
lastCommand.PositionTicks == null
) {
return;
}
try {
const commandWhen = new Date(lastCommand.When);
let targetTicks = lastCommand.PositionTicks;
if (lastCommand.Command === "Unpause") {
const remoteNow = localToRemote(new Date());
targetTicks +=
(remoteNow.getTime() - commandWhen.getTime()) * TicksPerMillisecond;
}
const targetMs = Math.max(0, targetTicks / TicksPerMillisecond);
const currentMs = controls.getCurrentPosition();
if (
Math.abs(currentMs - targetMs) >
SYNC_PLAY_TUNING.positionReconcileThresholdMs
) {
console.log(
`SyncPlay: player attached — seeking to estimated group position ${targetMs}ms (was ${currentMs}ms)`,
);
controls.seekTo(targetMs);
}
} catch (error) {
console.warn(
"SyncPlay: failed to estimate group position on attach",
error,
);
}
}

View File

@@ -1,183 +0,0 @@
/**
* queueTranslation — expand container items into a real playable queue.
*
* The server takes the queue we send via `syncPlaySetNewQueue` and
* rebroadcasts it verbatim to every group member. Sending a container
* ID (Series, Season, BoxSet, Playlist) means every receiver fails to
* open the player because they can't directly play a container. We must
* expand to real playable item IDs before sending the queue.
*
* Video-focused: music (MusicArtist/MusicGenre) and photo branches are
* intentionally omitted. Live TV (Program), Episode auto-advance, and
* folder expansion are preserved because they're the common video flows.
*/
import type { Api } from "@jellyfin/sdk";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import {
getItemsApi,
getTvShowsApi,
getUserApi,
getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api";
export interface TranslateOptions {
ids?: string[];
shuffle?: boolean;
queryOptions?: Record<string, unknown>;
}
const PLAYBACK_FIELDS = ["Chapters", "Trickplay"] as const;
async function getCurrentUser(api: Api) {
const user = (await getUserApi(api).getCurrentUser()).data;
if (!user?.Id) {
throw new Error("SyncPlay queueTranslation: no authenticated user");
}
return user;
}
async function queryItems(
api: Api,
userId: string,
params: Record<string, unknown>,
): Promise<BaseItemDto[]> {
const res = await getItemsApi(api).getItems({
limit: 300,
fields: PLAYBACK_FIELDS as unknown as never,
excludeLocationTypes: ["Virtual"] as unknown as never,
enableTotalRecordCount: false,
collapseBoxSetItems: false,
...params,
userId,
});
return res.data.Items ?? [];
}
function fetchFolderChildren(
api: Api,
userId: string,
params: Record<string, unknown>,
): Promise<BaseItemDto[]> {
return queryItems(api, userId, {
filters: ["IsNotFolder"],
recursive: true,
...params,
});
}
/**
* Resolve item IDs into full `BaseItemDto`s.
*
* - single ID → `getItem` (cheap, no Items wrapper)
* - multi ID → `getItems` with playback defaults
*/
export async function getItemsForPlayback(
api: Api,
ids: string[],
): Promise<BaseItemDto[]> {
if (!ids.length) return [];
const userId = (await getCurrentUser(api)).Id as string;
if (ids.length === 1) {
const res = await getUserLibraryApi(api).getItem({
userId,
itemId: ids[0],
});
return res.data ? [res.data] : [];
}
return queryItems(api, userId, { ids });
}
/**
* Expand a "first item" into a real playable queue.
*
* - Program → channel items
* - Playlist → playlist children
* - IsFolder (Series, Season, BoxSet, MusicAlbum, ...) → recursive descendants
* - single Episode (when `EnableNextEpisodeAutoPlay`) → remaining series episodes
* - everything else → passthrough (Movies, Audio, single Episode w/ autoplay off)
*
* Preserves the caller's `ids` order so the receiver sees the same
* queue order the sender intended.
*/
export async function translateItemsForPlayback(
api: Api,
items: BaseItemDto[],
options: TranslateOptions = {},
): Promise<BaseItemDto[]> {
if (!items.length) return [];
const workingItems =
items.length > 1 && options.ids
? [...items].sort(
(a, b) =>
(options.ids ?? []).indexOf(a.Id ?? "") -
(options.ids ?? []).indexOf(b.Id ?? ""),
)
: items;
const firstItem = workingItems[0];
if (firstItem.Type === "Program" && firstItem.ChannelId) {
return getItemsForPlayback(api, [firstItem.ChannelId]);
}
const user = await getCurrentUser(api);
const userId = user.Id as string;
if (firstItem.Type === "Playlist") {
return queryItems(api, userId, {
parentId: firstItem.Id,
sortBy: options.shuffle ? ["Random"] : undefined,
});
}
if (firstItem.IsFolder) {
// Series, Season, BoxSet, MusicAlbum, etc.
const sortBy = options.shuffle
? ["Random"]
: firstItem.Type === "BoxSet"
? ["SortName"]
: undefined;
return fetchFolderChildren(api, userId, {
parentId: firstItem.Id,
mediaTypes: ["Audio", "Video"],
sortBy,
...(options.queryOptions ?? {}),
});
}
if (firstItem.Type === "Episode" && workingItems.length === 1) {
// Single-episode auto-next: load all remaining episodes in the
// series, starting at this one. Gated on the user preference so we
// don't surprise users who disabled autoplay.
if (!user.Configuration?.EnableNextEpisodeAutoPlay || !firstItem.SeriesId) {
return workingItems;
}
const res = await getTvShowsApi(api).getEpisodes({
seriesId: firstItem.SeriesId,
userId,
isMissing: false,
fields: PLAYBACK_FIELDS as unknown as never,
// SDK omits `isVirtualUnaired` from typed request; server honours
// it. Cast keeps wire payload identical to jellyfin-web.
...({ isVirtualUnaired: false } as Record<string, unknown>),
} as Parameters<ReturnType<typeof getTvShowsApi>["getEpisodes"]>[0]);
const all = res.data.Items ?? [];
// Drop everything before firstItem; keep firstItem and everything
// after. Empty list if firstItem isn't in the series (shouldn't
// happen, but matches upstream's behaviour).
let foundItem = false;
return all.filter((e) => {
if (foundItem) return true;
if (e.Id === firstItem.Id) {
foundItem = true;
return true;
}
return false;
});
}
// Movies, Audio, single Episode w/ autoplay off, etc.
return workingItems;
}

View File

@@ -1,94 +0,0 @@
/**
* useSyncPlayWebSocket
*
* Hook that connects the SyncPlay manager to WebSocket messages.
* Listens for SyncPlayCommand and SyncPlayGroupUpdate messages.
*
* IMPORTANT: We subscribe directly to the WebSocket via `addEventListener`
* rather than reading WebSocketProvider's `lastMessage` state. That state
* only holds the most recent message, so when the server emits bursts
* after a join (GroupJoined + StateUpdate + UserJoined + PlayQueue, all
* within a few ms), React's batching causes earlier messages to be
* overwritten before our effect can read them — most notably the
* GroupJoined message, which left the joining client thinking it hadn't
* joined while other members already saw it as a participant.
*
* Listening on the raw socket guarantees we see every frame in order.
*/
import { useEffect } from "react";
import { useWebSocketContext } from "@/providers/WebSocketProvider";
import type { SyncPlayManager } from "../Manager";
import type { GroupUpdate, SendCommand } from "../types";
/**
* Hook to connect SyncPlay manager to WebSocket
*/
export function useSyncPlayWebSocket(manager: SyncPlayManager | null): void {
const { ws } = useWebSocketContext();
useEffect(() => {
if (!ws || !manager) return;
const handleMessage = (event: WebSocketMessageEvent) => {
let parsed: { MessageType?: string; Data?: unknown };
try {
parsed = JSON.parse(event.data as string);
} catch (error) {
console.error("SyncPlay: failed to parse WebSocket message", error);
return;
}
const { MessageType, Data } = parsed;
// Only handle SyncPlay messages here; everything else is handled
// elsewhere via WebSocketProvider's lastMessage.
if (!MessageType?.startsWith("SyncPlay")) return;
console.log(
`SyncPlay WebSocket [${MessageType}]:`,
JSON.stringify(Data).substring(0, 300),
);
switch (MessageType) {
case "SyncPlayCommand": {
const command = Data as SendCommand;
console.log(
`SyncPlay: COMMAND received - ${command.Command} at ${command.When}`,
command.Command === "Seek"
? `position=${command.PositionTicks}`
: "",
);
// Note: it's normal for controls to be missing here during the
// join → navigate → load window. Manager stashes the command and
// replays it on attach.
manager.processCommand(command);
break;
}
case "SyncPlayGroupUpdate": {
// SDK's `GroupUpdate` type is a discriminated union with a
// narrower `Type` enum than the wire format. Cast through
// unknown so upstream `Manager.processGroupUpdate` can switch
// on the real string.
const update = Data as unknown as GroupUpdate;
console.debug(
"SyncPlay: group update -",
(update as { Type?: string }).Type,
);
manager.processGroupUpdate(update);
break;
}
default:
break;
}
};
ws.addEventListener("message", handleMessage);
return () => {
ws.removeEventListener("message", handleMessage);
};
}, [ws, manager]);
}

View File

@@ -1,88 +0,0 @@
/**
* SyncPlay — public types and tuning constants.
*
* Re-exports the SDK types we use, defines the small RN-specific
* extensions (PlayerControls, OSD actions), and centralises the magic
* numbers that govern sync behaviour.
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
// SDK type re-exports — kept narrow on purpose, only what callers
// actually reach for.
export type {
GroupInfoDto,
GroupQueueMode,
GroupRepeatMode,
GroupShuffleMode,
GroupStateType,
GroupUpdate,
PlayQueueUpdate,
PlayQueueUpdateReason,
SendCommand,
SendCommandType,
SyncPlayQueueItem,
SyncPlayUserAccessType,
} from "@jellyfin/sdk/lib/generated-client/models";
/** Jellyfin's tick unit. 1ms = 10000 ticks. */
export const TicksPerMillisecond = 10000;
/**
* Player controls SyncPlay drives. The provider wires this up against
* the active RN player (mpv / VLC / expo-video).
*/
export interface PlayerControls {
play: () => void;
pause: () => void;
/** Seek to absolute position in milliseconds. */
seekTo: (positionMs: number) => void;
setSpeed: (speed: number) => void;
getSpeed: () => number;
/** Current position in milliseconds. */
getCurrentPosition: () => number;
isPlaying: () => boolean;
isBuffering: () => boolean;
}
/** OSD action types — drive optional player-overlay feedback. */
export type SyncPlayOsdAction =
/** transient — 1.5s pulse, the unpause command fired locally */
| "unpause"
/** transient — 1.5s pulse, the pause command fired locally */
| "pause"
/** transient — 1.5s pulse, a seek command applied locally */
| "seek"
/** persistent — group is about to play (Waiting+Unpause / pending Unpause) */
| "schedule-play"
/** persistent — another client is buffering (Waiting+Buffer) */
| "buffering"
/** persistent — group transitioning to pause (Waiting+Pause) */
| "wait-pause"
/** persistent — group transitioning to unpause; sibling of schedule-play */
| "wait-unpause";
/**
* Tuning constants. These mirror jellyfin-web's defaults; tweak with
* care — they affect perceived sync quality across all clients.
*/
export const SYNC_PLAY_TUNING = {
/** Drift threshold (ms) above which we hard-seek to catch up. */
minDelaySkipToSync: 400,
/** Drift beyond this (ms) is always corrected by seeking. */
maxDelaySync: 3000,
/** Don't escalate buffering to the group for blips shorter than this (ms). */
minBufferingThresholdMs: 3000,
/** Player-attach drift (ms) above which we reconcile to group position. */
positionReconcileThresholdMs: 500,
/** Safety timeout (ms) for in-flight Pause/Unpause optimistic UI. */
pendingPlaybackTimeoutMs: 1500,
} as const;
/** Options accepted by `Controller.play`. */
export interface PlayOptions {
ids?: string[];
items?: BaseItemDto[];
startIndex?: number;
startPositionTicks?: number;
}

View File

@@ -44,15 +44,6 @@ interface WebSocketContextType {
lastMessage: WebSocketMessage | null; lastMessage: WebSocketMessage | null;
sendMessage: (message: any) => void; sendMessage: (message: any) => void;
clearLastMessage: () => void; clearLastMessage: () => void;
/**
* Acquire a keep-alive token. While at least one token is held the
* WebSocket will NOT be closed on AppState background/inactive. Used
* by the video player while in Picture-in-Picture so SyncPlay (and
* any other server-pushed events) keep flowing. Returns a release
* function — call it (or rely on the React effect cleanup) when the
* keep-alive is no longer needed.
*/
acquireKeepAlive: () => () => void;
} }
const WebSocketContext = createContext<WebSocketContextType | null>(null); const WebSocketContext = createContext<WebSocketContextType | null>(null);
@@ -72,21 +63,6 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
const libraryChangeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>( const libraryChangeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(
null, null,
); );
// Ref-counted keep-alive: while > 0 we skip the AppState→background
// close so the socket survives PiP / brief OS suspensions. iOS keeps
// the audio session (and therefore networking) alive while PiP is
// active, so the WS can continue to receive SyncPlay commands.
const keepAliveCountRef = useRef(0);
const acquireKeepAlive = useCallback((): (() => void) => {
keepAliveCountRef.current += 1;
let released = false;
return () => {
if (released) return;
released = true;
keepAliveCountRef.current = Math.max(0, keepAliveCountRef.current - 1);
};
}, []);
const connectWebSocket = useCallback(() => { const connectWebSocket = useCallback(() => {
if (!deviceId || !api?.accessToken || !isNetworkConnected) { if (!deviceId || !api?.accessToken || !isNetworkConnected) {
@@ -259,20 +235,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
useEffect(() => { useEffect(() => {
const handleAppStateChange = (state: AppStateStatus) => { const handleAppStateChange = (state: AppStateStatus) => {
if (state === "background" || state === "inactive") { if (state === "background" || state === "inactive") {
if (keepAliveCountRef.current > 0) {
console.log(
`App backgrounded but WS keep-alive held (${keepAliveCountRef.current}); leaving socket open`,
);
return;
}
console.log("App moving to background, closing WebSocket..."); console.log("App moving to background, closing WebSocket...");
ws?.close(); ws?.close();
} else if (state === "active") { } else if (state === "active") {
// Only reconnect if we actually lost the socket (we may have
// skipped the close above because of a keep-alive token).
if (ws?.readyState === WebSocket.OPEN) {
return;
}
console.log("App coming to foreground, reconnecting WebSocket..."); console.log("App coming to foreground, reconnecting WebSocket...");
connectWebSocket(); connectWebSocket();
} }
@@ -302,14 +267,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
}, []); }, []);
return ( return (
<WebSocketContext.Provider <WebSocketContext.Provider
value={{ value={{ ws, isConnected, lastMessage, sendMessage, clearLastMessage }}
ws,
isConnected,
lastMessage,
sendMessage,
clearLastMessage,
acquireKeepAlive,
}}
> >
{children} {children}
</WebSocketContext.Provider> </WebSocketContext.Provider>

View File

@@ -4,9 +4,6 @@
"error_title": "خطأ", "error_title": "خطأ",
"login_title": "تسجيل الدخول", "login_title": "تسجيل الدخول",
"login_to_title": "تسجيل الدخول إلى", "login_to_title": "تسجيل الدخول إلى",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "اسم المستخدم", "username_placeholder": "اسم المستخدم",
"password_placeholder": "كلمة المرور", "password_placeholder": "كلمة المرور",
"login_button": "تسجيل الدخول", "login_button": "تسجيل الدخول",
@@ -33,54 +30,48 @@
"connect_button": "اتصل", "connect_button": "اتصل",
"previous_servers": "الخوادم السابقة", "previous_servers": "الخوادم السابقة",
"clear_button": "مسح", "clear_button": "مسح",
"swipe_to_remove": "مرر للإزالة", "swipe_to_remove": "Swipe to remove",
"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}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"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": "رمز PIN مكون من 4 أرقام مطلوب عند التبديل", "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": "التحقق من اتصال الخادم...",
@@ -95,9 +86,8 @@
"oops": "عفوًا!", "oops": "عفوًا!",
"error_message": "حدث خطأ ما.\nيرجى تسجيل الخروج ثم الدخول مرة أخرى.", "error_message": "حدث خطأ ما.\nيرجى تسجيل الخروج ثم الدخول مرة أخرى.",
"continue_watching": "متابعة المشاهدة", "continue_watching": "متابعة المشاهدة",
"continue": "Continue",
"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": "حلقات مقترحة",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "الإعدادات", "settings_title": "الإعدادات",
"log_out_button": "تسجيل الخروج", "log_out_button": "تسجيل الخروج",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "الأقسام" "title": "الأقسام"
}, },
@@ -136,45 +120,36 @@
}, },
"appearance": { "appearance": {
"title": "المظهر", "title": "المظهر",
"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"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"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": "رابط محلي", "local_url": "Local URL",
"local_url_hint": "أدخل عنوان الخادم المحلي الخاص بك (على سبيل المثال 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": "الـ URL الخارجي", "remote": "Remote URL",
"not_connected": "غير متصل", "not_connected": "Not connected",
"current_server": "الخادم الحالي", "current_server": "Current Server",
"remote_url": "الـ URL الخارجي", "remote_url": "Remote URL",
"active_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": "معلومات المستخدم",
@@ -199,22 +174,6 @@
"rewind_length": "مدة الترجيع", "rewind_length": "مدة الترجيع",
"seconds_unit": "ث" "seconds_unit": "ث"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "التحكم بالإيماءات", "gesture_controls_title": "التحكم بالإيماءات",
"horizontal_swipe_skip": "السحب الأفقي للتخطي", "horizontal_swipe_skip": "السحب الأفقي للتخطي",
@@ -223,10 +182,10 @@
"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": "الصوت",
@@ -236,12 +195,12 @@
"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": {
@@ -292,45 +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."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"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"
}, },
@@ -362,8 +305,8 @@
"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": "معطل"
}, },
@@ -371,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": "الإضافات",
@@ -414,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",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "أدخل رابط خادم Streamystats الخاص بك. يجب أن يتضمن الرابط البروتوكول 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": {
@@ -455,21 +398,15 @@
"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"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "المقدمة", "title": "المقدمة",
@@ -493,21 +430,6 @@
"error_deleting_files": "خطأ في حذف الملفات", "error_deleting_files": "خطأ في حذف الملفات",
"background_downloads_enabled": "تم تفعيل التنزيلات في الخلفية", "background_downloads_enabled": "تم تفعيل التنزيلات في الخلفية",
"background_downloads_disabled": "تم تعطيل التنزيلات في الخلفية" "background_downloads_disabled": "تم تعطيل التنزيلات في الخلفية"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "يتطلب التحديث الجديد تنزيل المحتوى مرة أخرى. يرجى إزالة كل المحتوى الذي تم تنزيله والمحاولة مرة أخرى.", "new_app_version_requires_re_download_description": "يتطلب التحديث الجديد تنزيل المحتوى مرة أخرى. يرجى إزالة كل المحتوى الذي تم تنزيله والمحاولة مرة أخرى.",
"back": "رجوع", "back": "رجوع",
"delete": "حذف", "delete": "حذف",
"delete_download": "Delete Download",
"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}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "اختر", "select": "اختر",
"no_trailer_available": "لا يوجد مقطع دعائي متوفر", "no_trailer_available": "لا يوجد مقطع دعائي متوفر",
"video": "فيديو", "video": "فيديو",
"audio": "الصوت", "audio": "الصوت",
"subtitle": "الترجمة", "subtitle": "الترجمة",
"play": "تشغيل", "play": "تشغيل",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "لا شيء", "none": "لا شيء",
"track": "أغنية", "track": "Track",
"cancel": "إلغاء", "cancel": "Cancel",
"stop": "Stop", "delete": "Delete",
"delete": "حذف", "ok": "OK",
"ok": "حسناً", "remove": "Remove",
"remove": "إزالة", "next": "Next",
"next": "التالي", "back": "Back",
"back": "رجوع", "continue": "Continue",
"continue": "متابعة", "verifying": "Verifying..."
"verifying": "جارٍ التحقق...",
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "بحث...", "search": "بحث...",
@@ -606,10 +521,10 @@
"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": "أضيف مؤخرًا",
@@ -641,7 +556,6 @@
"movies": "أفلام", "movies": "أفلام",
"series": "مسلسلات", "series": "مسلسلات",
"boxsets": "مجموعات", "boxsets": "مجموعات",
"playlists": "Playlists",
"items": "عناصر" "items": "عناصر"
}, },
"options": { "options": {
@@ -652,20 +566,15 @@
"poster": "ملصق", "poster": "ملصق",
"cover": "غلاف", "cover": "غلاف",
"show_titles": "إظهار العناوين", "show_titles": "إظهار العناوين",
"show_stats": "إظهار الإحصائيات", "show_stats": "إظهار الإحصائيات"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "الأنواع", "genres": "الأنواع",
"years": "السنوات", "years": "السنوات",
"sort_by": "ترتيب حسب", "sort_by": "ترتيب حسب",
"filter_by": "تصفية حسب", "filter_by": "Filter By",
"sort_order": "اتجاه الترتيب", "sort_order": "اتجاه الترتيب",
"tags": "الوسوم", "tags": "الوسوم"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "لا توجد روابط" "no_links": "لا توجد روابط"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "خطأ", "error": "خطأ",
"failed_to_get_stream_url": "فشل في الحصول على رابط البث", "failed_to_get_stream_url": "فشل في الحصول على رابط البث",
"an_error_occured_while_playing_the_video": "حدث خطأ أثناء تشغيل الفيديو. تحقق من السجلات في الإعدادات.", "an_error_occured_while_playing_the_video": "حدث خطأ أثناء تشغيل الفيديو. تحقق من السجلات في الإعدادات.",
@@ -697,39 +604,11 @@
"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": "هل تريد تشغيل الملف الذي تم تنزيله؟",
"downloaded_file_yes": "نعم", "downloaded_file_yes": "نعم",
"downloaded_file_no": "لا", "downloaded_file_no": "لا",
"downloaded_file_cancel": "إلغاء", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "التالي", "next_up": "التالي",
@@ -738,11 +617,6 @@
"series": "مسلسلات", "series": "مسلسلات",
"seasons": "مواسم", "seasons": "مواسم",
"season": "موسم", "season": "موسم",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "لا توجد حلقات لهذا الموسم", "no_episodes_for_this_season": "لا توجد حلقات لهذا الموسم",
"overview": "نظرة عامة", "overview": "نظرة عامة",
"more_with": "المزيد مع {{name}}", "more_with": "المزيد مع {{name}}",
@@ -750,24 +624,13 @@
"no_similar_items_found": "لم يتم العثور على عناصر مشابهة", "no_similar_items_found": "لم يتم العثور على عناصر مشابهة",
"video": "فيديو", "video": "فيديو",
"more_details": "المزيد من التفاصيل", "more_details": "المزيد من التفاصيل",
"media_options": "خيارات الوسائط", "media_options": "Media Options",
"quality": "الجودة", "quality": "الجودة",
"audio": "الصوت", "audio": "الصوت",
"subtitles": { "subtitles": "الترجمة",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "عرض المزيد", "show_more": "عرض المزيد",
"show_less": "عرض أقل", "show_less": "عرض أقل",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "ظهر في", "appeared_in": "ظهر في",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "تعذر تحميل العنصر", "could_not_load_item": "تعذر تحميل العنصر",
"none": "لا شيء", "none": "لا شيء",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "تنزيل {{item_count}} عناصر", "download_x_item": "تنزيل {{item_count}} عناصر",
"download_unwatched_only": "غير المشاهدة فقط", "download_unwatched_only": "غير المشاهدة فقط",
"download_button": "تنزيل" "download_button": "تنزيل"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "التالي", "next": "التالي",
@@ -795,18 +652,7 @@
"movies": "أفلام", "movies": "أفلام",
"sports": "رياضة", "sports": "رياضة",
"for_kids": "للأطفال", "for_kids": "للأطفال",
"news": "أخبار", "news": "أخبار"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "تأكيد", "confirm": "تأكيد",
@@ -851,12 +697,6 @@
"decline": "رفض", "decline": "رفض",
"requested_by": "مطلوب من {{user}}", "requested_by": "مطلوب من {{user}}",
"unknown_user": "مستخدم غير معروف", "unknown_user": "مستخدم غير معروف",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "خادم Seerr لا يستوفي الحد الأدنى للإصدار المطلوب! يرجى التحديث إلى إصدار 2.0.0 على الأقل", "jellyseer_does_not_meet_requirements": "خادم Seerr لا يستوفي الحد الأدنى للإصدار المطلوب! يرجى التحديث إلى إصدار 2.0.0 على الأقل",
"jellyseerr_test_failed": "فشل اختبار Seerr. يرجى المحاولة مرة أخرى.", "jellyseerr_test_failed": "فشل اختبار Seerr. يرجى المحاولة مرة أخرى.",
@@ -876,162 +716,130 @@
"search": "بحث", "search": "بحث",
"library": "المكتبة", "library": "المكتبة",
"custom_links": "روابط مخصصة", "custom_links": "روابط مخصصة",
"favorites": "المفضلة", "favorites": "المفضلة"
"settings": "Settings"
}, },
"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)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Error", "error_title": "Error",
"login_title": "Inicia sessió", "login_title": "Inicia sessió",
"login_to_title": "Inicia sessió a", "login_to_title": "Inicia sessió a",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Nom d'usuari", "username_placeholder": "Nom d'usuari",
"password_placeholder": "Contrasenya", "password_placeholder": "Contrasenya",
"login_button": "Inicia sessió", "login_button": "Inicia sessió",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Oops!", "oops": "Oops!",
"error_message": "Alguna cosa ha anat malament.\nTanqueu la sessió i torneu-la a iniciar.", "error_message": "Alguna cosa ha anat malament.\nTanqueu la sessió i torneu-la a iniciar.",
"continue_watching": "Continua veient", "continue_watching": "Continua veient",
"continue": "Continue",
"next_up": "A continuació", "next_up": "A continuació",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Afegit recentment a {{libraryName}}", "recently_added_in": "Afegit recentment a {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Configuració", "settings_title": "Configuració",
"log_out_button": "Tanca sessió", "log_out_button": "Tanca sessió",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categories" "title": "Categories"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Appearance", "title": "Appearance",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Durada del rebobinat", "rewind_length": "Durada del rebobinat",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Gesture Controls", "gesture_controls_title": "Gesture Controls",
"horizontal_swipe_skip": "Horizontal Swipe to Skip", "horizontal_swipe_skip": "Horizontal Swipe to Skip",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Error en suprimir fitxers", "error_deleting_files": "Error en suprimir fitxers",
"background_downloads_enabled": "Descàrregues en segon pla activades", "background_downloads_enabled": "Descàrregues en segon pla activades",
"background_downloads_disabled": "Descàrregues en segon pla desactivades" "background_downloads_disabled": "Descàrregues en segon pla desactivades"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "L'actualització nova requereix que el contingut es torni a descarregar. Suprimiu tot el contingut descarregat i torneu-ho a provar.", "new_app_version_requires_re_download_description": "L'actualització nova requereix que el contingut es torni a descarregar. Suprimiu tot el contingut descarregat i torneu-ho a provar.",
"back": "Enrere", "back": "Enrere",
"delete": "Suprimeix", "delete": "Suprimeix",
"delete_download": "Delete Download",
"something_went_wrong": "Alguna cosa ha anat malament", "something_went_wrong": "Alguna cosa ha anat malament",
"could_not_get_stream_url_from_jellyfin": "No s'ha pogut obtenir l'URL del flux de Jellyfin", "could_not_get_stream_url_from_jellyfin": "No s'ha pogut obtenir l'URL del flux de Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Select", "select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Vídeo", "video": "Vídeo",
"audio": "Àudio", "audio": "Àudio",
"subtitle": "Subtítols", "subtitle": "Subtítols",
"play": "Play", "play": "Play",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Cerca...", "search": "Cerca...",
@@ -641,7 +556,6 @@
"movies": "pel·lícules", "movies": "pel·lícules",
"series": "sèries", "series": "sèries",
"boxsets": "col·leccions", "boxsets": "col·leccions",
"playlists": "Playlists",
"items": "elements" "items": "elements"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Cartell", "poster": "Cartell",
"cover": "Coberta", "cover": "Coberta",
"show_titles": "Mostrar títols", "show_titles": "Mostrar títols",
"show_stats": "Mostrar estadístiques", "show_stats": "Mostrar estadístiques"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Gèneres", "genres": "Gèneres",
@@ -661,11 +574,7 @@
"sort_by": "Ordenar per", "sort_by": "Ordenar per",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Ordre", "sort_order": "Ordre",
"tags": "Etiquetes", "tags": "Etiquetes"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "No hi ha enllaços" "no_links": "No hi ha enllaços"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Error", "error": "Error",
"failed_to_get_stream_url": "No s'ha pogut obtenir l'URL del flux", "failed_to_get_stream_url": "No s'ha pogut obtenir l'URL del flux",
"an_error_occured_while_playing_the_video": "S'ha produït un error en reproduir el vídeo. Consulteu els registres a la configuració.", "an_error_occured_while_playing_the_video": "S'ha produït un error en reproduir el vídeo. Consulteu els registres a la configuració.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "A continuació", "next_up": "A continuació",
@@ -738,11 +617,6 @@
"series": "Sèries", "series": "Sèries",
"seasons": "Temporades", "seasons": "Temporades",
"season": "Temporada", "season": "Temporada",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "No hi ha episodis per a aquesta temporada", "no_episodes_for_this_season": "No hi ha episodis per a aquesta temporada",
"overview": "Descripció general", "overview": "Descripció general",
"more_with": "Més amb {{name}}", "more_with": "Més amb {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Media Options", "media_options": "Media Options",
"quality": "Qualitat", "quality": "Qualitat",
"audio": "Àudio", "audio": "Àudio",
"subtitles": { "subtitles": "Subtítols",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Mostra més", "show_more": "Mostra més",
"show_less": "Mostra menys", "show_less": "Mostra menys",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Va aparèixer a", "appeared_in": "Va aparèixer a",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "No s'ha pogut carregar l'element", "could_not_load_item": "No s'ha pogut carregar l'element",
"none": "Cap", "none": "Cap",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Descarrega {{item_count}} elements", "download_x_item": "Descarrega {{item_count}} elements",
"download_unwatched_only": "Unwatched Only", "download_unwatched_only": "Unwatched Only",
"download_button": "Descarrega" "download_button": "Descarrega"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Següent", "next": "Següent",
@@ -795,18 +652,7 @@
"movies": "Pel·lícules", "movies": "Pel·lícules",
"sports": "Esports", "sports": "Esports",
"for_kids": "Infantil", "for_kids": "Infantil",
"news": "Notícies", "news": "Notícies"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Confirma", "confirm": "Confirma",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "El servidor Jellyseerr no compleix els requisits mínims de versió! Actualitzeu-lo almenys a la versió 2.0.0", "jellyseer_does_not_meet_requirements": "El servidor Jellyseerr no compleix els requisits mínims de versió! Actualitzeu-lo almenys a la versió 2.0.0",
"jellyseerr_test_failed": "Ha fallat la prova de Jellyseerr. Torneu-ho a provar.", "jellyseerr_test_failed": "Ha fallat la prova de Jellyseerr. Torneu-ho a provar.",
@@ -876,8 +716,7 @@
"search": "Cercar", "search": "Cercar",
"library": "Biblioteca", "library": "Biblioteca",
"custom_links": "Enllaços personalitzats", "custom_links": "Enllaços personalitzats",
"favorites": "Preferits", "favorites": "Preferits"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Chyba", "error_title": "Chyba",
"login_title": "Přihlásit se", "login_title": "Přihlásit se",
"login_to_title": "Přihlásit se do", "login_to_title": "Přihlásit se do",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Uživatelské jméno", "username_placeholder": "Uživatelské jméno",
"password_placeholder": "Heslo", "password_placeholder": "Heslo",
"login_button": "Přihlásit se", "login_button": "Přihlásit se",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Jejda!", "oops": "Jejda!",
"error_message": "Něco se pokazilo.\nOdhlaste se a znovu se prosím.", "error_message": "Něco se pokazilo.\nOdhlaste se a znovu se prosím.",
"continue_watching": "Pokračovat ve sledování", "continue_watching": "Pokračovat ve sledování",
"continue": "Continue",
"next_up": "Další nahoru", "next_up": "Další nahoru",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Nedávno přidané v {{libraryName}}", "recently_added_in": "Nedávno přidané v {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Nastavení", "settings_title": "Nastavení",
"log_out_button": "Odhlásit se", "log_out_button": "Odhlásit se",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categories" "title": "Categories"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Appearance", "title": "Appearance",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Délka zpětného větru", "rewind_length": "Délka zpětného větru",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Ovládání gest", "gesture_controls_title": "Ovládání gest",
"horizontal_swipe_skip": "Horizontální přejetím přeskočit", "horizontal_swipe_skip": "Horizontální přejetím přeskočit",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Chyba při mazání souborů", "error_deleting_files": "Chyba při mazání souborů",
"background_downloads_enabled": "Stahování na pozadí povoleno", "background_downloads_enabled": "Stahování na pozadí povoleno",
"background_downloads_disabled": "Stahování na pozadí zakázáno" "background_downloads_disabled": "Stahování na pozadí zakázáno"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Nová aktualizace vyžaduje opětovné stažení obsahu. Odstraňte prosím veškerý stažený obsah a zkuste to znovu.", "new_app_version_requires_re_download_description": "Nová aktualizace vyžaduje opětovné stažení obsahu. Odstraňte prosím veškerý stažený obsah a zkuste to znovu.",
"back": "Zpět", "back": "Zpět",
"delete": "Vymazat", "delete": "Vymazat",
"delete_download": "Delete Download",
"something_went_wrong": "Něco se pokazilo", "something_went_wrong": "Něco se pokazilo",
"could_not_get_stream_url_from_jellyfin": "Nelze získat URL streamu z Jellyfin", "could_not_get_stream_url_from_jellyfin": "Nelze získat URL streamu z Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Vybrat", "select": "Vybrat",
"no_trailer_available": "Přípojné vozidlo není k dispozici", "no_trailer_available": "Přípojné vozidlo není k dispozici",
"video": "Video", "video": "Video",
"audio": "Zvuk", "audio": "Zvuk",
"subtitle": "Podtitulek", "subtitle": "Podtitulek",
"play": "Hrát", "play": "Hrát",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Hledat...", "search": "Hledat...",
@@ -641,7 +556,6 @@
"movies": "Filmy", "movies": "Filmy",
"series": "Série", "series": "Série",
"boxsets": "Sada boxů", "boxsets": "Sada boxů",
"playlists": "Playlists",
"items": "Položky" "items": "Položky"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Plakát", "poster": "Plakát",
"cover": "Kryt", "cover": "Kryt",
"show_titles": "Zobrazit názvy", "show_titles": "Zobrazit názvy",
"show_stats": "Zobrazit statistiky", "show_stats": "Zobrazit statistiky"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genres", "genres": "Genres",
@@ -661,11 +574,7 @@
"sort_by": "Seřadit podle", "sort_by": "Seřadit podle",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Řazení", "sort_order": "Řazení",
"tags": "Štítky", "tags": "Štítky"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Žádné odkazy" "no_links": "Žádné odkazy"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Chyba", "error": "Chyba",
"failed_to_get_stream_url": "Nepodařilo se získat URL streamu", "failed_to_get_stream_url": "Nepodařilo se získat URL streamu",
"an_error_occured_while_playing_the_video": "Při přehrávání videa došlo k chybě. Zkontrolujte logy v nastavení.", "an_error_occured_while_playing_the_video": "Při přehrávání videa došlo k chybě. Zkontrolujte logy v nastavení.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Další nahoru", "next_up": "Další nahoru",
@@ -738,11 +617,6 @@
"series": "Série", "series": "Série",
"seasons": "Série", "seasons": "Série",
"season": "Sezóna", "season": "Sezóna",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Žádné epizody pro tuto sezónu", "no_episodes_for_this_season": "Žádné epizody pro tuto sezónu",
"overview": "Přehled", "overview": "Přehled",
"more_with": "Více s {{name}}", "more_with": "Více s {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Media Options", "media_options": "Media Options",
"quality": "Kvalita", "quality": "Kvalita",
"audio": "Zvuk", "audio": "Zvuk",
"subtitles": { "subtitles": "Podtitulek",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Zobrazit více", "show_more": "Zobrazit více",
"show_less": "Zobrazit méně", "show_less": "Zobrazit méně",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Zobrazeno v", "appeared_in": "Zobrazeno v",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Nelze načíst položku", "could_not_load_item": "Nelze načíst položku",
"none": "Nic", "none": "Nic",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Stáhnout položky {{item_count}}", "download_x_item": "Stáhnout položky {{item_count}}",
"download_unwatched_only": "Pouze nezhlédnuté", "download_unwatched_only": "Pouze nezhlédnuté",
"download_button": "Stáhnout" "download_button": "Stáhnout"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Další", "next": "Další",
@@ -795,18 +652,7 @@
"movies": "Filmy", "movies": "Filmy",
"sports": "Sporty", "sports": "Sporty",
"for_kids": "Pro děti", "for_kids": "Pro děti",
"news": "Novinky", "news": "Novinky"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Potvrdit", "confirm": "Potvrdit",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Seerr server nesplňuje minimální požadavky na verzi! Aktualizujte prosím alespoň na 2.0.0", "jellyseer_does_not_meet_requirements": "Seerr server nesplňuje minimální požadavky na verzi! Aktualizujte prosím alespoň na 2.0.0",
"jellyseerr_test_failed": "Seerr test se nezdařil. Zkuste to prosím znovu.", "jellyseerr_test_failed": "Seerr test se nezdařil. Zkuste to prosím znovu.",
@@ -876,8 +716,7 @@
"search": "Hledat", "search": "Hledat",
"library": "Knihovna", "library": "Knihovna",
"custom_links": "Vlastní odkazy", "custom_links": "Vlastní odkazy",
"favorites": "Oblíbené", "favorites": "Oblíbené"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Fejl", "error_title": "Fejl",
"login_title": "Log ind", "login_title": "Log ind",
"login_to_title": "Log ind på", "login_to_title": "Log ind på",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Brugernavn", "username_placeholder": "Brugernavn",
"password_placeholder": "Adgangskode", "password_placeholder": "Adgangskode",
"login_button": "Log ind", "login_button": "Log ind",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Ups!", "oops": "Ups!",
"error_message": "Noget gik galt.\nLog venligst ud og ind igen.", "error_message": "Noget gik galt.\nLog venligst ud og ind igen.",
"continue_watching": "Fortsæt med at se", "continue_watching": "Fortsæt med at se",
"continue": "Continue",
"next_up": "Næste", "next_up": "Næste",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Senest tilføjet i {{libraryName}}", "recently_added_in": "Senest tilføjet i {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Indstillinger", "settings_title": "Indstillinger",
"log_out_button": "Log ud", "log_out_button": "Log ud",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categories" "title": "Categories"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Appearance", "title": "Appearance",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Spol tilbage længde", "rewind_length": "Spol tilbage længde",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Bevægelsesstyring", "gesture_controls_title": "Bevægelsesstyring",
"horizontal_swipe_skip": "Vandret Stryg for at springe over", "horizontal_swipe_skip": "Vandret Stryg for at springe over",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Fejl ved sletning af filer", "error_deleting_files": "Fejl ved sletning af filer",
"background_downloads_enabled": "Baggrundsdownloads aktiveret", "background_downloads_enabled": "Baggrundsdownloads aktiveret",
"background_downloads_disabled": "Baggrundsdownloads deaktiveret" "background_downloads_disabled": "Baggrundsdownloads deaktiveret"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Den nye opdatering kræver, at indhold downloades igen. Fjern venligst alt downloadet indhold og prøv igen.", "new_app_version_requires_re_download_description": "Den nye opdatering kræver, at indhold downloades igen. Fjern venligst alt downloadet indhold og prøv igen.",
"back": "Tilbage", "back": "Tilbage",
"delete": "Slet", "delete": "Slet",
"delete_download": "Delete Download",
"something_went_wrong": "Noget gik galt", "something_went_wrong": "Noget gik galt",
"could_not_get_stream_url_from_jellyfin": "Kunne ikke hente stream URL'en fra Jellyfin", "could_not_get_stream_url_from_jellyfin": "Kunne ikke hente stream URL'en fra Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Vælg", "select": "Vælg",
"no_trailer_available": "Intet påhængskøretøj tilgængeligt", "no_trailer_available": "Intet påhængskøretøj tilgængeligt",
"video": "Video", "video": "Video",
"audio": "Lyd", "audio": "Lyd",
"subtitle": "Undertekster", "subtitle": "Undertekster",
"play": "Afspil", "play": "Afspil",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Søg...", "search": "Søg...",
@@ -641,7 +556,6 @@
"movies": "film", "movies": "film",
"series": "serier", "series": "serier",
"boxsets": "box sæt", "boxsets": "box sæt",
"playlists": "Playlists",
"items": "elementer" "items": "elementer"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Plakat", "poster": "Plakat",
"cover": "Omslag", "cover": "Omslag",
"show_titles": "Vis titler", "show_titles": "Vis titler",
"show_stats": "Vis statistik", "show_stats": "Vis statistik"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genrer", "genres": "Genrer",
@@ -661,11 +574,7 @@
"sort_by": "Sortér efter", "sort_by": "Sortér efter",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Sorteringsrækkefølge", "sort_order": "Sorteringsrækkefølge",
"tags": "Mærker", "tags": "Mærker"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Ingen links" "no_links": "Ingen links"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Fejl", "error": "Fejl",
"failed_to_get_stream_url": "Kunne ikke hente stream URL'en", "failed_to_get_stream_url": "Kunne ikke hente stream URL'en",
"an_error_occured_while_playing_the_video": "Der opstod en fejl under afspilning af videoen. Tjek logfilerne i indstillinger.", "an_error_occured_while_playing_the_video": "Der opstod en fejl under afspilning af videoen. Tjek logfilerne i indstillinger.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Næste", "next_up": "Næste",
@@ -738,11 +617,6 @@
"series": "Serier", "series": "Serier",
"seasons": "Sæsoner", "seasons": "Sæsoner",
"season": "Sæson", "season": "Sæson",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Ingen episoder for denne sæson", "no_episodes_for_this_season": "Ingen episoder for denne sæson",
"overview": "Oversigt", "overview": "Oversigt",
"more_with": "Mere med {{name}}", "more_with": "Mere med {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Media Options", "media_options": "Media Options",
"quality": "Kvalitet", "quality": "Kvalitet",
"audio": "Lyd", "audio": "Lyd",
"subtitles": { "subtitles": "Undertekster",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Vis mere", "show_more": "Vis mere",
"show_less": "Vis mindre", "show_less": "Vis mindre",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Medvirket i", "appeared_in": "Medvirket i",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Kunne ikke indlæse elementet", "could_not_load_item": "Kunne ikke indlæse elementet",
"none": "Ingen", "none": "Ingen",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Download {{item_count}} elementer", "download_x_item": "Download {{item_count}} elementer",
"download_unwatched_only": "Kun Usete", "download_unwatched_only": "Kun Usete",
"download_button": "Hent" "download_button": "Hent"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Næste", "next": "Næste",
@@ -795,18 +652,7 @@
"movies": "Film", "movies": "Film",
"sports": "Sport", "sports": "Sport",
"for_kids": "For børn", "for_kids": "For børn",
"news": "Nyheder", "news": "Nyheder"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Bekræft", "confirm": "Bekræft",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Jellyseerr serveren opfylder ikke minimumskravene! Opdater venligst til mindst 2.0.0", "jellyseer_does_not_meet_requirements": "Jellyseerr serveren opfylder ikke minimumskravene! Opdater venligst til mindst 2.0.0",
"jellyseerr_test_failed": "Jellyseerr test mislykkedes. Prøv venligst igen.", "jellyseerr_test_failed": "Jellyseerr test mislykkedes. Prøv venligst igen.",
@@ -876,8 +716,7 @@
"search": "Søg", "search": "Søg",
"library": "Bibliotek", "library": "Bibliotek",
"custom_links": "Tilpassede links", "custom_links": "Tilpassede links",
"favorites": "Favoritter", "favorites": "Favoritter"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Fehler", "error_title": "Fehler",
"login_title": "Anmelden", "login_title": "Anmelden",
"login_to_title": "Anmelden bei", "login_to_title": "Anmelden bei",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Benutzername", "username_placeholder": "Benutzername",
"password_placeholder": "Passwort", "password_placeholder": "Passwort",
"login_button": "Anmelden", "login_button": "Anmelden",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} Konten", "accounts_count": "{{count}} Konten",
"select_account": "Konto auswählen", "select_account": "Konto auswählen",
"add_account": "Konto hinzufügen", "add_account": "Konto hinzufügen",
"remove_account_description": "Hiermit werden die gespeicherten Zugangsdaten für {{username}} entfernt.", "remove_account_description": "Hiermit werden die gespeicherten Zugangsdaten für {{username}} entfernt."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Konto speichern", "title": "Konto speichern",
@@ -95,7 +86,6 @@
"oops": "Ups!", "oops": "Ups!",
"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",
"continue": "Continue",
"next_up": "Als nächstes", "next_up": "Als nächstes",
"continue_and_next_up": "\"Weiterschauen\" und \"Als Nächstes\"", "continue_and_next_up": "\"Weiterschauen\" und \"Als Nächstes\"",
"recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}", "recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Einstellungen", "settings_title": "Einstellungen",
"log_out_button": "Abmelden", "log_out_button": "Abmelden",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Kategorien" "title": "Kategorien"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Aussehen", "title": "Aussehen",
"merge_next_up_continue_watching": "\"Weiterschauen\" und \"Als Nächstes\" kombinieren", "merge_next_up_continue_watching": "\"Weiterschauen\" und \"Als Nächstes\" kombinieren",
"hide_remote_session_button": "Button für Remote-Sitzung ausblenden", "hide_remote_session_button": "Button für Remote-Sitzung ausblenden"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Netzwerk", "title": "Netzwerk",
@@ -199,22 +174,6 @@
"rewind_length": "Rückspullänge", "rewind_length": "Rückspullänge",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Gestensteuerung", "gesture_controls_title": "Gestensteuerung",
"horizontal_swipe_skip": "Horizontal Wischen zum Überspringen", "horizontal_swipe_skip": "Horizontal Wischen zum Überspringen",
@@ -297,23 +256,7 @@
"subtitle_font": "Untertitel-Schriftart", "subtitle_font": "Untertitel-Schriftart",
"ksplayer_title": "KSPlayer Einstellungen", "ksplayer_title": "KSPlayer Einstellungen",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Hardwarebeschleunigung für Video Decoding verwenden. Deaktivieren wenn Wiedergabeprobleme auftreten.", "hardware_decode_description": "Hardwarebeschleunigung für Video Decoding verwenden. Deaktivieren wenn Wiedergabeprobleme auftreten."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Untertitel-Einstellungen", "title": "VLC Untertitel-Einstellungen",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Musik-Cache geleert", "music_cache_cleared": "Musik-Cache geleert",
"delete_all_downloaded_songs": "Alle heruntergeladenen Titel löschen", "delete_all_downloaded_songs": "Alle heruntergeladenen Titel löschen",
"downloaded_songs_size": "{{size}} heruntergeladen", "downloaded_songs_size": "{{size}} heruntergeladen",
"downloaded_songs_deleted": "Heruntergeladene Titel gelöscht", "downloaded_songs_deleted": "Heruntergeladene Titel gelöscht"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Einführung", "title": "Einführung",
@@ -493,21 +430,6 @@
"error_deleting_files": "Fehler beim Löschen von Dateien", "error_deleting_files": "Fehler beim Löschen von Dateien",
"background_downloads_enabled": "Hintergrunddownloads aktiviert", "background_downloads_enabled": "Hintergrunddownloads aktiviert",
"background_downloads_disabled": "Hintergrunddownloads deaktiviert" "background_downloads_disabled": "Hintergrunddownloads deaktiviert"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -571,7 +493,6 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Auswählen", "select": "Auswählen",
"no_trailer_available": "Kein Trailer verfügbar", "no_trailer_available": "Kein Trailer verfügbar",
"video": "Video", "video": "Video",
@@ -583,16 +504,13 @@
"none": "Keine", "none": "Keine",
"track": "Spur", "track": "Spur",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"stop": "Stop",
"delete": "Löschen", "delete": "Löschen",
"ok": "OK", "ok": "OK",
"remove": "Entfernen", "remove": "Entfernen",
"next": "Weiter", "next": "Weiter",
"back": "Zurück", "back": "Zurück",
"continue": "Fortsetzen", "continue": "Fortsetzen",
"verifying": "Verifiziere...", "verifying": "Verifiziere..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Suchen...", "search": "Suchen...",
@@ -641,7 +559,6 @@
"movies": "Filme", "movies": "Filme",
"series": "Serien", "series": "Serien",
"boxsets": "Boxsets", "boxsets": "Boxsets",
"playlists": "Playlists",
"items": "Elemente" "items": "Elemente"
}, },
"options": { "options": {
@@ -652,8 +569,7 @@
"poster": "Poster", "poster": "Poster",
"cover": "Cover", "cover": "Cover",
"show_titles": "Titel anzeigen", "show_titles": "Titel anzeigen",
"show_stats": "Statistiken anzeigen", "show_stats": "Statistiken anzeigen"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genres", "genres": "Genres",
@@ -661,11 +577,7 @@
"sort_by": "Sortieren nach", "sort_by": "Sortieren nach",
"filter_by": "Filtern nach", "filter_by": "Filtern nach",
"sort_order": "Sortierreihenfolge", "sort_order": "Sortierreihenfolge",
"tags": "Tags", "tags": "Tags"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +594,6 @@
"no_links": "Keine Links" "no_links": "Keine Links"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV 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. Logs in den Einstellungen überprüfen.",
@@ -702,34 +612,7 @@
"downloaded_file_yes": "Ja", "downloaded_file_yes": "Ja",
"downloaded_file_no": "Nein", "downloaded_file_no": "Nein",
"downloaded_file_cancel": "Abbrechen", "downloaded_file_cancel": "Abbrechen",
"swipe_down_settings": "Swipe down for settings", "ends_at": "Endet um {{time}}"
"ends_at": "Endet um {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Als Nächstes", "next_up": "Als Nächstes",
@@ -738,11 +621,6 @@
"series": "Serien", "series": "Serien",
"seasons": "Staffeln", "seasons": "Staffeln",
"season": "Staffel", "season": "Staffel",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Keine Episoden für diese Staffel", "no_episodes_for_this_season": "Keine Episoden für diese Staffel",
"overview": "Überblick", "overview": "Überblick",
"more_with": "Mehr mit {{name}}", "more_with": "Mehr mit {{name}}",
@@ -753,21 +631,10 @@
"media_options": "Medienoptionen", "media_options": "Medienoptionen",
"quality": "Qualität", "quality": "Qualität",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": "Untertitel",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Mehr anzeigen", "show_more": "Mehr anzeigen",
"show_less": "Weniger anzeigen", "show_less": "Weniger anzeigen",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Erschien in", "appeared_in": "Erschien in",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Konnte Element nicht laden", "could_not_load_item": "Konnte Element nicht laden",
"none": "Keine", "none": "Keine",
"download": { "download": {
@@ -778,13 +645,7 @@
"download_x_item": "{{item_count}} Elemente herunterladen", "download_x_item": "{{item_count}} Elemente herunterladen",
"download_unwatched_only": "Nur Ungesehene", "download_unwatched_only": "Nur Ungesehene",
"download_button": "Herunterladen" "download_button": "Herunterladen"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Nächste", "next": "Nächste",
@@ -795,18 +656,7 @@
"movies": "Filme", "movies": "Filme",
"sports": "Sport", "sports": "Sport",
"for_kids": "Für Kinder", "for_kids": "Für Kinder",
"news": "Nachrichten", "news": "Nachrichten"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Bestätigen", "confirm": "Bestätigen",
@@ -851,12 +701,6 @@
"decline": "Ablehnen", "decline": "Ablehnen",
"requested_by": "Angefragt von {{user}}", "requested_by": "Angefragt von {{user}}",
"unknown_user": "Unbekannter Nutzer", "unknown_user": "Unbekannter Nutzer",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"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": "Seerr-Server erfüllt nicht die minimalen Versionsanforderungen. Bitte den Seerr-Server auf mindestens 2.0.0 aktualisieren.",
"jellyseerr_test_failed": "Seerr-Test fehlgeschlagen. Bitte erneut versuchen.", "jellyseerr_test_failed": "Seerr-Test fehlgeschlagen. Bitte erneut versuchen.",
@@ -876,8 +720,7 @@
"search": "Suche", "search": "Suche",
"library": "Bibliothek", "library": "Bibliothek",
"custom_links": "Links", "custom_links": "Links",
"favorites": "Favoriten", "favorites": "Favoriten"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Musik", "title": "Musik",
@@ -1002,36 +845,5 @@
"show": "Nur diese Serie", "show": "Nur diese Serie",
"all": "Alle (Standard)" "all": "Alle (Standard)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Σφάλμα", "error_title": "Σφάλμα",
"login_title": "Σύνδεση", "login_title": "Σύνδεση",
"login_to_title": "Συνδεθείτε στο", "login_to_title": "Συνδεθείτε στο",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Όνομα Χρήστη", "username_placeholder": "Όνομα Χρήστη",
"password_placeholder": "Κωδικός", "password_placeholder": "Κωδικός",
"login_button": "Σύνδεση", "login_button": "Σύνδεση",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Ωχ!", "oops": "Ωχ!",
"error_message": "Something went wrong.\nPlease log out and in again.", "error_message": "Something went wrong.\nPlease log out and in again.",
"continue_watching": "Συνέχεια Παρακολούθησης", "continue_watching": "Συνέχεια Παρακολούθησης",
"continue": "Continue",
"next_up": "Επόμενο Επάνω", "next_up": "Επόμενο Επάνω",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Προστέθηκε πρόσφατα στο {{libraryName}}", "recently_added_in": "Προστέθηκε πρόσφατα στο {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Ρυθμίσεις", "settings_title": "Ρυθμίσεις",
"log_out_button": "Αποσύνδεση", "log_out_button": "Αποσύνδεση",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categories" "title": "Categories"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Appearance", "title": "Appearance",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Επαναφορά Μήκους", "rewind_length": "Επαναφορά Μήκους",
"seconds_unit": "ίνα" "seconds_unit": "ίνα"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Έλεγχοι Χειρονομιών", "gesture_controls_title": "Έλεγχοι Χειρονομιών",
"horizontal_swipe_skip": "Οριζόντια σάρωση για παράλειψη", "horizontal_swipe_skip": "Οριζόντια σάρωση για παράλειψη",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Σφάλμα Διαγραφής Αρχείων", "error_deleting_files": "Σφάλμα Διαγραφής Αρχείων",
"background_downloads_enabled": "Οι λήψεις στο παρασκήνιο ενεργοποιήθηκαν", "background_downloads_enabled": "Οι λήψεις στο παρασκήνιο ενεργοποιήθηκαν",
"background_downloads_disabled": "Οι λήψεις παρασκηνίου απενεργοποιήθηκαν" "background_downloads_disabled": "Οι λήψεις παρασκηνίου απενεργοποιήθηκαν"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Η νέα ενημέρωση απαιτεί λήψη περιεχομένου ξανά. Καταργήστε όλο το κατεβασμένο περιεχόμενο και προσπαθήστε ξανά.", "new_app_version_requires_re_download_description": "Η νέα ενημέρωση απαιτεί λήψη περιεχομένου ξανά. Καταργήστε όλο το κατεβασμένο περιεχόμενο και προσπαθήστε ξανά.",
"back": "Πίσω", "back": "Πίσω",
"delete": "Διαγραφή", "delete": "Διαγραφή",
"delete_download": "Delete Download",
"something_went_wrong": "Κάτι Πήγε Λάθος", "something_went_wrong": "Κάτι Πήγε Λάθος",
"could_not_get_stream_url_from_jellyfin": "Αδυναμία λήψης του URL ροής από το Jellyfin", "could_not_get_stream_url_from_jellyfin": "Αδυναμία λήψης του URL ροής από το Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Επιλογή", "select": "Επιλογή",
"no_trailer_available": "Δεν υπάρχει διαθέσιμο ρυμουλκούμενο", "no_trailer_available": "Δεν υπάρχει διαθέσιμο ρυμουλκούμενο",
"video": "Βίντεο", "video": "Βίντεο",
"audio": "Ήχος", "audio": "Ήχος",
"subtitle": "Υπότιτλος", "subtitle": "Υπότιτλος",
"play": "Αναπαραγωγή", "play": "Αναπαραγωγή",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Αναζήτηση...", "search": "Αναζήτηση...",
@@ -641,7 +556,6 @@
"movies": "Ταινίες", "movies": "Ταινίες",
"series": "Σειρά", "series": "Σειρά",
"boxsets": "Σύνολα Πλαισίων", "boxsets": "Σύνολα Πλαισίων",
"playlists": "Playlists",
"items": "Στοιχεία" "items": "Στοιχεία"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Αφίσα", "poster": "Αφίσα",
"cover": "Εξώφυλλο", "cover": "Εξώφυλλο",
"show_titles": "Εμφάνιση Τίτλων", "show_titles": "Εμφάνιση Τίτλων",
"show_stats": "Εμφάνιση Στατιστικών", "show_stats": "Εμφάνιση Στατιστικών"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genres", "genres": "Genres",
@@ -661,11 +574,7 @@
"sort_by": "Ταξινόμηση Κατά", "sort_by": "Ταξινόμηση Κατά",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Σειρά Ταξινόμησης", "sort_order": "Σειρά Ταξινόμησης",
"tags": "Ετικέτες", "tags": "Ετικέτες"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Δεν Υπάρχουν Σύνδεσμοι" "no_links": "Δεν Υπάρχουν Σύνδεσμοι"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Σφάλμα", "error": "Σφάλμα",
"failed_to_get_stream_url": "Αποτυχία λήψης του URL ροής", "failed_to_get_stream_url": "Αποτυχία λήψης του URL ροής",
"an_error_occured_while_playing_the_video": "Παρουσιάστηκε σφάλμα κατά την αναπαραγωγή του βίντεο. Ελέγξτε τα αρχεία καταγραφής στις ρυθμίσεις.", "an_error_occured_while_playing_the_video": "Παρουσιάστηκε σφάλμα κατά την αναπαραγωγή του βίντεο. Ελέγξτε τα αρχεία καταγραφής στις ρυθμίσεις.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Επόμενο Επάνω", "next_up": "Επόμενο Επάνω",
@@ -738,11 +617,6 @@
"series": "Σειρά", "series": "Σειρά",
"seasons": "Περίοδοι", "seasons": "Περίοδοι",
"season": "Σεζόν", "season": "Σεζόν",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Δεν υπάρχουν επεισόδια για αυτή τη σεζόν", "no_episodes_for_this_season": "Δεν υπάρχουν επεισόδια για αυτή τη σεζόν",
"overview": "Επισκόπηση", "overview": "Επισκόπηση",
"more_with": "More with {{name}}", "more_with": "More with {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Media Options", "media_options": "Media Options",
"quality": "Ποιότητα", "quality": "Ποιότητα",
"audio": "Ήχος", "audio": "Ήχος",
"subtitles": { "subtitles": "Υπότιτλος",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Εμφάνιση Περισσότερων", "show_more": "Εμφάνιση Περισσότερων",
"show_less": "Εμφάνιση Λιγότερων", "show_less": "Εμφάνιση Λιγότερων",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Εμφανίστηκε Σε", "appeared_in": "Εμφανίστηκε Σε",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Αδύνατη Η Φόρτωση Του Στοιχείου", "could_not_load_item": "Αδύνατη Η Φόρτωση Του Στοιχείου",
"none": "Κανένα", "none": "Κανένα",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Λήψη Αντικειμένων {{item_count}}", "download_x_item": "Λήψη Αντικειμένων {{item_count}}",
"download_unwatched_only": "Μόνο Χωρίς Παρακολούθηση", "download_unwatched_only": "Μόνο Χωρίς Παρακολούθηση",
"download_button": "Λήψη" "download_button": "Λήψη"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Επόμενο", "next": "Επόμενο",
@@ -795,18 +652,7 @@
"movies": "Ταινίες", "movies": "Ταινίες",
"sports": "Αθλητισμός", "sports": "Αθλητισμός",
"for_kids": "Για Παιδιά", "for_kids": "Για Παιδιά",
"news": "Νέα", "news": "Νέα"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Επιβεβαίωση", "confirm": "Επιβεβαίωση",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Ο διακομιστής Seerr δεν πληροί τις ελάχιστες απαιτήσεις έκδοσης! Παρακαλούμε ενημερώστε τουλάχιστον σε 2.0.0", "jellyseer_does_not_meet_requirements": "Ο διακομιστής Seerr δεν πληροί τις ελάχιστες απαιτήσεις έκδοσης! Παρακαλούμε ενημερώστε τουλάχιστον σε 2.0.0",
"jellyseerr_test_failed": "Ο έλεγχος Seerr απέτυχε. Παρακαλώ προσπαθήστε ξανά.", "jellyseerr_test_failed": "Ο έλεγχος Seerr απέτυχε. Παρακαλώ προσπαθήστε ξανά.",
@@ -876,8 +716,7 @@
"search": "Αναζήτηση", "search": "Αναζήτηση",
"library": "Βιβλιοθήκη", "library": "Βιβλιοθήκη",
"custom_links": "Προσαρμοσμένοι Σύνδεσμοι", "custom_links": "Προσαρμοσμένοι Σύνδεσμοι",
"favorites": "Αγαπημένα", "favorites": "Αγαπημένα"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -652,8 +652,7 @@
"poster": "Poster", "poster": "Poster",
"cover": "Cover", "cover": "Cover",
"show_titles": "Show Titles", "show_titles": "Show Titles",
"show_stats": "Show Stats", "show_stats": "Show Stats"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genres", "genres": "Genres",
@@ -683,7 +682,6 @@
}, },
"player": { "player": {
"live": "LIVE", "live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Error", "error": "Error",
"failed_to_get_stream_url": "Failed to get the stream URL", "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.", "an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
@@ -1003,30 +1001,6 @@
"all": "All media (default)" "all": "All media (default)"
} }
}, },
"syncplay": {
"title": "SyncPlay",
"my_group": "My Group",
"join_group": "Join Group",
"leave_group": "Leave Group",
"create_group": "Create Group",
"create_new_group": "Create New Group",
"available_groups": "Available Groups",
"group_id": "Group ID",
"leader": "Leader",
"members": "members",
"enabled": "SyncPlay enabled",
"disabled": "SyncPlay disabled",
"user_joined": "{{username}} joined the group",
"user_left": "{{username}} left the group",
"permission_required": "Permission required to use SyncPlay",
"group_does_not_exist": "Group does not exist",
"create_denied": "Permission denied to create group",
"join_denied": "Permission denied to join group",
"library_access_denied": "Library access denied",
"waiting_for_group": "Waiting for group...",
"joining_playback": "Joining group playback...",
"failed_to_start": "Failed to start SyncPlay group playback"
},
"companion_login": { "companion_login": {
"title": "Pair with TV", "title": "Pair with TV",
"align_qr": "Align the QR code within the frame", "align_qr": "Align the QR code within the frame",

View File

@@ -4,9 +4,6 @@
"error_title": "Error", "error_title": "Error",
"login_title": "Iniciar sesión", "login_title": "Iniciar sesión",
"login_to_title": "Iniciar sesión en", "login_to_title": "Iniciar sesión en",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Nombre de usuario", "username_placeholder": "Nombre de usuario",
"password_placeholder": "Contraseña", "password_placeholder": "Contraseña",
"login_button": "Iniciar sesión", "login_button": "Iniciar sesión",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} cuentas", "accounts_count": "{{count}} cuentas",
"select_account": "Seleccione una cuenta", "select_account": "Seleccione una cuenta",
"add_account": "Añadir cuenta", "add_account": "Añadir cuenta",
"remove_account_description": "Esto eliminará las credenciales guardadas para {{username}}.", "remove_account_description": "Esto eliminará las credenciales guardadas para {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Guardar Cuenta", "title": "Guardar Cuenta",
@@ -95,7 +86,6 @@
"oops": "¡Vaya!", "oops": "¡Vaya!",
"error_message": "Algo ha salido mal.\nPor favor, cierra la sesión y vuelve a iniciar.", "error_message": "Algo ha salido mal.\nPor favor, cierra la sesión y vuelve a iniciar.",
"continue_watching": "Seguir viendo", "continue_watching": "Seguir viendo",
"continue": "Continue",
"next_up": "A continuación", "next_up": "A continuación",
"continue_and_next_up": "Continuar y siguiente", "continue_and_next_up": "Continuar y siguiente",
"recently_added_in": "Recientemente añadido en {{libraryName}}", "recently_added_in": "Recientemente añadido en {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Configuración", "settings_title": "Configuración",
"log_out_button": "Cerrar sesión", "log_out_button": "Cerrar sesión",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categorías" "title": "Categorías"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Apariencia", "title": "Apariencia",
"merge_next_up_continue_watching": "Fusionar continuar viendo y siguiente", "merge_next_up_continue_watching": "Fusionar continuar viendo y siguiente",
"hide_remote_session_button": "Ocultar botón de sesión remota", "hide_remote_session_button": "Ocultar botón de sesión remota"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Cadena", "title": "Cadena",
@@ -199,22 +174,6 @@
"rewind_length": "Longitud de retroceso", "rewind_length": "Longitud de retroceso",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Controles de gestos", "gesture_controls_title": "Controles de gestos",
"horizontal_swipe_skip": "Deslizar horizontal para omitir", "horizontal_swipe_skip": "Deslizar horizontal para omitir",
@@ -297,23 +256,7 @@
"subtitle_font": "Fuente de los subtítulos", "subtitle_font": "Fuente de los subtítulos",
"ksplayer_title": "Ajustes de KSPlayer", "ksplayer_title": "Ajustes de KSPlayer",
"hardware_decode": "Decodificación de hardware", "hardware_decode": "Decodificación de hardware",
"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."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "Configuración de subtítulos VLC", "title": "Configuración de subtítulos VLC",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Caché de música eliminado", "music_cache_cleared": "Caché de música eliminado",
"delete_all_downloaded_songs": "Eliminar todas las descargas", "delete_all_downloaded_songs": "Eliminar todas las descargas",
"downloaded_songs_size": "{{tamaño}} descargado", "downloaded_songs_size": "{{tamaño}} descargado",
"downloaded_songs_deleted": "Canciones descargadas eliminadas", "downloaded_songs_deleted": "Canciones descargadas eliminadas"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Error al eliminar archivos", "error_deleting_files": "Error al eliminar archivos",
"background_downloads_enabled": "Descargas en segundo plano habilitadas", "background_downloads_enabled": "Descargas en segundo plano habilitadas",
"background_downloads_disabled": "Descargas en segundo plano deshabilitadas" "background_downloads_disabled": "Descargas en segundo plano deshabilitadas"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "La nueva actualización requiere volver a descargar el contenido. Por favor, elimina todo el código descargado y vuélvelo a intentar.", "new_app_version_requires_re_download_description": "La nueva actualización requiere volver a descargar el contenido. Por favor, elimina todo el código descargado y vuélvelo a intentar.",
"back": "Atrás", "back": "Atrás",
"delete": "Borrar", "delete": "Borrar",
"delete_download": "Delete Download",
"something_went_wrong": "Algo ha salido mal", "something_went_wrong": "Algo ha salido mal",
"could_not_get_stream_url_from_jellyfin": "No se pudo obtener la URL del stream de Jellyfin", "could_not_get_stream_url_from_jellyfin": "No se pudo obtener la URL del stream de Jellyfin",
"eta": "{{eta}} restante", "eta": "{{eta}} restante",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Seleccionar", "select": "Seleccionar",
"no_trailer_available": "No hay tráiler disponible", "no_trailer_available": "No hay tráiler disponible",
"video": "Vídeo", "video": "Vídeo",
"audio": "Audio", "audio": "Audio",
"subtitle": "Subtítulos", "subtitle": "Subtítulos",
"play": "Jugar", "play": "Jugar",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Nada", "none": "Nada",
"track": "Pista", "track": "Pista",
"cancel": "Cancelar", "cancel": "Cancelar",
"stop": "Stop",
"delete": "Borrar", "delete": "Borrar",
"ok": "Aceptar", "ok": "Aceptar",
"remove": "Eliminar", "remove": "Eliminar",
"next": "Siguiente", "next": "Siguiente",
"back": "Atrás", "back": "Atrás",
"continue": "Continuar", "continue": "Continuar",
"verifying": "Verificando...", "verifying": "Verificando..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Buscar...", "search": "Buscar...",
@@ -641,7 +556,6 @@
"movies": "Películas", "movies": "Películas",
"series": "Series", "series": "Series",
"boxsets": "Colecciones", "boxsets": "Colecciones",
"playlists": "Playlists",
"items": "Elementos" "items": "Elementos"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Póster", "poster": "Póster",
"cover": "Portada", "cover": "Portada",
"show_titles": "Mostrar títulos", "show_titles": "Mostrar títulos",
"show_stats": "Mostrar estadísticas", "show_stats": "Mostrar estadísticas"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Géneros", "genres": "Géneros",
@@ -661,11 +574,7 @@
"sort_by": "Ordenar por", "sort_by": "Ordenar por",
"filter_by": "Filtrar por", "filter_by": "Filtrar por",
"sort_order": "Ordenar", "sort_order": "Ordenar",
"tags": "Etiquetas", "tags": "Etiquetas"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Sin enlaces" "no_links": "Sin enlaces"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Error", "error": "Error",
"failed_to_get_stream_url": "Error al obtener la URL del Steam", "failed_to_get_stream_url": "Error al obtener la URL del Steam",
"an_error_occured_while_playing_the_video": "Ha ocurrido un error al reproducir el vídeo. Comprueba los registros en la configuración.", "an_error_occured_while_playing_the_video": "Ha ocurrido un error al reproducir el vídeo. Comprueba los registros en la configuración.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "¿Quieres reproducir el archivo descargado?", "downloaded_file_message": "¿Quieres reproducir el archivo descargado?",
"downloaded_file_yes": "Sí", "downloaded_file_yes": "Sí",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancelar", "downloaded_file_cancel": "Cancelar"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "A continuación", "next_up": "A continuación",
@@ -738,11 +617,6 @@
"series": "Series", "series": "Series",
"seasons": "Temporadas", "seasons": "Temporadas",
"season": "Temporada", "season": "Temporada",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "No hay episodios para esta temporada", "no_episodes_for_this_season": "No hay episodios para esta temporada",
"overview": "Resumen", "overview": "Resumen",
"more_with": "Más con {{name}}", "more_with": "Más con {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Opciones de medios", "media_options": "Opciones de medios",
"quality": "Calidad", "quality": "Calidad",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": "Subtítulos",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Mostrar más", "show_more": "Mostrar más",
"show_less": "Mostrar menos", "show_less": "Mostrar menos",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Apareció en", "appeared_in": "Apareció en",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "No se pudo cargar el ítem", "could_not_load_item": "No se pudo cargar el ítem",
"none": "Ninguno", "none": "Ninguno",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Descargar {{item_count}} ítems", "download_x_item": "Descargar {{item_count}} ítems",
"download_unwatched_only": "No visto", "download_unwatched_only": "No visto",
"download_button": "Descargar" "download_button": "Descargar"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Siguiente", "next": "Siguiente",
@@ -795,18 +652,7 @@
"movies": "Películas", "movies": "Películas",
"sports": "Deportes", "sports": "Deportes",
"for_kids": "Para niños", "for_kids": "Para niños",
"news": "Noticias", "news": "Noticias"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Confirmar", "confirm": "Confirmar",
@@ -851,12 +697,6 @@
"decline": "Rechazar", "decline": "Rechazar",
"requested_by": "Solicitado por {{user}}", "requested_by": "Solicitado por {{user}}",
"unknown_user": "Usuario desconocido", "unknown_user": "Usuario desconocido",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "¡Jellyseer no cumple con los requisitos! Por favor, actualízalo al menos a la versión 2.0.0.", "jellyseer_does_not_meet_requirements": "¡Jellyseer no cumple con los requisitos! Por favor, actualízalo al menos a la versión 2.0.0.",
"jellyseerr_test_failed": "La prueba de Jellyseerr ha fallado. Por favor inténtalo de nuevo.", "jellyseerr_test_failed": "La prueba de Jellyseerr ha fallado. Por favor inténtalo de nuevo.",
@@ -876,8 +716,7 @@
"search": "Buscar", "search": "Buscar",
"library": "Bibliotecas", "library": "Bibliotecas",
"custom_links": "Enlaces personalizados", "custom_links": "Enlaces personalizados",
"favorites": "Favoritos", "favorites": "Favoritos"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Música", "title": "Música",
@@ -1002,36 +841,5 @@
"show": "Este programa", "show": "Este programa",
"all": "Todos los medios (por defecto)" "all": "Todos los medios (por defecto)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Virhe", "error_title": "Virhe",
"login_title": "Kirjaudu sisään", "login_title": "Kirjaudu sisään",
"login_to_title": "Kirjaudu sisään palveluun", "login_to_title": "Kirjaudu sisään palveluun",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Käyttäjätunnus", "username_placeholder": "Käyttäjätunnus",
"password_placeholder": "Salasana", "password_placeholder": "Salasana",
"login_button": "Kirjaudu sisään", "login_button": "Kirjaudu sisään",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Ups!", "oops": "Ups!",
"error_message": "Jotain meni pieleen.\nKirjaudu ulos ja takaisin sisään.", "error_message": "Jotain meni pieleen.\nKirjaudu ulos ja takaisin sisään.",
"continue_watching": "Jatka katsomista", "continue_watching": "Jatka katsomista",
"continue": "Continue",
"next_up": "Seuraavaksi", "next_up": "Seuraavaksi",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Äskettäin lisätty {{libraryName}}-kirjastoon", "recently_added_in": "Äskettäin lisätty {{libraryName}}-kirjastoon",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Asetukset", "settings_title": "Asetukset",
"log_out_button": "Kirjaudu ulos", "log_out_button": "Kirjaudu ulos",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Kategoriat" "title": "Kategoriat"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Ulkoasu", "title": "Ulkoasu",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Taaksepäin hyppäämisen pituus", "rewind_length": "Taaksepäin hyppäämisen pituus",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Ele Ohjaus", "gesture_controls_title": "Ele Ohjaus",
"horizontal_swipe_skip": "Ohita vaakatasossa pyyhkäisemällä", "horizontal_swipe_skip": "Ohita vaakatasossa pyyhkäisemällä",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Esittely", "title": "Esittely",
@@ -493,21 +430,6 @@
"error_deleting_files": "Virhe tiedostojen poistamisessa", "error_deleting_files": "Virhe tiedostojen poistamisessa",
"background_downloads_enabled": "Taustalataukset käytössä", "background_downloads_enabled": "Taustalataukset käytössä",
"background_downloads_disabled": "Taustalataukset pois käytöstä" "background_downloads_disabled": "Taustalataukset pois käytöstä"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Uusi päivitys vaatii sisällön lataamista uudelleen. Poista kaikki ladattu sisältö ja yritä uudelleen.", "new_app_version_requires_re_download_description": "Uusi päivitys vaatii sisällön lataamista uudelleen. Poista kaikki ladattu sisältö ja yritä uudelleen.",
"back": "Takaisin", "back": "Takaisin",
"delete": "Poista", "delete": "Poista",
"delete_download": "Delete Download",
"something_went_wrong": "Jotain meni pieleen", "something_went_wrong": "Jotain meni pieleen",
"could_not_get_stream_url_from_jellyfin": "Ei voitu saada suoratoiston URL:ia Jellyfinilta", "could_not_get_stream_url_from_jellyfin": "Ei voitu saada suoratoiston URL:ia Jellyfinilta",
"eta": "Arvio {{eta}}", "eta": "Arvio {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Valitse", "select": "Valitse",
"no_trailer_available": "Perävaunua ei saatavilla", "no_trailer_available": "Perävaunua ei saatavilla",
"video": "Video", "video": "Video",
"audio": "Ääni", "audio": "Ääni",
"subtitle": "Tekstitys", "subtitle": "Tekstitys",
"play": "Toista", "play": "Toista",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Ei mitään", "none": "Ei mitään",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Haku...", "search": "Haku...",
@@ -641,7 +556,6 @@
"movies": "elokuvat", "movies": "elokuvat",
"series": "sarjat", "series": "sarjat",
"boxsets": "bokset", "boxsets": "bokset",
"playlists": "Playlists",
"items": "kohteet" "items": "kohteet"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Juliste", "poster": "Juliste",
"cover": "Kansi", "cover": "Kansi",
"show_titles": "Näytä otsikot", "show_titles": "Näytä otsikot",
"show_stats": "Näytä tilastot", "show_stats": "Näytä tilastot"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genret", "genres": "Genret",
@@ -661,11 +574,7 @@
"sort_by": "Lajittele", "sort_by": "Lajittele",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Lajittelujärjestys", "sort_order": "Lajittelujärjestys",
"tags": "Tunnisteet", "tags": "Tunnisteet"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Ei Linkkejä" "no_links": "Ei Linkkejä"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Virhe", "error": "Virhe",
"failed_to_get_stream_url": "Lähetyksen URL-osoitteen haku epäonnistui", "failed_to_get_stream_url": "Lähetyksen URL-osoitteen haku epäonnistui",
"an_error_occured_while_playing_the_video": "Videon toiston yhteydessä tapahtui virhe. Tarkista lokit asetuksista.", "an_error_occured_while_playing_the_video": "Videon toiston yhteydessä tapahtui virhe. Tarkista lokit asetuksista.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Haluatko toistaa ladatun tiedoston?", "downloaded_file_message": "Haluatko toistaa ladatun tiedoston?",
"downloaded_file_yes": "Kyllä", "downloaded_file_yes": "Kyllä",
"downloaded_file_no": "Ei", "downloaded_file_no": "Ei",
"downloaded_file_cancel": "Peruuta", "downloaded_file_cancel": "Peruuta"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Seuraavaksi", "next_up": "Seuraavaksi",
@@ -738,11 +617,6 @@
"series": "Sarjat", "series": "Sarjat",
"seasons": "Kaudet", "seasons": "Kaudet",
"season": "Kausi", "season": "Kausi",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Ei jaksoja tälle kaudelle", "no_episodes_for_this_season": "Ei jaksoja tälle kaudelle",
"overview": "Yleiskatsaus", "overview": "Yleiskatsaus",
"more_with": "Enemmän {{name}} kanssa", "more_with": "Enemmän {{name}} kanssa",
@@ -753,21 +627,10 @@
"media_options": "Media-asetukset", "media_options": "Media-asetukset",
"quality": "Laatu", "quality": "Laatu",
"audio": "Ääni", "audio": "Ääni",
"subtitles": { "subtitles": "Tekstitys",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Näytä Lisää", "show_more": "Näytä Lisää",
"show_less": "Näytä Vähemmän", "show_less": "Näytä Vähemmän",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Esiintyy Sisään", "appeared_in": "Esiintyy Sisään",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Kohdetta Ei Voitu Ladata", "could_not_load_item": "Kohdetta Ei Voitu Ladata",
"none": "Ei mitään", "none": "Ei mitään",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Lataa {{item_count}} Kohteita", "download_x_item": "Lataa {{item_count}} Kohteita",
"download_unwatched_only": "Vain Katsomattomat", "download_unwatched_only": "Vain Katsomattomat",
"download_button": "Lataa" "download_button": "Lataa"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Seuraava", "next": "Seuraava",
@@ -795,18 +652,7 @@
"movies": "Elokuvat", "movies": "Elokuvat",
"sports": "Urheilu", "sports": "Urheilu",
"for_kids": "Lapsille", "for_kids": "Lapsille",
"news": "Uutiset", "news": "Uutiset"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Vahvista", "confirm": "Vahvista",
@@ -851,12 +697,6 @@
"decline": "Hylkää", "decline": "Hylkää",
"requested_by": "Käyttäjän {{user}} pyynnöstä", "requested_by": "Käyttäjän {{user}} pyynnöstä",
"unknown_user": "Tuntematon käyttäjä", "unknown_user": "Tuntematon käyttäjä",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Jellyseerr-palvelin ei täytä vähimmäisversiovaatimuksia! Päivitä vähintään versioon 2.0.0", "jellyseer_does_not_meet_requirements": "Jellyseerr-palvelin ei täytä vähimmäisversiovaatimuksia! Päivitä vähintään versioon 2.0.0",
"jellyseerr_test_failed": "Jellyseerr-testi epäonnistui. Yritä uudelleen.", "jellyseerr_test_failed": "Jellyseerr-testi epäonnistui. Yritä uudelleen.",
@@ -876,8 +716,7 @@
"search": "Haku", "search": "Haku",
"library": "Kirjasto", "library": "Kirjasto",
"custom_links": "Mukautetut linkit", "custom_links": "Mukautetut linkit",
"favorites": "Suosikit", "favorites": "Suosikit"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Erreur", "error_title": "Erreur",
"login_title": "Se connecter", "login_title": "Se connecter",
"login_to_title": "Se connecter à", "login_to_title": "Se connecter à",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Nom d'utilisateur", "username_placeholder": "Nom d'utilisateur",
"password_placeholder": "Mot de passe", "password_placeholder": "Mot de passe",
"login_button": "Se connecter", "login_button": "Se connecter",
@@ -45,13 +42,7 @@
"accounts_count": "Comptes {{count}}", "accounts_count": "Comptes {{count}}",
"select_account": "Sélectionnez un compte", "select_account": "Sélectionnez un compte",
"add_account": "Ajouter un compte", "add_account": "Ajouter un compte",
"remove_account_description": "Cela supprimera les identifiants enregistrés pour {{username}}.", "remove_account_description": "Cela supprimera les identifiants enregistrés pour {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Sauvegarder le compte", "title": "Sauvegarder le compte",
@@ -95,7 +86,6 @@
"oops": "Oups!", "oops": "Oups!",
"error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.", "error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.",
"continue_watching": "Continuer à regarder", "continue_watching": "Continuer à regarder",
"continue": "Continue",
"next_up": "À suivre", "next_up": "À suivre",
"continue_and_next_up": "Continuer de regarder et à suivre", "continue_and_next_up": "Continuer de regarder et à suivre",
"recently_added_in": "Ajoutés récemment dans {{libraryName}}", "recently_added_in": "Ajoutés récemment dans {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Paramètres", "settings_title": "Paramètres",
"log_out_button": "Déconnexion", "log_out_button": "Déconnexion",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Catégories" "title": "Catégories"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Apparence", "title": "Apparence",
"merge_next_up_continue_watching": "Fusionner, continuer à regarder et à suivre", "merge_next_up_continue_watching": "Fusionner, continuer à regarder et à suivre",
"hide_remote_session_button": "Masquer le bouton de session distante", "hide_remote_session_button": "Masquer le bouton de session distante"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Réseau", "title": "Réseau",
@@ -199,22 +174,6 @@
"rewind_length": "Durée de retour en arrière", "rewind_length": "Durée de retour en arrière",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Commandes gestuelles", "gesture_controls_title": "Commandes gestuelles",
"horizontal_swipe_skip": "Glisser horizontalement pour passer", "horizontal_swipe_skip": "Glisser horizontalement pour passer",
@@ -297,23 +256,7 @@
"subtitle_font": "Police des sous-titres", "subtitle_font": "Police des sous-titres",
"ksplayer_title": "Paramètres de KSPlayer", "ksplayer_title": "Paramètres de KSPlayer",
"hardware_decode": "Décodage matériel", "hardware_decode": "Décodage matériel",
"hardware_decode_description": "Utilisez laccélération matérielle pour le décodage vidéo. Désactivez si vous rencontrez des problèmes de lecture.", "hardware_decode_description": "Utilisez laccélération matérielle pour le décodage vidéo. Désactivez si vous rencontrez des problèmes de lecture."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "Paramètres des sous-titres VLC", "title": "Paramètres des sous-titres VLC",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Cache de musique effacé", "music_cache_cleared": "Cache de musique effacé",
"delete_all_downloaded_songs": "Supprimer toutes les musiques téléchargées", "delete_all_downloaded_songs": "Supprimer toutes les musiques téléchargées",
"downloaded_songs_size": "{{size}} téléchargé", "downloaded_songs_size": "{{size}} téléchargé",
"downloaded_songs_deleted": "Chansons téléchargées supprimées", "downloaded_songs_deleted": "Chansons téléchargées supprimées"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Introduction", "title": "Introduction",
@@ -493,21 +430,6 @@
"error_deleting_files": "Erreur lors de la suppression des fichiers", "error_deleting_files": "Erreur lors de la suppression des fichiers",
"background_downloads_enabled": "Téléchargements en arrière-plan activés", "background_downloads_enabled": "Téléchargements en arrière-plan activés",
"background_downloads_disabled": "Téléchargements en arrière-plan désactivés" "background_downloads_disabled": "Téléchargements en arrière-plan désactivés"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "La nouvelle mise à jour nécessite que le contenu soit téléchargé à nouveau. Veuillez supprimer tout le contenu téléchargé et réessayer.", "new_app_version_requires_re_download_description": "La nouvelle mise à jour nécessite que le contenu soit téléchargé à nouveau. Veuillez supprimer tout le contenu téléchargé et réessayer.",
"back": "Retour", "back": "Retour",
"delete": "Supprimer", "delete": "Supprimer",
"delete_download": "Delete Download",
"something_went_wrong": "Quelque chose s'est mal passé", "something_went_wrong": "Quelque chose s'est mal passé",
"could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin", "could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Sélectionner", "select": "Sélectionner",
"no_trailer_available": "Aucune bande-annonce disponible", "no_trailer_available": "Aucune bande-annonce disponible",
"video": "Vidéo", "video": "Vidéo",
"audio": "Audio", "audio": "Audio",
"subtitle": "Sous-titres", "subtitle": "Sous-titres",
"play": "Lecture", "play": "Lecture",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Aucun", "none": "Aucun",
"track": "Suivre", "track": "Suivre",
"cancel": "Annuler", "cancel": "Annuler",
"stop": "Stop",
"delete": "Supprimer", "delete": "Supprimer",
"ok": "Ok", "ok": "Ok",
"remove": "Retirer", "remove": "Retirer",
"next": "Suivant", "next": "Suivant",
"back": "Précédent", "back": "Précédent",
"continue": "Continuer", "continue": "Continuer",
"verifying": "Vérification...", "verifying": "Vérification..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Rechercher...", "search": "Rechercher...",
@@ -641,7 +556,6 @@
"movies": "Films", "movies": "Films",
"series": "Séries", "series": "Séries",
"boxsets": "Coffrets ", "boxsets": "Coffrets ",
"playlists": "Playlists",
"items": "Médias" "items": "Médias"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Affiche", "poster": "Affiche",
"cover": "Couverture", "cover": "Couverture",
"show_titles": "Afficher les titres", "show_titles": "Afficher les titres",
"show_stats": "Afficher les statistiques", "show_stats": "Afficher les statistiques"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genres", "genres": "Genres",
@@ -661,11 +574,7 @@
"sort_by": "Trier par", "sort_by": "Trier par",
"filter_by": "Filtrer par", "filter_by": "Filtrer par",
"sort_order": "Ordre de tri", "sort_order": "Ordre de tri",
"tags": "Tags", "tags": "Tags"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Aucuns liens" "no_links": "Aucuns liens"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"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.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Voulez-vous lire le fichier téléchargé ?", "downloaded_file_message": "Voulez-vous lire le fichier téléchargé ?",
"downloaded_file_yes": "Oui", "downloaded_file_yes": "Oui",
"downloaded_file_no": "Non", "downloaded_file_no": "Non",
"downloaded_file_cancel": "Annuler", "downloaded_file_cancel": "Annuler"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "À suivre", "next_up": "À suivre",
@@ -738,11 +617,6 @@
"series": "Séries", "series": "Séries",
"seasons": "Saisons", "seasons": "Saisons",
"season": "Saison", "season": "Saison",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Aucun épisode pour cette saison", "no_episodes_for_this_season": "Aucun épisode pour cette saison",
"overview": "Aperçu", "overview": "Aperçu",
"more_with": "Plus avec {{name}}", "more_with": "Plus avec {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Options média", "media_options": "Options média",
"quality": "Qualité", "quality": "Qualité",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": "Sous-titres",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Afficher plus", "show_more": "Afficher plus",
"show_less": "Afficher moins", "show_less": "Afficher moins",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Apparu dans", "appeared_in": "Apparu dans",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Impossible de charger le média", "could_not_load_item": "Impossible de charger le média",
"none": "Aucun", "none": "Aucun",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Télécharger {{item_count}} médias", "download_x_item": "Télécharger {{item_count}} médias",
"download_unwatched_only": "Non visionné uniquement", "download_unwatched_only": "Non visionné uniquement",
"download_button": "Télécharger" "download_button": "Télécharger"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Suivant", "next": "Suivant",
@@ -795,18 +652,7 @@
"movies": "Films", "movies": "Films",
"sports": "Sports", "sports": "Sports",
"for_kids": "Pour enfants", "for_kids": "Pour enfants",
"news": "Actualités", "news": "Actualités"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Confirmer", "confirm": "Confirmer",
@@ -851,12 +697,6 @@
"decline": "Refuser", "decline": "Refuser",
"requested_by": "Demandé par {{user}}", "requested_by": "Demandé par {{user}}",
"unknown_user": "Utilisateur inconnu", "unknown_user": "Utilisateur inconnu",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Seerr ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.", "jellyseer_does_not_meet_requirements": "Seerr ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.",
"jellyseerr_test_failed": "Le test Seerr a échoué. Veuillez réessayer.", "jellyseerr_test_failed": "Le test Seerr a échoué. Veuillez réessayer.",
@@ -876,8 +716,7 @@
"search": "Recherche", "search": "Recherche",
"library": "Bibliothèque", "library": "Bibliothèque",
"custom_links": "Liens personnalisés", "custom_links": "Liens personnalisés",
"favorites": "Favoris", "favorites": "Favoris"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Musique", "title": "Musique",
@@ -949,8 +788,8 @@
} }
}, },
"watchlists": { "watchlists": {
"title": "Listes de lecture", "title": "Watchlists",
"my_watchlists": "Mes listes de lecture", "my_watchlists": "My Watchlists",
"public_watchlists": "Watchlist publique", "public_watchlists": "Watchlist publique",
"create_title": "Créer une Watchlist", "create_title": "Créer une Watchlist",
"edit_title": "Modifier la Watchlist", "edit_title": "Modifier la Watchlist",
@@ -963,7 +802,7 @@
"name_placeholder": "Entrer le nom de la playlist", "name_placeholder": "Entrer le nom de la playlist",
"description_label": "Description", "description_label": "Description",
"description_placeholder": "Entrez la description (facultatif)", "description_placeholder": "Entrez la description (facultatif)",
"is_public_label": "Liste de lecture Publique", "is_public_label": "Public Watchlist",
"is_public_description": "Autoriser d'autres personnes à voir cette liste de suivi", "is_public_description": "Autoriser d'autres personnes à voir cette liste de suivi",
"allowed_type_label": "Type de contenu", "allowed_type_label": "Type de contenu",
"sort_order_label": "Ordre de tri par défaut", "sort_order_label": "Ordre de tri par défaut",
@@ -1002,36 +841,5 @@
"show": "Cette série", "show": "Cette série",
"all": "Tous les médias (par défaut)" "all": "Tous les médias (par défaut)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "שגיאה", "error_title": "שגיאה",
"login_title": "התחבר", "login_title": "התחבר",
"login_to_title": "התחבר אל", "login_to_title": "התחבר אל",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "שם משתמש", "username_placeholder": "שם משתמש",
"password_placeholder": "סיסמה", "password_placeholder": "סיסמה",
"login_button": "התחבר", "login_button": "התחבר",
@@ -42,16 +39,10 @@
"please_login_again": "Your saved session has expired. Please log in 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",
"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_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "אופס!", "oops": "אופס!",
"error_message": "קרתה תקלה. אנא התנתק והתחבר מחדש.", "error_message": "קרתה תקלה. אנא התנתק והתחבר מחדש.",
"continue_watching": "המשך לצפות", "continue_watching": "המשך לצפות",
"continue": "Continue",
"next_up": "הבא בתור", "next_up": "הבא בתור",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "התווסף לאחרונה ב-{{libraryName}}", "recently_added_in": "התווסף לאחרונה ב-{{libraryName}}",
@@ -119,34 +109,19 @@
"settings": { "settings": {
"settings_title": "הגדרות", "settings_title": "הגדרות",
"log_out_button": "התנתק", "log_out_button": "התנתק",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "קטגוריות" "title": "Categories"
}, },
"playback_controls": { "playback_controls": {
"title": "Playback & Controls" "title": "Playback & Controls"
}, },
"audio_subtitles": { "audio_subtitles": {
"title": "שמע וכתוביות" "title": "Audio & Subtitles"
}, },
"appearance": { "appearance": {
"title": "מראה", "title": "Appearance",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "אורך הזזה אחורה", "rewind_length": "אורך הזזה אחורה",
"seconds_unit": "שנ'" "seconds_unit": "שנ'"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "פקדי מחוות", "gesture_controls_title": "פקדי מחוות",
"horizontal_swipe_skip": "החלקה אופקית לדילוג", "horizontal_swipe_skip": "החלקה אופקית לדילוג",
@@ -229,7 +188,7 @@
"hide_brightness_slider_description": "Hide the brightness slider in the video player" "hide_brightness_slider_description": "Hide the brightness slider in the video player"
}, },
"audio": { "audio": {
"audio_title": "שמע", "audio_title": "אודיו",
"set_audio_track": "בחר רצועת שמע מהפריט הקודם", "set_audio_track": "בחר רצועת שמע מהפריט הקודם",
"audio_language": "שפת שמע", "audio_language": "שפת שמע",
"audio_hint": "בחר שפת שמע אוטומטית.", "audio_hint": "בחר שפת שמע אוטומטית.",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -328,8 +271,8 @@
"margin": "Bottom Margin" "margin": "Bottom Margin"
}, },
"video_player": { "video_player": {
"title": "נגן וידאו", "title": "Video Player",
"video_player": "נגן וידאו", "video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.", "video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer", "ksplayer": "KSPlayer",
"vlc": "VLC" "vlc": "VLC"
@@ -371,7 +314,7 @@
"downloads_title": "הורדות" "downloads_title": "הורדות"
}, },
"music": { "music": {
"title": "מוזיקה", "title": "Music",
"playback_title": "Playback", "playback_title": "Playback",
"playback_description": "Configure how music is played.", "playback_description": "Configure how music is played.",
"prefer_downloaded": "Prefer Downloaded Songs", "prefer_downloaded": "Prefer Downloaded Songs",
@@ -463,16 +406,10 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "הקדמה", "title": "Intro",
"show_intro": "הצג פתיח", "show_intro": "הצג פתיח",
"reset_intro": "אפס פתיח" "reset_intro": "אפס פתיח"
}, },
@@ -493,21 +430,6 @@
"error_deleting_files": "שגיאה במחיקת קבצים", "error_deleting_files": "שגיאה במחיקת קבצים",
"background_downloads_enabled": "הורדה ברקע מופעלת", "background_downloads_enabled": "הורדה ברקע מופעלת",
"background_downloads_disabled": "הורדה ברקע כבויה" "background_downloads_disabled": "הורדה ברקע כבויה"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "הגרסה החדשה דורשת לתוכן לרדת מחדש. אנא מחק את כל התוכן שכבר הורדת ונסה שוב.", "new_app_version_requires_re_download_description": "הגרסה החדשה דורשת לתוכן לרדת מחדש. אנא מחק את כל התוכן שכבר הורדת ונסה שוב.",
"back": "חזרה", "back": "חזרה",
"delete": "מחק", "delete": "מחק",
"delete_download": "Delete Download",
"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}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "בחר", "select": "בחר",
"no_trailer_available": "אין טריילר זמין", "no_trailer_available": "אין טריילר זמין",
"video": "וידאו", "video": "וידאו",
"audio": "שמע", "audio": "אודיו",
"subtitle": "כתובית", "subtitle": "כתובית",
"play": "נגן", "play": "נגן",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "ללא", "none": "ללא",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "חפש...", "search": "חפש...",
@@ -606,9 +521,9 @@
"episodes": "פרקים", "episodes": "פרקים",
"collections": "אוספים", "collections": "אוספים",
"actors": "שחקנים", "actors": "שחקנים",
"artists": "אומנים", "artists": "Artists",
"albums": "אלבומים", "albums": "Albums",
"songs": "שירים", "songs": "Songs",
"playlists": "Playlists", "playlists": "Playlists",
"request_movies": "סרטים מבוקשים", "request_movies": "סרטים מבוקשים",
"request_series": "סדרות מבוקשים", "request_series": "סדרות מבוקשים",
@@ -641,7 +556,6 @@
"movies": "סרטים", "movies": "סרטים",
"series": "סדרות", "series": "סדרות",
"boxsets": "אוסף", "boxsets": "אוסף",
"playlists": "Playlists",
"items": "פריטים" "items": "פריטים"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "פוסטר", "poster": "פוסטר",
"cover": "עטיפה", "cover": "עטיפה",
"show_titles": "הצג כותרות", "show_titles": "הצג כותרות",
"show_stats": "הצג סטטיסטיקה", "show_stats": "הצג סטטיסטיקה"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "סגנונות", "genres": "סגנונות",
@@ -661,11 +574,7 @@
"sort_by": "מיין לפי", "sort_by": "מיין לפי",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "סדר מיון", "sort_order": "סדר מיון",
"tags": "תגים", "tags": "תגים"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "אין קישורים" "no_links": "אין קישורים"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "שגיאה", "error": "שגיאה",
"failed_to_get_stream_url": "נכשל בהשגת קישור הזרם", "failed_to_get_stream_url": "נכשל בהשגת קישור הזרם",
"an_error_occured_while_playing_the_video": "קרתה תקלה במהלך הניגון של הקובץ. בדוק את הלוגים בהגדרות.", "an_error_occured_while_playing_the_video": "קרתה תקלה במהלך הניגון של הקובץ. בדוק את הלוגים בהגדרות.",
@@ -699,37 +606,9 @@
"go_back": "חזור", "go_back": "חזור",
"downloaded_file_title": "You have this file downloaded", "downloaded_file_title": "You have this file downloaded",
"downloaded_file_message": "Do you want to play the downloaded file?", "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": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "הבא בתור", "next_up": "הבא בתור",
@@ -738,11 +617,6 @@
"series": "סדרות", "series": "סדרות",
"seasons": "עונות", "seasons": "עונות",
"season": "עונה", "season": "עונה",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "אין פרקים בעונה זו", "no_episodes_for_this_season": "אין פרקים בעונה זו",
"overview": "סקירה", "overview": "סקירה",
"more_with": "עוד עם {{name}}", "more_with": "עוד עם {{name}}",
@@ -752,22 +626,11 @@
"more_details": "פרטים נוספים", "more_details": "פרטים נוספים",
"media_options": "Media Options", "media_options": "Media Options",
"quality": "איכות", "quality": "איכות",
"audio": "שמע", "audio": "אודיו",
"subtitles": { "subtitles": "כתובית",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "הצג עוד", "show_more": "הצג עוד",
"show_less": "הצג פחות", "show_less": "הצג פחות",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "הופיע ב-", "appeared_in": "הופיע ב-",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "נכשל בטעינת פריט", "could_not_load_item": "נכשל בטעינת פריט",
"none": "ללא", "none": "ללא",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "הורד {{item_count}} פריטים", "download_x_item": "הורד {{item_count}} פריטים",
"download_unwatched_only": "רק שלא נצפו", "download_unwatched_only": "רק שלא נצפו",
"download_button": "הורד" "download_button": "הורד"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "הבא", "next": "הבא",
@@ -795,18 +652,7 @@
"movies": "סרטים", "movies": "סרטים",
"sports": "ספורט", "sports": "ספורט",
"for_kids": "לילדים", "for_kids": "לילדים",
"news": "חדשות", "news": "חדשות"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "אשר", "confirm": "אשר",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "שרת ה-Seerr לא תואם את הגרסה המינימלית הנרדת! אנא עדכן לפחות לגרסה 2.0.0", "jellyseer_does_not_meet_requirements": "שרת ה-Seerr לא תואם את הגרסה המינימלית הנרדת! אנא עדכן לפחות לגרסה 2.0.0",
"jellyseerr_test_failed": "בדיקת ה-Seerr נכשלה. אנא נסה שוב.", "jellyseerr_test_failed": "בדיקת ה-Seerr נכשלה. אנא נסה שוב.",
@@ -876,14 +716,13 @@
"search": "חיפוש", "search": "חיפוש",
"library": "ספריה", "library": "ספריה",
"custom_links": "קישורים מותאמים אישית", "custom_links": "קישורים מותאמים אישית",
"favorites": "מועדפים", "favorites": "מועדפים"
"settings": "Settings"
}, },
"music": { "music": {
"title": "מוזיקה", "title": "Music",
"tabs": { "tabs": {
"suggestions": "Suggestions", "suggestions": "Suggestions",
"albums": "אלבומים", "albums": "Albums",
"artists": "Artists", "artists": "Artists",
"playlists": "Playlists", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
@@ -959,9 +798,9 @@
"delete_button": "Delete", "delete_button": "Delete",
"remove_button": "Remove", "remove_button": "Remove",
"cancel_button": "Cancel", "cancel_button": "Cancel",
"name_label": "שם", "name_label": "Name",
"name_placeholder": "Enter watchlist name", "name_placeholder": "Enter watchlist name",
"description_label": "תיאור", "description_label": "Description",
"description_placeholder": "Enter description (optional)", "description_placeholder": "Enter description (optional)",
"is_public_label": "Public Watchlist", "is_public_label": "Public Watchlist",
"is_public_description": "Allow others to view this watchlist", "is_public_description": "Allow others to view this watchlist",
@@ -978,10 +817,10 @@
"remove_from_watchlist": "Remove from Watchlist", "remove_from_watchlist": "Remove from Watchlist",
"select_watchlist": "Select Watchlist", "select_watchlist": "Select Watchlist",
"create_new": "Create New Watchlist", "create_new": "Create New Watchlist",
"item": "פריט", "item": "item",
"items": "פריטים", "items": "items",
"public": "ציבורי", "public": "Public",
"private": "פרטי", "private": "Private",
"you": "You", "you": "You",
"by_owner": "By another user", "by_owner": "By another user",
"not_found": "Watchlist not found", "not_found": "Watchlist not found",
@@ -996,42 +835,11 @@
"playback_speed": { "playback_speed": {
"title": "Playback Speed", "title": "Playback Speed",
"apply_to": "Apply To", "apply_to": "Apply To",
"speed": "מהירות", "speed": "Speed",
"scope": { "scope": {
"media": "This media only", "media": "This media only",
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Hiba", "error_title": "Hiba",
"login_title": "Bejelentkezés", "login_title": "Bejelentkezés",
"login_to_title": "Bejelentkezés ide", "login_to_title": "Bejelentkezés ide",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Felhasználónév", "username_placeholder": "Felhasználónév",
"password_placeholder": "Jelszó", "password_placeholder": "Jelszó",
"login_button": "Bejelentkezés", "login_button": "Bejelentkezés",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Hoppá!", "oops": "Hoppá!",
"error_message": "Valami nem stimmel.\nKérjük, jelentkezz ki, majd újra be.", "error_message": "Valami nem stimmel.\nKérjük, jelentkezz ki, majd újra be.",
"continue_watching": "Nézd Tovább", "continue_watching": "Nézd Tovább",
"continue": "Continue",
"next_up": "Következő", "next_up": "Következő",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Új a(z) {{libraryName}} könyvtárban", "recently_added_in": "Új a(z) {{libraryName}} könyvtárban",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Beállítások", "settings_title": "Beállítások",
"log_out_button": "Kijelentkezés", "log_out_button": "Kijelentkezés",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categories" "title": "Categories"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Appearance", "title": "Appearance",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Visszatekerés Hossza", "rewind_length": "Visszatekerés Hossza",
"seconds_unit": "mp" "seconds_unit": "mp"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Gesztusvezérlés", "gesture_controls_title": "Gesztusvezérlés",
"horizontal_swipe_skip": "Vízszintes Húzás Ugráshoz", "horizontal_swipe_skip": "Vízszintes Húzás Ugráshoz",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Hiba a Fájlok Törlésekor", "error_deleting_files": "Hiba a Fájlok Törlésekor",
"background_downloads_enabled": "Background downloads enabled", "background_downloads_enabled": "Background downloads enabled",
"background_downloads_disabled": "Background downloads disabled" "background_downloads_disabled": "Background downloads disabled"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Az új frissítéshez az összes tartalmat újra le kell tölteni. Kérjük, töröld az összes letöltött tartalmat, majd próbáld újra.", "new_app_version_requires_re_download_description": "Az új frissítéshez az összes tartalmat újra le kell tölteni. Kérjük, töröld az összes letöltött tartalmat, majd próbáld újra.",
"back": "Vissza", "back": "Vissza",
"delete": "Törlés", "delete": "Törlés",
"delete_download": "Delete Download",
"something_went_wrong": "Hiba Történt", "something_went_wrong": "Hiba Történt",
"could_not_get_stream_url_from_jellyfin": "Nem sikerült lekérni a stream URL-t a Jellyfinből", "could_not_get_stream_url_from_jellyfin": "Nem sikerült lekérni a stream URL-t a Jellyfinből",
"eta": "Várható Idő: {{eta}}", "eta": "Várható Idő: {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Select", "select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Videó", "video": "Videó",
"audio": "Hang", "audio": "Hang",
"subtitle": "Felirat", "subtitle": "Felirat",
"play": "Play", "play": "Play",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Keresés...", "search": "Keresés...",
@@ -641,7 +556,6 @@
"movies": "Filmek", "movies": "Filmek",
"series": "Sorozatok", "series": "Sorozatok",
"boxsets": "Gyűjtemények", "boxsets": "Gyűjtemények",
"playlists": "Playlists",
"items": "Elemek" "items": "Elemek"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Poszter", "poster": "Poszter",
"cover": "Borító", "cover": "Borító",
"show_titles": "Címek Megjelenítése", "show_titles": "Címek Megjelenítése",
"show_stats": "Statisztikák Megjelenítése", "show_stats": "Statisztikák Megjelenítése"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Műfajok", "genres": "Műfajok",
@@ -661,11 +574,7 @@
"sort_by": "Rendezés", "sort_by": "Rendezés",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Rendezés Iránya", "sort_order": "Rendezés Iránya",
"tags": "Címkék", "tags": "Címkék"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Nincsenek Linkek" "no_links": "Nincsenek Linkek"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Hiba", "error": "Hiba",
"failed_to_get_stream_url": "Nem sikerült lekérni a stream URL-t", "failed_to_get_stream_url": "Nem sikerült lekérni a stream URL-t",
"an_error_occured_while_playing_the_video": "Hiba történt a videó lejátszása közben. Ellenőrizd a naplókat a beállításokban.", "an_error_occured_while_playing_the_video": "Hiba történt a videó lejátszása közben. Ellenőrizd a naplókat a beállításokban.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Következő", "next_up": "Következő",
@@ -738,11 +617,6 @@
"series": "Sorozat", "series": "Sorozat",
"seasons": "Évadok", "seasons": "Évadok",
"season": "Évad", "season": "Évad",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Ehhez az évadhoz nincs epizód", "no_episodes_for_this_season": "Ehhez az évadhoz nincs epizód",
"overview": "Áttekintés", "overview": "Áttekintés",
"more_with": "További {{name}} Alkotások", "more_with": "További {{name}} Alkotások",
@@ -753,21 +627,10 @@
"media_options": "Media Options", "media_options": "Media Options",
"quality": "Minőség", "quality": "Minőség",
"audio": "Hang", "audio": "Hang",
"subtitles": { "subtitles": "Felirat",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Több Megjelenítése", "show_more": "Több Megjelenítése",
"show_less": "Kevesebb Megjelenítése", "show_less": "Kevesebb Megjelenítése",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Megjelent:", "appeared_in": "Megjelent:",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Nem Sikerült Betölteni az Elemet", "could_not_load_item": "Nem Sikerült Betölteni az Elemet",
"none": "Nincs", "none": "Nincs",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "{{item_count}} Elem Letöltése", "download_x_item": "{{item_count}} Elem Letöltése",
"download_unwatched_only": "Csak Nem Megtekintett", "download_unwatched_only": "Csak Nem Megtekintett",
"download_button": "Letöltés" "download_button": "Letöltés"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Következő", "next": "Következő",
@@ -795,18 +652,7 @@
"movies": "Filmek", "movies": "Filmek",
"sports": "Sport", "sports": "Sport",
"for_kids": "Gyerekeknek", "for_kids": "Gyerekeknek",
"news": "Hírek", "news": "Hírek"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Megerősítés", "confirm": "Megerősítés",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "A Jellyseerr szerver nem felel meg a minimum verziókövetelményeknek! Kérlek frissítsd legalább 2.0.0-ra.", "jellyseer_does_not_meet_requirements": "A Jellyseerr szerver nem felel meg a minimum verziókövetelményeknek! Kérlek frissítsd legalább 2.0.0-ra.",
"jellyseerr_test_failed": "A Jellyseerr teszt sikertelen. Próbáld újra.", "jellyseerr_test_failed": "A Jellyseerr teszt sikertelen. Próbáld újra.",
@@ -876,8 +716,7 @@
"search": "Keresés", "search": "Keresés",
"library": "Könyvtár", "library": "Könyvtár",
"custom_links": "Egyéni Linkek", "custom_links": "Egyéni Linkek",
"favorites": "Kedvencek", "favorites": "Kedvencek"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Errore", "error_title": "Errore",
"login_title": "Accesso", "login_title": "Accesso",
"login_to_title": "Accedi a", "login_to_title": "Accedi a",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Nome utente", "username_placeholder": "Nome utente",
"password_placeholder": "Password", "password_placeholder": "Password",
"login_button": "Accedi", "login_button": "Accedi",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Ops!", "oops": "Ops!",
"error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.", "error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.",
"continue_watching": "Continua a guardare", "continue_watching": "Continua a guardare",
"continue": "Continue",
"next_up": "Prossimo", "next_up": "Prossimo",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Aggiunti di recente a {{libraryName}}", "recently_added_in": "Aggiunti di recente a {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Impostazioni", "settings_title": "Impostazioni",
"log_out_button": "Esci", "log_out_button": "Esci",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categorie" "title": "Categorie"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Aspetto", "title": "Aspetto",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -161,7 +136,7 @@
"not_connected_to_wifi": "Not connected to WiFi", "not_connected_to_wifi": "Not connected to WiFi",
"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": "WiFi Attuale", "current_wifi": "Current WiFi",
"using_url": "Sta utilizzando", "using_url": "Sta utilizzando",
"local": "Local URL", "local": "Local URL",
"remote": "Remote URL", "remote": "Remote URL",
@@ -199,22 +174,6 @@
"rewind_length": "Lunghezza del riavvolgimento", "rewind_length": "Lunghezza del riavvolgimento",
"seconds_unit": "secondi" "seconds_unit": "secondi"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Controlli Gesture", "gesture_controls_title": "Controlli Gesture",
"horizontal_swipe_skip": "Scorrimento orizzontale per saltare", "horizontal_swipe_skip": "Scorrimento orizzontale per saltare",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Errore nella cancellazione dei file", "error_deleting_files": "Errore nella cancellazione dei file",
"background_downloads_enabled": "Scaricamento in background abilitato", "background_downloads_enabled": "Scaricamento in background abilitato",
"background_downloads_disabled": "Scaricamento in background disabilitato" "background_downloads_disabled": "Scaricamento in background disabilitato"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.", "new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.",
"back": "Indietro", "back": "Indietro",
"delete": "Cancella", "delete": "Cancella",
"delete_download": "Delete Download",
"something_went_wrong": "Qualcosa è andato storto", "something_went_wrong": "Qualcosa è andato storto",
"could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin", "could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin",
"eta": "Tempo stimato di completamento {{eta}}", "eta": "Tempo stimato di completamento {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Seleziona", "select": "Seleziona",
"no_trailer_available": "Nessun trailer disponibile", "no_trailer_available": "Nessun trailer disponibile",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
"subtitle": "Sottotitoli", "subtitle": "Sottotitoli",
"play": "Gioca", "play": "Gioca",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Nulla", "none": "Nulla",
"track": "Traccia", "track": "Traccia",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Cerca...", "search": "Cerca...",
@@ -641,7 +556,6 @@
"movies": "film", "movies": "film",
"series": "serie TV", "series": "serie TV",
"boxsets": "cofanetti", "boxsets": "cofanetti",
"playlists": "Playlists",
"items": "elementi" "items": "elementi"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Poster", "poster": "Poster",
"cover": "Copertina", "cover": "Copertina",
"show_titles": "Mostra titoli", "show_titles": "Mostra titoli",
"show_stats": "Mostra statistiche", "show_stats": "Mostra statistiche"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Generi", "genres": "Generi",
@@ -661,11 +574,7 @@
"sort_by": "Ordina per", "sort_by": "Ordina per",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Criterio di ordinamento", "sort_order": "Criterio di ordinamento",
"tags": "Tag", "tags": "Tag"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Nessun link" "no_links": "Nessun link"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Errore", "error": "Errore",
"failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream", "failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream",
"an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.", "an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Il prossimo", "next_up": "Il prossimo",
@@ -738,11 +617,6 @@
"series": "Serie", "series": "Serie",
"seasons": "Stagioni", "seasons": "Stagioni",
"season": "Stagione", "season": "Stagione",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Nessun episodio per questa stagione", "no_episodes_for_this_season": "Nessun episodio per questa stagione",
"overview": "Panoramica", "overview": "Panoramica",
"more_with": "Altri con {{name}}", "more_with": "Altri con {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Opzioni Media", "media_options": "Opzioni Media",
"quality": "Qualità", "quality": "Qualità",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": "Sottotitoli",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Mostra di più", "show_more": "Mostra di più",
"show_less": "Mostra di meno", "show_less": "Mostra di meno",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Apparso in", "appeared_in": "Apparso in",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Impossibile caricare l'elemento", "could_not_load_item": "Impossibile caricare l'elemento",
"none": "Nessuno", "none": "Nessuno",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Scarica {{item_count}} elementi", "download_x_item": "Scarica {{item_count}} elementi",
"download_unwatched_only": "Solo Non Visti", "download_unwatched_only": "Solo Non Visti",
"download_button": "Scarica" "download_button": "Scarica"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Prossimo", "next": "Prossimo",
@@ -795,18 +652,7 @@
"movies": "Film", "movies": "Film",
"sports": "Sport", "sports": "Sport",
"for_kids": "Per Bambini", "for_kids": "Per Bambini",
"news": "Notiziari", "news": "Notiziari"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Conferma", "confirm": "Conferma",
@@ -851,12 +697,6 @@
"decline": "Rifiuta", "decline": "Rifiuta",
"requested_by": "Richiesto da {{user}}", "requested_by": "Richiesto da {{user}}",
"unknown_user": "Utente Sconosciuto", "unknown_user": "Utente Sconosciuto",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.", "jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.",
"jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.", "jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.",
@@ -876,8 +716,7 @@
"search": "Cerca", "search": "Cerca",
"library": "Libreria", "library": "Libreria",
"custom_links": "Collegamenti personalizzati", "custom_links": "Collegamenti personalizzati",
"favorites": "Preferiti", "favorites": "Preferiti"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "エラー", "error_title": "エラー",
"login_title": "ログイン", "login_title": "ログイン",
"login_to_title": "ログイン先", "login_to_title": "ログイン先",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "ユーザー名", "username_placeholder": "ユーザー名",
"password_placeholder": "パスワード", "password_placeholder": "パスワード",
"login_button": "ログイン", "login_button": "ログイン",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "おっと!", "oops": "おっと!",
"error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。", "error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。",
"continue_watching": "続きを見る", "continue_watching": "続きを見る",
"continue": "Continue",
"next_up": "次の動画", "next_up": "次の動画",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "{{libraryName}}に最近追加された", "recently_added_in": "{{libraryName}}に最近追加された",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "設定", "settings_title": "設定",
"log_out_button": "ログアウト", "log_out_button": "ログアウト",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "カテゴリ" "title": "カテゴリ"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Appearance", "title": "Appearance",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "巻き戻しの長さ", "rewind_length": "巻き戻しの長さ",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "ジェスチャーコントロール", "gesture_controls_title": "ジェスチャーコントロール",
"horizontal_swipe_skip": "水平方向にスワイプしてスキップ", "horizontal_swipe_skip": "水平方向にスワイプしてスキップ",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "イントロ", "title": "イントロ",
@@ -493,21 +430,6 @@
"error_deleting_files": "ファイルの削除エラー", "error_deleting_files": "ファイルの削除エラー",
"background_downloads_enabled": "バックグラウンドでのダウンロードは有効です", "background_downloads_enabled": "バックグラウンドでのダウンロードは有効です",
"background_downloads_disabled": "バックグラウンドでのダウンロードは無効です" "background_downloads_disabled": "バックグラウンドでのダウンロードは無効です"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。", "new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。",
"back": "戻る", "back": "戻る",
"delete": "削除", "delete": "削除",
"delete_download": "Delete Download",
"something_went_wrong": "問題が発生しました", "something_went_wrong": "問題が発生しました",
"could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした", "could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "選択", "select": "選択",
"no_trailer_available": "トレーラーがありません", "no_trailer_available": "トレーラーがありません",
"video": "映像", "video": "映像",
"audio": "音声", "audio": "音声",
"subtitle": "字幕", "subtitle": "字幕",
"play": "再生", "play": "再生",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "検索...", "search": "検索...",
@@ -641,7 +556,6 @@
"movies": "映画", "movies": "映画",
"series": "シリーズ", "series": "シリーズ",
"boxsets": "ボックスセット", "boxsets": "ボックスセット",
"playlists": "Playlists",
"items": "アイテム" "items": "アイテム"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "ポスター", "poster": "ポスター",
"cover": "カバー", "cover": "カバー",
"show_titles": "タイトルの表示", "show_titles": "タイトルの表示",
"show_stats": "統計を表示", "show_stats": "統計を表示"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "ジャンル", "genres": "ジャンル",
@@ -661,11 +574,7 @@
"sort_by": "ソート", "sort_by": "ソート",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "ソート順", "sort_order": "ソート順",
"tags": "タグ", "tags": "タグ"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "リンクがありません" "no_links": "リンクがありません"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "エラー", "error": "エラー",
"failed_to_get_stream_url": "ストリームURLを取得できませんでした", "failed_to_get_stream_url": "ストリームURLを取得できませんでした",
"an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。", "an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "次", "next_up": "次",
@@ -738,11 +617,6 @@
"series": "シリーズ", "series": "シリーズ",
"seasons": "シーズン", "seasons": "シーズン",
"season": "シーズン", "season": "シーズン",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "このシーズンのエピソードはありません", "no_episodes_for_this_season": "このシーズンのエピソードはありません",
"overview": "ストーリー", "overview": "ストーリー",
"more_with": "{{name}}の詳細", "more_with": "{{name}}の詳細",
@@ -753,21 +627,10 @@
"media_options": "Media Options", "media_options": "Media Options",
"quality": "画質", "quality": "画質",
"audio": "音声", "audio": "音声",
"subtitles": { "subtitles": "字幕",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "もっと見る", "show_more": "もっと見る",
"show_less": "少なく表示", "show_less": "少なく表示",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "出演作品", "appeared_in": "出演作品",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "アイテムを読み込めませんでした", "could_not_load_item": "アイテムを読み込めませんでした",
"none": "なし", "none": "なし",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "{{item_count}}のアイテムをダウンロード", "download_x_item": "{{item_count}}のアイテムをダウンロード",
"download_unwatched_only": "未視聴のみ", "download_unwatched_only": "未視聴のみ",
"download_button": "ダウンロード" "download_button": "ダウンロード"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "次", "next": "次",
@@ -795,18 +652,7 @@
"movies": "映画", "movies": "映画",
"sports": "スポーツ", "sports": "スポーツ",
"for_kids": "子供向け", "for_kids": "子供向け",
"news": "ニュース", "news": "ニュース"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "確認", "confirm": "確認",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"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テストに失敗しました。もう一度お試しください。",
@@ -876,8 +716,7 @@
"search": "検索", "search": "検索",
"library": "ライブラリ", "library": "ライブラリ",
"custom_links": "カスタムリンク", "custom_links": "カスタムリンク",
"favorites": "お気に入り", "favorites": "お気に入り"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -1,228 +1,187 @@
{ {
"login": { "login": {
"username_required": "사용자 이름이 필요합니다", "username_required": "Username Is Required",
"error_title": "오류", "error_title": "Error",
"login_title": "로그인", "login_title": "Log In",
"login_to_title": "다음 서비스에 연결 중", "login_to_title": "Log in to",
"select_user": "Select a user to log in", "username_placeholder": "Username",
"add_user_to_login": "Add a user to log in", "password_placeholder": "Password",
"add_user": "Add User", "login_button": "Log In",
"username_placeholder": "사용자 이름", "quick_connect": "Quick Connect",
"password_placeholder": "비밀번호", "enter_code_to_login": "Enter code {{code}} to login",
"login_button": "로그인", "failed_to_initiate_quick_connect": "Failed to initiate Quick Connect",
"quick_connect": "퀵 커넥트", "got_it": "Got It",
"enter_code_to_login": "로그인 하기 위해 코드{{code}}를 입력하세요", "connection_failed": "Connection Failed",
"failed_to_initiate_quick_connect": "Quick Connect 연결을 시작하는 데 실패했습니다", "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
"got_it": "성공", "an_unexpected_error_occured": "An Unexpected Error Occurred",
"connection_failed": "연결 실패", "change_server": "Change Server",
"could_not_connect_to_server": "서버에 연결되지 않았습니다. URL과 네트워크 상태를 확인하세요.", "invalid_username_or_password": "Invalid Username or Password",
"an_unexpected_error_occured": "예기치 않은 오류가 발생했습니다", "user_does_not_have_permission_to_log_in": "User does not have permission to log in",
"change_server": "서버 변경", "server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later",
"invalid_username_or_password": "잘못된 아이디 혹은 비밀번호입니다", "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
"user_does_not_have_permission_to_log_in": "로그인 하기 위한 권한이 없습니다", "there_is_a_server_error": "There is a server error",
"server_is_taking_too_long_to_respond_try_again_later": "서버 응답이 너무 느립니다. 나중에 다시 시도하세요", "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
"server_received_too_many_requests_try_again_later": "서버가 너무 많은 요청을 받았습니다. 나중에 다시 시도하세요.",
"there_is_a_server_error": "서버 에러",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "예기치 않은 오류가 발생했습니다. 서버 URL을 올바르게 입력하셨습니까?",
"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"
}, },
"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",
"connect_button": "연결", "connect_button": "Connect",
"previous_servers": "이전 서버", "previous_servers": "Previous Servers",
"clear_button": "모두 지우기", "clear_button": "Clear all",
"swipe_to_remove": "스와이프해서 지우기", "swipe_to_remove": "Swipe to remove",
"search_for_local_servers": "로컬 서버 찾기", "search_for_local_servers": "Search for Local Servers",
"searching": "찾는 중...", "searching": "Searching...",
"servers": "서버", "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}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"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": "{{username}} PIN 입력", "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": "Checking server connection...",
"no_internet": "인터넷에 연결되지 않음", "no_internet": "No Internet",
"no_items": "항목 없음", "no_items": "No Items",
"no_internet_message": "걱정마세요. 다운로드 된 컨텐츠는 여전히 볼 수 있습니다.", "no_internet_message": "No worries, you can still watch\ndownloaded content.",
"checking_server_connection_message": "Checking connection to server", "checking_server_connection_message": "Checking connection to server",
"go_to_downloads": "Go to Downloads", "go_to_downloads": "Go to Downloads",
"retry": "재시도", "retry": "Retry",
"server_unreachable": "서버에 연결할 수 없음", "server_unreachable": "Server Unreachable",
"server_unreachable_message": "서버에 연결할 수 없습니다. 네트워크 상태를 체크하세요.", "server_unreachable_message": "Could not reach the server.\nPlease check your network connection.",
"oops": "이런!", "oops": "Oops!",
"error_message": "문제가 발생했습니다.\n로그아웃 후 다시 로그인해 주세요.", "error_message": "Something went wrong.\nPlease log out and in again.",
"continue_watching": "이어서 보기", "continue_watching": "Continue Watching",
"continue": "Continue", "next_up": "Next Up",
"next_up": "다음 시청", "continue_and_next_up": "Continue & Next Up",
"continue_and_next_up": "이어서 보기 & 다음 시청", "recently_added_in": "Recently Added in {{libraryName}}",
"recently_added_in": "최근에 추가된 {{libraryName}}", "suggested_movies": "Suggested Movies",
"suggested_movies": "추천 영화", "suggested_episodes": "Suggested Episodes",
"suggested_episodes": "추천 에피소드",
"intro": { "intro": {
"welcome_to_streamyfin": "스트리미핀에 오신 것을 환영합니다", "welcome_to_streamyfin": "Welcome to Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "젤리핀을 위한 무료 오픈소스 클라이언트입니다.", "a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin.",
"features_title": "기능", "features_title": "Features",
"features_description": "스트리미핀은 다양한 기능을 제공하며 설정 메뉴에서 확인할 수 있는 여러 소프트웨어와 통합됩니다. 이러한 소프트웨어에는 다음이 포함됩니다:", "features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:",
"jellyseerr_feature_description": "Seerr 인스턴스에 연결하여 앱에서 직접 영화를 요청할 수 있습니다.", "jellyseerr_feature_description": "Connect to your Seerr instance and request movies directly in the app.",
"downloads_feature_title": "다운로드된 컨텐츠", "downloads_feature_title": "Downloads",
"downloads_feature_description": "오프라인으로 보기위해 다운로드 하세요. 기본 다운로드 방식을 사용하거나, 백그라운드에서 파일을 다운로드하는 최적화 서버를 설치할 수 있습니다.", "downloads_feature_description": "Download movies and tv-shows to view offline. Use either the default method or install the optimize server to download files in the background.",
"chromecast_feature_description": "영화와 TV 프로그램을 Chromecast 기기로 전송하기", "chromecast_feature_description": "Cast movies and tv-shows to your Chromecast devices.",
"centralised_settings_plugin_title": "중앙 설정 플러그인", "centralised_settings_plugin_title": "Centralised Settings Plugin",
"centralised_settings_plugin_description": "Jellyfin 서버의 중앙 집중식 위치에서 설정을 구성합니다. 모든 사용자의 모든 클라이언트 설정이 자동으로 동기화됩니다.", "centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.",
"done_button": "확인", "done_button": "Done",
"go_to_settings_button": "설정으로 이동", "go_to_settings_button": "Go to Settings",
"read_more": "자세히 보기" "read_more": "Read More"
}, },
"settings": { "settings": {
"settings_title": "설정", "settings_title": "Settings",
"log_out_button": "로그아웃", "log_out_button": "Log Out",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"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"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"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": "홈 WiFi에 연결되었을 때 로컬 URL로 자동 전환", "auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
"local_url": "로컬 URL", "local_url": "Local URL",
"local_url_hint": "로컬 서버 주소를 입력하세요 (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": "로컬 URL", "local": "Local URL",
"remote": "원격 URL", "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": "자동 전환 Wi-Fi 네트워크를 감지하려면 위치 권한이 필요합니다. 설정에서 위치 권한을 활성화해 주세요." "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": "User Info",
"user": "사용자", "user": "User",
"server": "서버", "server": "Server",
"token": "토큰", "token": "Token",
"app_version": "앱 버전" "app_version": "App Version"
}, },
"quick_connect": { "quick_connect": {
"quick_connect_title": "퀵 커넥트", "quick_connect_title": "Quick Connect",
"authorize_button": "퀵 커넥트 승인", "authorize_button": "Authorize Quick Connect",
"enter_the_quick_connect_code": "퀵 커넥트 코드 입력...", "enter_the_quick_connect_code": "Enter the quick connect code...",
"success": "성공", "success": "Success",
"quick_connect_autorized": "퀵 커넥트 승인됨", "quick_connect_autorized": "Quick Connect Authorized",
"error": "오류", "error": "Error",
"invalid_code": "유효하지 않은 코드", "invalid_code": "Invalid Code",
"authorize": "승인" "authorize": "Authorize"
}, },
"media_controls": { "media_controls": {
"media_controls_title": "미디어 컨트롤", "media_controls_title": "Media Controls",
"forward_skip_length": "앞으로 건너뛸 시간", "forward_skip_length": "Forward Skip Length",
"rewind_length": "뒤로 되감을 시간", "rewind_length": "Rewind Length",
"seconds_unit": "" "seconds_unit": "s"
},
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
}, },
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "제스처 제어", "gesture_controls_title": "Gesture Controls",
"horizontal_swipe_skip": "좌/우로 스와이프하여 건너뛰기", "horizontal_swipe_skip": "Horizontal Swipe to Skip",
"horizontal_swipe_skip_description": "컨트롤 숨김상태에서 좌/우로 스와이프하여 건너뛰기", "horizontal_swipe_skip_description": "Swipe left/right when controls are hidden to skip",
"left_side_brightness": "왼쪽 영역 밝기 조정 컨트롤", "left_side_brightness": "Left Side Brightness Control",
"left_side_brightness_description": "왼쪽 영역을 위/아래 스와이프하여 밝기 조절", "left_side_brightness_description": "Swipe up/down on left side to adjust brightness",
"right_side_volume": "오른쪽 영역 볼륨 컨트롤", "right_side_volume": "Right Side Volume Control",
"right_side_volume_description": "오른족 영역을 위/아래로 스와이프 하여 볼륨 조절", "right_side_volume_description": "Swipe up/down on right side to adjust volume",
"hide_volume_slider": "Hide Volume Slider", "hide_volume_slider": "Hide Volume Slider",
"hide_volume_slider_description": "Hide the volume slider in the video player", "hide_volume_slider_description": "Hide the volume slider in the video player",
"hide_brightness_slider": "Hide Brightness Slider", "hide_brightness_slider": "Hide Brightness Slider",
@@ -237,7 +196,7 @@
"language": "Language", "language": "Language",
"transcode_mode": { "transcode_mode": {
"title": "Audio Transcoding", "title": "Audio Transcoding",
"description": "서라운드 오디오(7.1, TrueHD, DTS-HD)를 어떻게 처리할지 설정합니다", "description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
"auto": "Auto", "auto": "Auto",
"stereo": "Force Stereo", "stereo": "Force Stereo",
"5_1": "Allow 5.1", "5_1": "Allow 5.1",
@@ -269,68 +228,52 @@
"outline_opacity": "Outline Opacity", "outline_opacity": "Outline Opacity",
"bold_text": "Bold Text", "bold_text": "Bold Text",
"colors": { "colors": {
"Black": "검정색", "Black": "Black",
"Gray": "회색", "Gray": "Gray",
"Silver": "은색", "Silver": "Silver",
"White": "흰색", "White": "White",
"Maroon": "밤색", "Maroon": "Maroon",
"Red": "빨간색", "Red": "Red",
"Fuchsia": "분홍색", "Fuchsia": "Fuchsia",
"Yellow": "노란색", "Yellow": "Yellow",
"Olive": "올리브 색", "Olive": "Olive",
"Green": "녹색", "Green": "Green",
"Teal": "청록색", "Teal": "Teal",
"Lime": "라임색", "Lime": "Lime",
"Purple": "보라색", "Purple": "Purple",
"Navy": "남색", "Navy": "Navy",
"Blue": "파란색", "Blue": "Blue",
"Aqua": "아쿠아색" "Aqua": "Aqua"
}, },
"thickness": { "thickness": {
"None": "없음", "None": "None",
"Thin": "얇게", "Thin": "Thin",
"Normal": "보통", "Normal": "Normal",
"Thick": "굵게" "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."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"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"
}, },
@@ -345,20 +288,20 @@
"PORTRAIT_UP": "Portrait Up", "PORTRAIT_UP": "Portrait Up",
"PORTRAIT_DOWN": "Portrait Down", "PORTRAIT_DOWN": "Portrait Down",
"LANDSCAPE": "Landscape", "LANDSCAPE": "Landscape",
"LANDSCAPE_LEFT": "왼쪽 가로 모드", "LANDSCAPE_LEFT": "Landscape Left",
"LANDSCAPE_RIGHT": "오른쪽 가로 모드", "LANDSCAPE_RIGHT": "Landscape Right",
"OTHER": "Other", "OTHER": "Other",
"UNKNOWN": "Unknown" "UNKNOWN": "Unknown"
}, },
"safe_area_in_controls": "컨트롤 안전 영역", "safe_area_in_controls": "Safe Area in Controls",
"video_player": "Video Player", "video_player": "Video Player",
"video_players": { "video_players": {
"VLC_3": "VLC 3", "VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)" "VLC_4": "VLC 4 (Experimental + PiP)"
}, },
"show_custom_menu_links": "사용자 지정 메뉴 링크 표시", "show_custom_menu_links": "Show Custom Menu Links",
"show_large_home_carousel": "대형 홈 슬라이드 배너 표시 (베타)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "라이브러리 숨기기", "hide_libraries": "Hide Libraries",
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", "select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
"disable_haptic_feedback": "Disable Haptic Feedback", "disable_haptic_feedback": "Disable Haptic Feedback",
"default_quality": "Default Quality", "default_quality": "Default Quality",
@@ -391,24 +334,24 @@
"password": "Password", "password": "Password",
"password_placeholder": "Enter password for Jellyfin user {{username}}", "password_placeholder": "Enter password for Jellyfin user {{username}}",
"login_button": "Login", "login_button": "Login",
"total_media_requests": "전체 미디어 요청 수", "total_media_requests": "Total Media Requests",
"movie_quota_limit": "영화 요청 한도", "movie_quota_limit": "Movie Quota Limit",
"movie_quota_days": "영화 요청 제한 기간", "movie_quota_days": "Movie Quota Days",
"tv_quota_limit": "TV Quota Limit", "tv_quota_limit": "TV Quota Limit",
"tv_quota_days": "TV 요청 제한 기간", "tv_quota_days": "TV Quota Days",
"reset_jellyseerr_config_button": "Seerr 설정 초기화", "reset_jellyseerr_config_button": "Reset Seerr Config",
"unlimited": "Unlimited", "unlimited": "Unlimited",
"plus_n_more": "+{{n}}개 더", "plus_n_more": "+{{n}} More",
"order_by": { "order_by": {
"DEFAULT": "Default", "DEFAULT": "Default",
"VOTE_COUNT_AND_AVERAGE": "평균 평점 및 투표 수", "VOTE_COUNT_AND_AVERAGE": "Vote count and average",
"POPULARITY": "Popularity" "POPULARITY": "Popularity"
} }
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Marlin 검색 활성화", "enable_marlin_search": "Enable Marlin Search",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://도메인:포트", "server_url_placeholder": "http(s)://domain.org:port",
"marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.", "marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.",
"read_more_about_marlin": "Read More About Marlin.", "read_more_about_marlin": "Read More About Marlin.",
"save_button": "Save", "save_button": "Save",
@@ -431,28 +374,28 @@
"features_title": "Features", "features_title": "Features",
"home_sections_title": "Home Sections", "home_sections_title": "Home Sections",
"enable_movie_recommendations": "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 Movies",
"recommended_series": "추천 시리즈", "recommended_series": "Recommended Series",
"toasts": { "toasts": {
"saved": "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": {
"storage_title": "Storage", "storage_title": "Storage",
"app_usage": " {{usedSpace}}", "app_usage": "App {{usedSpace}}%",
"device_usage": "디바이스 {{availableSpace}}%", "device_usage": "Device {{availableSpace}}%",
"size_used": "{{used}} of {{total}} Used", "size_used": "{{used}} of {{total}} Used",
"delete_all_downloaded_files": "Delete All Downloaded Files", "delete_all_downloaded_files": "Delete All Downloaded Files",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
@@ -460,16 +403,10 @@
"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}} cached", "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": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "다운로드한 노래가 삭제되었습니다", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,26 +430,11 @@
"error_deleting_files": "Error Deleting Files", "error_deleting_files": "Error Deleting Files",
"background_downloads_enabled": "Background downloads enabled", "background_downloads_enabled": "Background downloads enabled",
"background_downloads_disabled": "Background downloads disabled" "background_downloads_disabled": "Background downloads disabled"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
"title": "Sessions", "title": "Sessions",
"no_active_sessions": "세션 비활성화" "no_active_sessions": "No Active Sessions"
}, },
"downloads": { "downloads": {
"downloads_title": "Downloads", "downloads_title": "Downloads",
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.", "new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.",
"back": "Back", "back": "Back",
"delete": "Delete", "delete": "Delete",
"delete_download": "Delete Download",
"something_went_wrong": "Something Went Wrong", "something_went_wrong": "Something Went Wrong",
"could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Select", "select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
"subtitle": "Subtitle", "subtitle": "Subtitle",
"play": "Play", "play": "Play",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Search...", "search": "Search...",
@@ -641,7 +556,6 @@
"movies": "Movies", "movies": "Movies",
"series": "Series", "series": "Series",
"boxsets": "Box Sets", "boxsets": "Box Sets",
"playlists": "Playlists",
"items": "Items" "items": "Items"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Poster", "poster": "Poster",
"cover": "Cover", "cover": "Cover",
"show_titles": "Show Titles", "show_titles": "Show Titles",
"show_stats": "Show Stats", "show_stats": "Show Stats"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genres", "genres": "Genres",
@@ -661,11 +574,7 @@
"sort_by": "Sort By", "sort_by": "Sort By",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Sort Order", "sort_order": "Sort Order",
"tags": "Tags", "tags": "Tags"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "No Links" "no_links": "No Links"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Error", "error": "Error",
"failed_to_get_stream_url": "Failed to get the stream URL", "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.", "an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Next Up", "next_up": "Next Up",
@@ -738,11 +617,6 @@
"series": "Series", "series": "Series",
"seasons": "Seasons", "seasons": "Seasons",
"season": "Season", "season": "Season",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "No episodes for this season", "no_episodes_for_this_season": "No episodes for this season",
"overview": "Overview", "overview": "Overview",
"more_with": "More with {{name}}", "more_with": "More with {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Media Options", "media_options": "Media Options",
"quality": "Quality", "quality": "Quality",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": "Subtitle",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Show More", "show_more": "Show More",
"show_less": "Show Less", "show_less": "Show Less",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Appeared In", "appeared_in": "Appeared In",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Could Not Load Item", "could_not_load_item": "Could Not Load Item",
"none": "None", "none": "None",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Download {{item_count}} Items", "download_x_item": "Download {{item_count}} Items",
"download_unwatched_only": "Unwatched Only", "download_unwatched_only": "Unwatched Only",
"download_button": "Download" "download_button": "Download"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Next", "next": "Next",
@@ -795,18 +652,7 @@
"movies": "Movies", "movies": "Movies",
"sports": "Sports", "sports": "Sports",
"for_kids": "For Kids", "for_kids": "For Kids",
"news": "News", "news": "News"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Confirm", "confirm": "Confirm",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0", "jellyseer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0",
"jellyseerr_test_failed": "Seerr test failed. Please try again.", "jellyseerr_test_failed": "Seerr test failed. Please try again.",
@@ -876,8 +716,7 @@
"search": "Search", "search": "Search",
"library": "Library", "library": "Library",
"custom_links": "Custom Links", "custom_links": "Custom Links",
"favorites": "Favorites", "favorites": "Favorites"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Fout", "error_title": "Fout",
"login_title": "Aanmelden", "login_title": "Aanmelden",
"login_to_title": "Aanmelden bij", "login_to_title": "Aanmelden bij",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Gebruikersnaam", "username_placeholder": "Gebruikersnaam",
"password_placeholder": "Wachtwoord", "password_placeholder": "Wachtwoord",
"login_button": "Aanmelden", "login_button": "Aanmelden",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Account selecteren", "select_account": "Account selecteren",
"add_account": "Account toevoegen", "add_account": "Account toevoegen",
"remove_account_description": "Hiermee worden de opgeslagen inloggegevens voor {{username}} verwijderd.", "remove_account_description": "Hiermee worden de opgeslagen inloggegevens voor {{username}} verwijderd."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Account opslaan", "title": "Account opslaan",
@@ -95,7 +86,6 @@
"oops": "Oeps!", "oops": "Oeps!",
"error_message": "Er ging iets fout\nProbeer opnieuw in te loggen.", "error_message": "Er ging iets fout\nProbeer opnieuw in te loggen.",
"continue_watching": "Verder Kijken", "continue_watching": "Verder Kijken",
"continue": "Continue",
"next_up": "Volgende", "next_up": "Volgende",
"continue_and_next_up": "Doorgaan & Volgende", "continue_and_next_up": "Doorgaan & Volgende",
"recently_added_in": "Recent toegevoegd in {{libraryName}}", "recently_added_in": "Recent toegevoegd in {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Instellingen", "settings_title": "Instellingen",
"log_out_button": "Afmelden", "log_out_button": "Afmelden",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categorieën" "title": "Categorieën"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Weergave", "title": "Weergave",
"merge_next_up_continue_watching": "Doorgaan met kijken & Volgende samenvoegen", "merge_next_up_continue_watching": "Doorgaan met kijken & Volgende samenvoegen",
"hide_remote_session_button": "Verberg Knop voor Externe Sessie", "hide_remote_session_button": "Verberg Knop voor Externe Sessie"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Netwerk", "title": "Netwerk",
@@ -199,22 +174,6 @@
"rewind_length": "Duur terugspoelen", "rewind_length": "Duur terugspoelen",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Gebaar Bediening", "gesture_controls_title": "Gebaar Bediening",
"horizontal_swipe_skip": "Horizontale Swipe om over te slaan", "horizontal_swipe_skip": "Horizontale Swipe om over te slaan",
@@ -297,23 +256,7 @@
"subtitle_font": "Lettertype ondertitels", "subtitle_font": "Lettertype ondertitels",
"ksplayer_title": "KSPlayer Instellingen", "ksplayer_title": "KSPlayer Instellingen",
"hardware_decode": "Hardware Acceleratie", "hardware_decode": "Hardware Acceleratie",
"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."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC ondertitel instellingen", "title": "VLC ondertitel instellingen",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Muziek cache gewist", "music_cache_cleared": "Muziek cache gewist",
"delete_all_downloaded_songs": "Verwijder alle gedownloade liedjes", "delete_all_downloaded_songs": "Verwijder alle gedownloade liedjes",
"downloaded_songs_size": "{{size}} gedownload", "downloaded_songs_size": "{{size}} gedownload",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Fout bij het verwijderen van bestanden", "error_deleting_files": "Fout bij het verwijderen van bestanden",
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld", "background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld" "background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.", "new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.",
"back": "Terug", "back": "Terug",
"delete": "Verwijder", "delete": "Verwijder",
"delete_download": "Delete Download",
"something_went_wrong": "Er ging iets mis", "something_went_wrong": "Er ging iets mis",
"could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin", "could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Selecteren", "select": "Selecteren",
"no_trailer_available": "Geen trailer beschikbaar", "no_trailer_available": "Geen trailer beschikbaar",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
"subtitle": "Ondertitel", "subtitle": "Ondertitel",
"play": "Afspelen", "play": "Afspelen",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Geen", "none": "Geen",
"track": "Spoor", "track": "Spoor",
"cancel": "Annuleren", "cancel": "Annuleren",
"stop": "Stop",
"delete": "Verwijderen", "delete": "Verwijderen",
"ok": "Oké", "ok": "Oké",
"remove": "Verwijderen", "remove": "Verwijderen",
"next": "Volgende", "next": "Volgende",
"back": "Terug", "back": "Terug",
"continue": "Doorgaan", "continue": "Doorgaan",
"verifying": "Verifiëren...", "verifying": "Verifiëren..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Zoek...", "search": "Zoek...",
@@ -641,7 +556,6 @@
"movies": "Films", "movies": "Films",
"series": "Series", "series": "Series",
"boxsets": "Boxsets", "boxsets": "Boxsets",
"playlists": "Playlists",
"items": "items" "items": "items"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Poster", "poster": "Poster",
"cover": "Omslag", "cover": "Omslag",
"show_titles": "Toon titels", "show_titles": "Toon titels",
"show_stats": "Toon statistieken", "show_stats": "Toon statistieken"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genres", "genres": "Genres",
@@ -661,11 +574,7 @@
"sort_by": "Sorteren op", "sort_by": "Sorteren op",
"filter_by": "Filteren op", "filter_by": "Filteren op",
"sort_order": "Sorteer volgorde", "sort_order": "Sorteer volgorde",
"tags": "Labels", "tags": "Labels"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Geen links" "no_links": "Geen links"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Fout", "error": "Fout",
"failed_to_get_stream_url": "De stream-URL kon niet worden verkregen", "failed_to_get_stream_url": "De stream-URL kon niet worden verkregen",
"an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.", "an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Wil je het gedownloade bestand afspelen?", "downloaded_file_message": "Wil je het gedownloade bestand afspelen?",
"downloaded_file_yes": "Ja", "downloaded_file_yes": "Ja",
"downloaded_file_no": "Nee", "downloaded_file_no": "Nee",
"downloaded_file_cancel": "Annuleren", "downloaded_file_cancel": "Annuleren"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Volgende", "next_up": "Volgende",
@@ -738,11 +617,6 @@
"series": "Series", "series": "Series",
"seasons": "Seizoenen", "seasons": "Seizoenen",
"season": "Seizoen", "season": "Seizoen",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Geen afleveringen voor dit seizoen", "no_episodes_for_this_season": "Geen afleveringen voor dit seizoen",
"overview": "Overzicht", "overview": "Overzicht",
"more_with": "Meer met {{name}}", "more_with": "Meer met {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Media opties", "media_options": "Media opties",
"quality": "Kwaliteit", "quality": "Kwaliteit",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": "Ondertitel",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Toon meer", "show_more": "Toon meer",
"show_less": "Toon minder", "show_less": "Toon minder",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Verschenen in", "appeared_in": "Verschenen in",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Kon item niet laden", "could_not_load_item": "Kon item niet laden",
"none": "Geen", "none": "Geen",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Download {{item_count}} items", "download_x_item": "Download {{item_count}} items",
"download_unwatched_only": "Alleen niet bekeken", "download_unwatched_only": "Alleen niet bekeken",
"download_button": "Downloaden" "download_button": "Downloaden"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Volgende ", "next": "Volgende ",
@@ -795,18 +652,7 @@
"movies": "Films", "movies": "Films",
"sports": "Sport", "sports": "Sport",
"for_kids": "Voor kinderen", "for_kids": "Voor kinderen",
"news": "Nieuws", "news": "Nieuws"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Bevestig", "confirm": "Bevestig",
@@ -851,12 +697,6 @@
"decline": "Weigeren", "decline": "Weigeren",
"requested_by": "Aangevraagd door {{user}}", "requested_by": "Aangevraagd door {{user}}",
"unknown_user": "Onbekende gebruiker", "unknown_user": "Onbekende gebruiker",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0", "jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0",
"jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.", "jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.",
@@ -876,8 +716,7 @@
"search": "Zoeken", "search": "Zoeken",
"library": "Bibliotheek", "library": "Bibliotheek",
"custom_links": "Aangepaste links", "custom_links": "Aangepaste links",
"favorites": "Favorieten", "favorites": "Favorieten"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Muziek", "title": "Muziek",
@@ -1002,36 +841,5 @@
"show": "Deze serie", "show": "Deze serie",
"all": "Alle media (standaard)" "all": "Alle media (standaard)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Feil", "error_title": "Feil",
"login_title": "Logg inn", "login_title": "Logg inn",
"login_to_title": "Logg inn i", "login_to_title": "Logg inn i",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Brukernavn", "username_placeholder": "Brukernavn",
"password_placeholder": "Passord", "password_placeholder": "Passord",
"login_button": "Logg inn", "login_button": "Logg inn",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Oisann!", "oops": "Oisann!",
"error_message": "Noe gikk galt.\nVennligst logg ut og inn igjen.", "error_message": "Noe gikk galt.\nVennligst logg ut og inn igjen.",
"continue_watching": "Fortsett å se", "continue_watching": "Fortsett å se",
"continue": "Continue",
"next_up": "Neste opp", "next_up": "Neste opp",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Nylig lagt til i {{libraryName}}", "recently_added_in": "Nylig lagt til i {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Innstillinger", "settings_title": "Innstillinger",
"log_out_button": "Logg ut", "log_out_button": "Logg ut",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categories" "title": "Categories"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Appearance", "title": "Appearance",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Omspar lengde", "rewind_length": "Omspar lengde",
"seconds_unit": "S" "seconds_unit": "S"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Gest kontroller", "gesture_controls_title": "Gest kontroller",
"horizontal_swipe_skip": "Vannrett sveip for å hoppe over", "horizontal_swipe_skip": "Vannrett sveip for å hoppe over",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Feil ved sletting av filer", "error_deleting_files": "Feil ved sletting av filer",
"background_downloads_enabled": "Nedlastinger av bakgrunn aktivert", "background_downloads_enabled": "Nedlastinger av bakgrunn aktivert",
"background_downloads_disabled": "Bakgrunnsnedlastinger deaktivert" "background_downloads_disabled": "Bakgrunnsnedlastinger deaktivert"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Den nye oppdateringen krever at innholdet lastes ned på nytt. Fjern alt nedlastet innhold og prøv på nytt.", "new_app_version_requires_re_download_description": "Den nye oppdateringen krever at innholdet lastes ned på nytt. Fjern alt nedlastet innhold og prøv på nytt.",
"back": "Tilbake", "back": "Tilbake",
"delete": "Slett", "delete": "Slett",
"delete_download": "Delete Download",
"something_went_wrong": "Noe gikk galt", "something_went_wrong": "Noe gikk galt",
"could_not_get_stream_url_from_jellyfin": "Kunne ikke hente stream-URL fra Jellyfin", "could_not_get_stream_url_from_jellyfin": "Kunne ikke hente stream-URL fra Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Velg", "select": "Velg",
"no_trailer_available": "Ingen trailer tilgjengelig", "no_trailer_available": "Ingen trailer tilgjengelig",
"video": "Video", "video": "Video",
"audio": "Lyd", "audio": "Lyd",
"subtitle": "Undertittel", "subtitle": "Undertittel",
"play": "Spill", "play": "Spill",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Søk...", "search": "Søk...",
@@ -641,7 +556,6 @@
"movies": "Filmer", "movies": "Filmer",
"series": "Serier", "series": "Serier",
"boxsets": "Boks sett", "boxsets": "Boks sett",
"playlists": "Playlists",
"items": "Elementer" "items": "Elementer"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Plakat", "poster": "Plakat",
"cover": "Omslag", "cover": "Omslag",
"show_titles": "Vis titler", "show_titles": "Vis titler",
"show_stats": "Vis statistikk", "show_stats": "Vis statistikk"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genres", "genres": "Genres",
@@ -661,11 +574,7 @@
"sort_by": "Sorter etter", "sort_by": "Sorter etter",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Sorter etter", "sort_order": "Sorter etter",
"tags": "Tagger", "tags": "Tagger"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Ingen lenke" "no_links": "Ingen lenke"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Feil", "error": "Feil",
"failed_to_get_stream_url": "Kan ikke hente nettadressen for stream", "failed_to_get_stream_url": "Kan ikke hente nettadressen for stream",
"an_error_occured_while_playing_the_video": "En feil oppstod under video. Sjekk loggene i innstillingene.", "an_error_occured_while_playing_the_video": "En feil oppstod under video. Sjekk loggene i innstillingene.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Neste opp", "next_up": "Neste opp",
@@ -738,11 +617,6 @@
"series": "Serier", "series": "Serier",
"seasons": "Sesonger", "seasons": "Sesonger",
"season": "Sesong", "season": "Sesong",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Ingen episoder for denne sesongen", "no_episodes_for_this_season": "Ingen episoder for denne sesongen",
"overview": "Oversikt", "overview": "Oversikt",
"more_with": "Mer med {{name}}", "more_with": "Mer med {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Media Options", "media_options": "Media Options",
"quality": "Kvalitet", "quality": "Kvalitet",
"audio": "Lyd", "audio": "Lyd",
"subtitles": { "subtitles": "Undertittel",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Vis mer", "show_more": "Vis mer",
"show_less": "Vis mindre", "show_less": "Vis mindre",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Ble brukt i", "appeared_in": "Ble brukt i",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Kan ikke laste inn produkt", "could_not_load_item": "Kan ikke laste inn produkt",
"none": "Ingen", "none": "Ingen",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Last ned {{item_count}} Objekter", "download_x_item": "Last ned {{item_count}} Objekter",
"download_unwatched_only": "Bare usette", "download_unwatched_only": "Bare usette",
"download_button": "Nedlasting" "download_button": "Nedlasting"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Neste", "next": "Neste",
@@ -795,18 +652,7 @@
"movies": "Filmer", "movies": "Filmer",
"sports": "Sport", "sports": "Sport",
"for_kids": "For barn", "for_kids": "For barn",
"news": "Nyheter", "news": "Nyheter"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Bekreft", "confirm": "Bekreft",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Seerr server oppfyller ikke minimumskravene til versjoner! Vennligst oppdater til minst 2.0.0", "jellyseer_does_not_meet_requirements": "Seerr server oppfyller ikke minimumskravene til versjoner! Vennligst oppdater til minst 2.0.0",
"jellyseerr_test_failed": "Seerr-test mislyktes. Vennligst prøv på nytt.", "jellyseerr_test_failed": "Seerr-test mislyktes. Vennligst prøv på nytt.",
@@ -876,8 +716,7 @@
"search": "Søk", "search": "Søk",
"library": "Bibliotek", "library": "Bibliotek",
"custom_links": "Egendefinerte lenker", "custom_links": "Egendefinerte lenker",
"favorites": "Favoritter", "favorites": "Favoritter"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Błąd", "error_title": "Błąd",
"login_title": "Zaloguj się", "login_title": "Zaloguj się",
"login_to_title": "Zaloguj się do", "login_to_title": "Zaloguj się do",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Nazwa użytkownika", "username_placeholder": "Nazwa użytkownika",
"password_placeholder": "Hasło", "password_placeholder": "Hasło",
"login_button": "Zaloguj się", "login_button": "Zaloguj się",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} kont", "accounts_count": "{{count}} kont",
"select_account": "Wybierz konto", "select_account": "Wybierz konto",
"add_account": "Dodaj konto", "add_account": "Dodaj konto",
"remove_account_description": "Spowoduje to usunięcie zapisanych danych logowania dla {{username}}.", "remove_account_description": "Spowoduje to usunięcie zapisanych danych logowania dla {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Zapisz konto", "title": "Zapisz konto",
@@ -95,7 +86,6 @@
"oops": "Ups!", "oops": "Ups!",
"error_message": "Coś poszło nie tak.\nWyloguj się i zaloguj ponownie.", "error_message": "Coś poszło nie tak.\nWyloguj się i zaloguj ponownie.",
"continue_watching": "Kontynuuj oglądanie", "continue_watching": "Kontynuuj oglądanie",
"continue": "Continue",
"next_up": "Następne w kolejce", "next_up": "Następne w kolejce",
"continue_and_next_up": "Oglądaj dalej i Następne", "continue_and_next_up": "Oglądaj dalej i Następne",
"recently_added_in": "Ostatnio dodano w {{libraryName}}", "recently_added_in": "Ostatnio dodano w {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Ustawienia", "settings_title": "Ustawienia",
"log_out_button": "Wyloguj się", "log_out_button": "Wyloguj się",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Kategorie" "title": "Kategorie"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Wygląd", "title": "Wygląd",
"merge_next_up_continue_watching": "Połącz Oglądaj dalej i Następne", "merge_next_up_continue_watching": "Połącz Oglądaj dalej i Następne",
"hide_remote_session_button": "Ukryj przycisk Zdalnej Sesji", "hide_remote_session_button": "Ukryj przycisk Zdalnej Sesji"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Długość przewijania do tyłu", "rewind_length": "Długość przewijania do tyłu",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Sterowanie gestami", "gesture_controls_title": "Sterowanie gestami",
"horizontal_swipe_skip": "Przesuń w poziomie, aby pominąć", "horizontal_swipe_skip": "Przesuń w poziomie, aby pominąć",
@@ -297,23 +256,7 @@
"subtitle_font": "Czcionka napisów", "subtitle_font": "Czcionka napisów",
"ksplayer_title": "Ustawienia KSPlayer", "ksplayer_title": "Ustawienia KSPlayer",
"hardware_decode": "Dekodowanie sprzętowe", "hardware_decode": "Dekodowanie sprzętowe",
"hardware_decode_description": "Używaj akceleracji sprzętowej dla dekodowania wideo. Wyłącz, jeśli doświadczasz problemów z odtwarzaniem.", "hardware_decode_description": "Używaj akceleracji sprzętowej dla dekodowania wideo. Wyłącz, jeśli doświadczasz problemów z odtwarzaniem."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "Ustawienia napisów VLC", "title": "Ustawienia napisów VLC",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Wyczyszczono bufor muzyki", "music_cache_cleared": "Wyczyszczono bufor muzyki",
"delete_all_downloaded_songs": "Usuń wszystkie pobrane piosenki", "delete_all_downloaded_songs": "Usuń wszystkie pobrane piosenki",
"downloaded_songs_size": "Pobrano {{size}}", "downloaded_songs_size": "Pobrano {{size}}",
"downloaded_songs_deleted": "Usunięto pobrane piosenki", "downloaded_songs_deleted": "Usunięto pobrane piosenki"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Wstęp", "title": "Wstęp",
@@ -493,21 +430,6 @@
"error_deleting_files": "Błąd podczas usuwania plików", "error_deleting_files": "Błąd podczas usuwania plików",
"background_downloads_enabled": "Pobieranie w tle włączone", "background_downloads_enabled": "Pobieranie w tle włączone",
"background_downloads_disabled": "Pobieranie w tle wyłączone" "background_downloads_disabled": "Pobieranie w tle wyłączone"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Nowa aktualizacja wymaga ponownego pobrania treści. Usuń wszystkie pobrane materiały i spróbuj ponownie.", "new_app_version_requires_re_download_description": "Nowa aktualizacja wymaga ponownego pobrania treści. Usuń wszystkie pobrane materiały i spróbuj ponownie.",
"back": "Wstecz", "back": "Wstecz",
"delete": "Usuń", "delete": "Usuń",
"delete_download": "Delete Download",
"something_went_wrong": "Coś poszło nie tak", "something_went_wrong": "Coś poszło nie tak",
"could_not_get_stream_url_from_jellyfin": "Nie udało się pobrać adresu URL transmisji z Jellyfin", "could_not_get_stream_url_from_jellyfin": "Nie udało się pobrać adresu URL transmisji z Jellyfin",
"eta": "Szacowany czas: {{eta}}", "eta": "Szacowany czas: {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Wybierz", "select": "Wybierz",
"no_trailer_available": "Brak dostępnego zwiastunu", "no_trailer_available": "Brak dostępnego zwiastunu",
"video": "Wideo", "video": "Wideo",
"audio": "Dźwięk", "audio": "Dźwięk",
"subtitle": "Napisy", "subtitle": "Napisy",
"play": "Odtwórz", "play": "Odtwórz",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Nic", "none": "Nic",
"track": "Utwór", "track": "Utwór",
"cancel": "Anuluj", "cancel": "Anuluj",
"stop": "Stop",
"delete": "Usuń", "delete": "Usuń",
"ok": "OK", "ok": "OK",
"remove": "Usuń", "remove": "Usuń",
"next": "Następne", "next": "Następne",
"back": "Poprzednie", "back": "Poprzednie",
"continue": "Kontynuuj", "continue": "Kontynuuj",
"verifying": "Weryfikacja...", "verifying": "Weryfikacja..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Szukaj...", "search": "Szukaj...",
@@ -641,7 +556,6 @@
"movies": "filmy", "movies": "filmy",
"series": "seriale", "series": "seriale",
"boxsets": "zestawy", "boxsets": "zestawy",
"playlists": "Playlists",
"items": "elementy" "items": "elementy"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Plakat", "poster": "Plakat",
"cover": "Okładka", "cover": "Okładka",
"show_titles": "Pokaż tytuły", "show_titles": "Pokaż tytuły",
"show_stats": "Pokaż statystyki", "show_stats": "Pokaż statystyki"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Gatunki", "genres": "Gatunki",
@@ -661,11 +574,7 @@
"sort_by": "Sortuj według", "sort_by": "Sortuj według",
"filter_by": "Filtruj po", "filter_by": "Filtruj po",
"sort_order": "Kolejność sortowania", "sort_order": "Kolejność sortowania",
"tags": "Tagi", "tags": "Tagi"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Brak odnośników" "no_links": "Brak odnośników"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Błąd", "error": "Błąd",
"failed_to_get_stream_url": "Nie udało się pobrać adresu strumienia", "failed_to_get_stream_url": "Nie udało się pobrać adresu strumienia",
"an_error_occured_while_playing_the_video": "Wystąpił błąd podczas odtwarzania wideo. Sprawdź logi w ustawieniach.", "an_error_occured_while_playing_the_video": "Wystąpił błąd podczas odtwarzania wideo. Sprawdź logi w ustawieniach.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Chcesz odtworzyć pobrany plik?", "downloaded_file_message": "Chcesz odtworzyć pobrany plik?",
"downloaded_file_yes": "Tak", "downloaded_file_yes": "Tak",
"downloaded_file_no": "Nie", "downloaded_file_no": "Nie",
"downloaded_file_cancel": "Anuluj", "downloaded_file_cancel": "Anuluj"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Następne", "next_up": "Następne",
@@ -738,11 +617,6 @@
"series": "Serial", "series": "Serial",
"seasons": "Sezony", "seasons": "Sezony",
"season": "Sezon", "season": "Sezon",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Brak odcinków w tym sezonie", "no_episodes_for_this_season": "Brak odcinków w tym sezonie",
"overview": "Opis", "overview": "Opis",
"more_with": "Więcej z {{name}}", "more_with": "Więcej z {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Ustawienia mediów", "media_options": "Ustawienia mediów",
"quality": "Jakość", "quality": "Jakość",
"audio": "Dźwięk", "audio": "Dźwięk",
"subtitles": { "subtitles": "Napisy",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Pokaż więcej", "show_more": "Pokaż więcej",
"show_less": "Pokaż mniej", "show_less": "Pokaż mniej",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Wystąpił w", "appeared_in": "Wystąpił w",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Nie udało się wczytać elementu", "could_not_load_item": "Nie udało się wczytać elementu",
"none": "Brak", "none": "Brak",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Pobierz {{item_count}} elementów", "download_x_item": "Pobierz {{item_count}} elementów",
"download_unwatched_only": "Tylko nieobejrzane", "download_unwatched_only": "Tylko nieobejrzane",
"download_button": "Pobierz" "download_button": "Pobierz"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Następny", "next": "Następny",
@@ -795,18 +652,7 @@
"movies": "Filmy", "movies": "Filmy",
"sports": "Sport", "sports": "Sport",
"for_kids": "Dla dzieci", "for_kids": "Dla dzieci",
"news": "Wiadomości", "news": "Wiadomości"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Potwierdź", "confirm": "Potwierdź",
@@ -851,12 +697,6 @@
"decline": "Odrzuć", "decline": "Odrzuć",
"requested_by": "Poproszone przez {{user}}", "requested_by": "Poproszone przez {{user}}",
"unknown_user": "Nieznany użytkownik", "unknown_user": "Nieznany użytkownik",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Serwer Jellyseerr nie spełnia minimalnych wymagań wersji! Zaktualizuj go co najmniej do wersji 2.0.0", "jellyseer_does_not_meet_requirements": "Serwer Jellyseerr nie spełnia minimalnych wymagań wersji! Zaktualizuj go co najmniej do wersji 2.0.0",
"jellyseerr_test_failed": "Test Jellyseerr nie powiódł się. Spróbuj ponownie.", "jellyseerr_test_failed": "Test Jellyseerr nie powiódł się. Spróbuj ponownie.",
@@ -876,8 +716,7 @@
"search": "Szukaj", "search": "Szukaj",
"library": "Biblioteka", "library": "Biblioteka",
"custom_links": "Niestandardowe odnośniki", "custom_links": "Niestandardowe odnośniki",
"favorites": "Ulubione", "favorites": "Ulubione"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Muzyka", "title": "Muzyka",
@@ -1002,36 +841,5 @@
"show": "Ten odcinek", "show": "Ten odcinek",
"all": "Wszystkie media (domyślne)" "all": "Wszystkie media (domyślne)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Erro", "error_title": "Erro",
"login_title": "Iniciar sessão", "login_title": "Iniciar sessão",
"login_to_title": "Iniciar sessão em", "login_to_title": "Iniciar sessão em",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Usuário", "username_placeholder": "Usuário",
"password_placeholder": "Palavra-passe", "password_placeholder": "Palavra-passe",
"login_button": "Iniciar sessão", "login_button": "Iniciar sessão",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Opa!", "oops": "Opa!",
"error_message": "Algo deu errado.\nPor favor, saia e entre novamente.", "error_message": "Algo deu errado.\nPor favor, saia e entre novamente.",
"continue_watching": "Continuar assistindo", "continue_watching": "Continuar assistindo",
"continue": "Continue",
"next_up": "A Seguir", "next_up": "A Seguir",
"continue_and_next_up": "Continuar e Próximo", "continue_and_next_up": "Continuar e Próximo",
"recently_added_in": "Adicionado recentemente em {{libraryName}}", "recently_added_in": "Adicionado recentemente em {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Confirgurações", "settings_title": "Confirgurações",
"log_out_button": "Encerrar Sessão", "log_out_button": "Encerrar Sessão",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categorias" "title": "Categorias"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Aparência", "title": "Aparência",
"merge_next_up_continue_watching": "Mesclar Continuar Assistindo e Próximo", "merge_next_up_continue_watching": "Mesclar Continuar Assistindo e Próximo",
"hide_remote_session_button": "Esconder botão de sessão remota", "hide_remote_session_button": "Esconder botão de sessão remota"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Comprimento de Retroceder", "rewind_length": "Comprimento de Retroceder",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Controles de Gestos", "gesture_controls_title": "Controles de Gestos",
"horizontal_swipe_skip": "Deslizar horizontalmente para pular", "horizontal_swipe_skip": "Deslizar horizontalmente para pular",
@@ -297,23 +256,7 @@
"subtitle_font": "Fonte da legenda", "subtitle_font": "Fonte da legenda",
"ksplayer_title": "Configurações do KSPlayer", "ksplayer_title": "Configurações do KSPlayer",
"hardware_decode": "Decodificação por hardware", "hardware_decode": "Decodificação por hardware",
"hardware_decode_description": "Use aceleração de hardware para decodificação de vídeo. Desative se você tiver problemas de reprodução.", "hardware_decode_description": "Use aceleração de hardware para decodificação de vídeo. Desative se você tiver problemas de reprodução."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Cache de música limpo", "music_cache_cleared": "Cache de música limpo",
"delete_all_downloaded_songs": "Excluir todas as músicas baixadas", "delete_all_downloaded_songs": "Excluir todas as músicas baixadas",
"downloaded_songs_size": "{{size}} baixado", "downloaded_songs_size": "{{size}} baixado",
"downloaded_songs_deleted": "Músicas baixadas excluídas", "downloaded_songs_deleted": "Músicas baixadas excluídas"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Erro ao excluir arquivos", "error_deleting_files": "Erro ao excluir arquivos",
"background_downloads_enabled": "Downloads em segundo plano ativados", "background_downloads_enabled": "Downloads em segundo plano ativados",
"background_downloads_disabled": "Downloads em segundo plano desativados" "background_downloads_disabled": "Downloads em segundo plano desativados"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "A nova atualização requer que o conteúdo seja baixado novamente. Por favor, remova todo o conteúdo baixado e tente novamente.", "new_app_version_requires_re_download_description": "A nova atualização requer que o conteúdo seja baixado novamente. Por favor, remova todo o conteúdo baixado e tente novamente.",
"back": "Anterior", "back": "Anterior",
"delete": "excluir", "delete": "excluir",
"delete_download": "Delete Download",
"something_went_wrong": "Ocorreu Um Erro", "something_went_wrong": "Ocorreu Um Erro",
"could_not_get_stream_url_from_jellyfin": "Não foi possível obter o URL de transmissão do Jellyfin", "could_not_get_stream_url_from_jellyfin": "Não foi possível obter o URL de transmissão do Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Selecionar", "select": "Selecionar",
"no_trailer_available": "Nenhum trailer disponível", "no_trailer_available": "Nenhum trailer disponível",
"video": "Vídeo", "video": "Vídeo",
"audio": "Áudio", "audio": "Áudio",
"subtitle": "Legenda", "subtitle": "Legenda",
"play": "Reproduzir", "play": "Reproduzir",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Nenhum", "none": "Nenhum",
"track": "Faixa", "track": "Faixa",
"cancel": "Cancelar", "cancel": "Cancelar",
"stop": "Stop",
"delete": "Apagar", "delete": "Apagar",
"ok": "OK", "ok": "OK",
"remove": "Remover", "remove": "Remover",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Buscar...", "search": "Buscar...",
@@ -641,7 +556,6 @@
"movies": "Filmes", "movies": "Filmes",
"series": "Série", "series": "Série",
"boxsets": "Conjuntos de caixas", "boxsets": "Conjuntos de caixas",
"playlists": "Playlists",
"items": "itens" "items": "itens"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Cartaz", "poster": "Cartaz",
"cover": "Capa", "cover": "Capa",
"show_titles": "Mostrar Títulos", "show_titles": "Mostrar Títulos",
"show_stats": "Mostrar estatísticas", "show_stats": "Mostrar estatísticas"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genres", "genres": "Genres",
@@ -661,11 +574,7 @@
"sort_by": "Classificar por", "sort_by": "Classificar por",
"filter_by": "Filtrar Por", "filter_by": "Filtrar Por",
"sort_order": "Ordem de classificação", "sort_order": "Ordem de classificação",
"tags": "Etiquetas", "tags": "Etiquetas"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Sem links" "no_links": "Sem links"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "ERRO", "error": "ERRO",
"failed_to_get_stream_url": "Falha ao obter a URL de transmissão", "failed_to_get_stream_url": "Falha ao obter a URL de transmissão",
"an_error_occured_while_playing_the_video": "Ocorreu um erro ao reproduzir o vídeo. Verifique os logs nas configurações.", "an_error_occured_while_playing_the_video": "Ocorreu um erro ao reproduzir o vídeo. Verifique os logs nas configurações.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Você quer reproduzir o arquivo baixado?", "downloaded_file_message": "Você quer reproduzir o arquivo baixado?",
"downloaded_file_yes": "SIm", "downloaded_file_yes": "SIm",
"downloaded_file_no": "Não", "downloaded_file_no": "Não",
"downloaded_file_cancel": "Cancelar", "downloaded_file_cancel": "Cancelar"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "A Seguir", "next_up": "A Seguir",
@@ -738,11 +617,6 @@
"series": "Série", "series": "Série",
"seasons": "Estações", "seasons": "Estações",
"season": "Temporada", "season": "Temporada",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Não há episódios para esta temporada", "no_episodes_for_this_season": "Não há episódios para esta temporada",
"overview": "Geral", "overview": "Geral",
"more_with": "Mais com {{name}}", "more_with": "Mais com {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Opções de Mídia", "media_options": "Opções de Mídia",
"quality": "Qualidade", "quality": "Qualidade",
"audio": "Áudio", "audio": "Áudio",
"subtitles": { "subtitles": "Legenda",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Mostrar mais", "show_more": "Mostrar mais",
"show_less": "Mostrar menos", "show_less": "Mostrar menos",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Aparece em", "appeared_in": "Aparece em",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Não foi possível carregar o item", "could_not_load_item": "Não foi possível carregar o item",
"none": "Nenhuma", "none": "Nenhuma",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Baixar itens de {{item_count}}", "download_x_item": "Baixar itens de {{item_count}}",
"download_unwatched_only": "Apenas não assistidos", "download_unwatched_only": "Apenas não assistidos",
"download_button": "BAIXAR" "download_button": "BAIXAR"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Próximo", "next": "Próximo",
@@ -795,18 +652,7 @@
"movies": "Filmes", "movies": "Filmes",
"sports": "Esportes", "sports": "Esportes",
"for_kids": "Para crianças", "for_kids": "Para crianças",
"news": "Notícias", "news": "Notícias"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Confirmar", "confirm": "Confirmar",
@@ -851,12 +697,6 @@
"decline": "Declinar", "decline": "Declinar",
"requested_by": "Solicitado por {{user}}", "requested_by": "Solicitado por {{user}}",
"unknown_user": "Usuário desconhecido", "unknown_user": "Usuário desconhecido",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "O servidor do Seerr não atende ao mínimo aos requisitos de versão! Por favor, atualize para pelo menos 2.0.0", "jellyseer_does_not_meet_requirements": "O servidor do Seerr não atende ao mínimo aos requisitos de versão! Por favor, atualize para pelo menos 2.0.0",
"jellyseerr_test_failed": "Falha no teste do senhor. Por favor, tente novamente.", "jellyseerr_test_failed": "Falha no teste do senhor. Por favor, tente novamente.",
@@ -876,8 +716,7 @@
"search": "Pesquisa", "search": "Pesquisa",
"library": "Biblioteca", "library": "Biblioteca",
"custom_links": "Links personalizados", "custom_links": "Links personalizados",
"favorites": "Atalhos", "favorites": "Atalhos"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Música", "title": "Música",
@@ -1002,36 +841,5 @@
"show": "Esta série", "show": "Esta série",
"all": "Todas as mídias (Padrão)" "all": "Todas as mídias (Padrão)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Eroare", "error_title": "Eroare",
"login_title": "Conectare", "login_title": "Conectare",
"login_to_title": "Conectare la", "login_to_title": "Conectare la",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Utilizator", "username_placeholder": "Utilizator",
"password_placeholder": "Parola", "password_placeholder": "Parola",
"login_button": "Conectare", "login_button": "Conectare",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Ups!", "oops": "Ups!",
"error_message": "Ceva nu e bine.\nAutentificați-vă din nou.", "error_message": "Ceva nu e bine.\nAutentificați-vă din nou.",
"continue_watching": "Continuă vizionarea", "continue_watching": "Continuă vizionarea",
"continue": "Continue",
"next_up": "Urmează", "next_up": "Urmează",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Adăugat recent în {{libraryName}}", "recently_added_in": "Adăugat recent în {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Setări", "settings_title": "Setări",
"log_out_button": "Deconectare", "log_out_button": "Deconectare",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categorii" "title": "Categorii"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Aspect", "title": "Aspect",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Durata saltului înapoi", "rewind_length": "Durata saltului înapoi",
"seconds_unit": "S" "seconds_unit": "S"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Controale gesturi", "gesture_controls_title": "Controale gesturi",
"horizontal_swipe_skip": "Glisați orizontal pentru a sări", "horizontal_swipe_skip": "Glisați orizontal pentru a sări",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Introducere", "title": "Introducere",
@@ -493,21 +430,6 @@
"error_deleting_files": "Eroare la ștergerea fișierelor", "error_deleting_files": "Eroare la ștergerea fișierelor",
"background_downloads_enabled": "Descărcări în fundal activate", "background_downloads_enabled": "Descărcări în fundal activate",
"background_downloads_disabled": "Descărcări în fundal dezactivate" "background_downloads_disabled": "Descărcări în fundal dezactivate"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Noua actualizare necesită descărcarea din nou a conținutului. Vă rugăm să eliminați tot conținutul descărcat și să încercați din nou.", "new_app_version_requires_re_download_description": "Noua actualizare necesită descărcarea din nou a conținutului. Vă rugăm să eliminați tot conținutul descărcat și să încercați din nou.",
"back": "Înapoi", "back": "Înapoi",
"delete": "Șterge", "delete": "Șterge",
"delete_download": "Delete Download",
"something_went_wrong": "Ceva nu a mers bine.", "something_went_wrong": "Ceva nu a mers bine.",
"could_not_get_stream_url_from_jellyfin": "Nu s-a putut obține adresa URL a fluxului de la Jellyfin", "could_not_get_stream_url_from_jellyfin": "Nu s-a putut obține adresa URL a fluxului de la Jellyfin",
"eta": "Estimat {{eta}}", "eta": "Estimat {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Selectare", "select": "Selectare",
"no_trailer_available": "Nicio remorcă disponibilă", "no_trailer_available": "Nicio remorcă disponibilă",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
"subtitle": "Subtitrare", "subtitle": "Subtitrare",
"play": "Redare", "play": "Redare",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Nimic", "none": "Nimic",
"track": "Limbă audio", "track": "Limbă audio",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Caută...", "search": "Caută...",
@@ -641,7 +556,6 @@
"movies": "filme", "movies": "filme",
"series": "seriale", "series": "seriale",
"boxsets": "box sets", "boxsets": "box sets",
"playlists": "Playlists",
"items": "articole" "items": "articole"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Poster", "poster": "Poster",
"cover": "Copertă", "cover": "Copertă",
"show_titles": "Afișează titlurile", "show_titles": "Afișează titlurile",
"show_stats": "Afișează statisticile", "show_stats": "Afișează statisticile"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genuri", "genres": "Genuri",
@@ -661,11 +574,7 @@
"sort_by": "Sortează după", "sort_by": "Sortează după",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Ordine de sortare", "sort_order": "Ordine de sortare",
"tags": "Taguri", "tags": "Taguri"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Niciun link" "no_links": "Niciun link"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Eroare", "error": "Eroare",
"failed_to_get_stream_url": "Nu s-a putut obține adresa URL a fluxului", "failed_to_get_stream_url": "Nu s-a putut obține adresa URL a fluxului",
"an_error_occured_while_playing_the_video": "A apărut o eroare la redarea videoclipului. Verificați jurnalele în setări.", "an_error_occured_while_playing_the_video": "A apărut o eroare la redarea videoclipului. Verificați jurnalele în setări.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Doriți să redați fișierul descărcat?", "downloaded_file_message": "Doriți să redați fișierul descărcat?",
"downloaded_file_yes": "Da", "downloaded_file_yes": "Da",
"downloaded_file_no": "Nu", "downloaded_file_no": "Nu",
"downloaded_file_cancel": "Anulează", "downloaded_file_cancel": "Anulează"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Urmează", "next_up": "Urmează",
@@ -738,11 +617,6 @@
"series": "Seriale", "series": "Seriale",
"seasons": "Sezoane", "seasons": "Sezoane",
"season": "Sezon", "season": "Sezon",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Niciun episod pt acest sezon", "no_episodes_for_this_season": "Niciun episod pt acest sezon",
"overview": "Prezentare generală", "overview": "Prezentare generală",
"more_with": "Mai multe cu {{name}}", "more_with": "Mai multe cu {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Opțiuni Media", "media_options": "Opțiuni Media",
"quality": "Calitate", "quality": "Calitate",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": "Subtitrare",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Arată mai mult", "show_more": "Arată mai mult",
"show_less": "Arată mai puțin", "show_less": "Arată mai puțin",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Apare în", "appeared_in": "Apare în",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Nu s-a putut încărca elementul", "could_not_load_item": "Nu s-a putut încărca elementul",
"none": "Nimic", "none": "Nimic",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Descărcați {{item_count}} articole", "download_x_item": "Descărcați {{item_count}} articole",
"download_unwatched_only": "Numai nevizionate", "download_unwatched_only": "Numai nevizionate",
"download_button": "Descarcă" "download_button": "Descarcă"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Următorul", "next": "Următorul",
@@ -795,18 +652,7 @@
"movies": "Filme", "movies": "Filme",
"sports": "Sport", "sports": "Sport",
"for_kids": "Pt copii", "for_kids": "Pt copii",
"news": "Știri", "news": "Știri"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Confirmă", "confirm": "Confirmă",
@@ -851,12 +697,6 @@
"decline": "Respinge", "decline": "Respinge",
"requested_by": "Solicitat de {{user}}", "requested_by": "Solicitat de {{user}}",
"unknown_user": "Utilizator necunoscut", "unknown_user": "Utilizator necunoscut",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Serverul Jellyseerr nu îndeplinește cerințele minime de versiune! Vă rugăm să actualizați cel puțin la versiunea 2.0.0", "jellyseer_does_not_meet_requirements": "Serverul Jellyseerr nu îndeplinește cerințele minime de versiune! Vă rugăm să actualizați cel puțin la versiunea 2.0.0",
"jellyseerr_test_failed": "Testul Jellyseerr a eșuat. Vă rugăm să încercați din nou.", "jellyseerr_test_failed": "Testul Jellyseerr a eșuat. Vă rugăm să încercați din nou.",
@@ -876,8 +716,7 @@
"search": "Caută", "search": "Caută",
"library": "Bibiliotecă", "library": "Bibiliotecă",
"custom_links": "Linkuri personalizate", "custom_links": "Linkuri personalizate",
"favorites": "Favorite", "favorites": "Favorite"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Ошибка", "error_title": "Ошибка",
"login_title": "Вход", "login_title": "Вход",
"login_to_title": "Вход в", "login_to_title": "Вход в",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Имя пользователя", "username_placeholder": "Имя пользователя",
"password_placeholder": "Пароль", "password_placeholder": "Пароль",
"login_button": "Войти", "login_button": "Войти",
@@ -15,25 +12,25 @@
"failed_to_initiate_quick_connect": "Не удалось инициировать быстрое подключение", "failed_to_initiate_quick_connect": "Не удалось инициировать быстрое подключение",
"got_it": "Принято", "got_it": "Принято",
"connection_failed": "Соединение не удалось", "connection_failed": "Соединение не удалось",
"could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста, проверьте URL и ваше интернет-соединение.", "could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста проверьте URL и ваше интернет соединение.",
"an_unexpected_error_occured": "Возникла непредвиденная ошибка", "an_unexpected_error_occured": "Возникла непредвиденная ошибка",
"change_server": "Поменять сервер", "change_server": "Поменять сервер",
"invalid_username_or_password": "Неправильное имя пользователя или пароль", "invalid_username_or_password": "Неправильное имя пользователя или пароль",
"user_does_not_have_permission_to_log_in": "Пользователь не имеет прав на вход", "user_does_not_have_permission_to_log_in": "Пользователь не имеет прав на вход",
"server_is_taking_too_long_to_respond_try_again_later": "Сервер долго не отвечает, попробуйте позже", "server_is_taking_too_long_to_respond_try_again_later": "Сервер долго не отвечает, попробуйте позже.",
"server_received_too_many_requests_try_again_later": "Сервер получил слишком много запросов, попробуйте позже.", "server_received_too_many_requests_try_again_later": "Сервер получил слишком много запросов, попробуйте позже.",
"there_is_a_server_error": "Возникла ошибка сервера", "there_is_a_server_error": "Возникла ошибка сервера",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Возникла непредвиденная ошибка. Вы правильно ввели URL?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Возникла непредвиденная ошибка. Вы правильно ввели URL?",
"too_old_server_text": "Обнаружен неподдерживаемый сервер Jellyfin", "too_old_server_text": "Неподдерживаемый сервер Jellyfin обнаружен",
"too_old_server_description": "Пожалуйста, обновите Jellyfin до последней версии" "too_old_server_description": "Пожалуйста, обновите Jellyfin до последней версии"
}, },
"server": { "server": {
"enter_url_to_jellyfin_server": "Укажите URL на ваш Jellyfin сервер", "enter_url_to_jellyfin_server": "Укажите URL на ваш Jellyfin сервер",
"server_url_placeholder": "http(s)://your-server.com", "server_url_placeholder": "http(s)://your-server.com",
"connect_button": "Подключиться", "connect_button": "Подключиться",
"previous_servers": "Предыдущие серверы", "previous_servers": "предыдущие серверы",
"clear_button": "Очистить", "clear_button": "Очистить",
"swipe_to_remove": "Смахните для удаления", "swipe_to_remove": "Swipe to remove",
"search_for_local_servers": "Поиск локальных серверов", "search_for_local_servers": "Поиск локальных серверов",
"searching": "Поиск...", "searching": "Поиск...",
"servers": "Сервера", "servers": "Сервера",
@@ -42,16 +39,10 @@
"please_login_again": "Ваша сессия истекла. Пожалуйста, войдите снова.", "please_login_again": "Ваша сессия истекла. Пожалуйста, войдите снова.",
"remove_saved_login": "Удалить сохраненный аккаунт", "remove_saved_login": "Удалить сохраненный аккаунт",
"remove_saved_login_description": "Ваши сохранённые данные для входа от этого сервера будут удалены. Вам придётся ввести ваши логин и пароль ещё раз.", "remove_saved_login_description": "Ваши сохранённые данные для входа от этого сервера будут удалены. Вам придётся ввести ваши логин и пароль ещё раз.",
"accounts_count": "Аккаунтов: {{count}}", "accounts_count": "{{count}} аккаунтов",
"select_account": "Выбрать аккаунт", "select_account": "Выбрать аккаунт",
"add_account": "Добавить аккаунт", "add_account": "Добавить аккаунт",
"remove_account_description": "Данные для входа {{username}} будут удалены.", "remove_account_description": "Данные для входа {{username}} будут удалены."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Сохранить аккаунт", "title": "Сохранить аккаунт",
@@ -67,14 +58,14 @@
"cancel_button": "Отмена" "cancel_button": "Отмена"
}, },
"pin": { "pin": {
"enter_pin": "Введите PIN-код", "enter_pin": "Введите PIN",
"enter_pin_for": "Введите PIN-код для {{username}}", "enter_pin_for": "Введите PIN для {{username}}",
"enter_4_digits": "Введите 4 цифры", "enter_4_digits": "Введите 4 цифры",
"invalid_pin": "Некорректный PIN-код", "invalid_pin": "Некорректный PIN",
"setup_pin": "Установить PIN-код", "setup_pin": "Установить PIN",
"confirm_pin": "Подтвердите PIN-код", "confirm_pin": "Подтвердите PIN",
"pins_dont_match": "PIN-коды не совпадают", "pins_dont_match": "PIN-коды не совпадают",
"forgot_pin": "Забыли PIN-код?", "forgot_pin": "Забыли PIN?",
"forgot_pin_desc": "Ваши данные для входа будут удалены" "forgot_pin_desc": "Ваши данные для входа будут удалены"
}, },
"password": { "password": {
@@ -93,9 +84,8 @@
"server_unreachable": "Сервер недоступен", "server_unreachable": "Сервер недоступен",
"server_unreachable_message": "Не удалось соединиться с сервером.\nПожалуйста, проверьте настройки сети.", "server_unreachable_message": "Не удалось соединиться с сервером.\nПожалуйста, проверьте настройки сети.",
"oops": "Упс!", "oops": "Упс!",
"error_message": "Что-то пошло не так.\nПожалуйста, выйдите и зайдите снова.", "error_message": "Что-то пошло не так.\nПожалуйста выйдите и зайдите снова.",
"continue_watching": "Продолжить", "continue_watching": "Продолжить",
"continue": "Continue",
"next_up": "Далее", "next_up": "Далее",
"continue_and_next_up": "Продолжить и Далее", "continue_and_next_up": "Продолжить и Далее",
"recently_added_in": "Недавно добавлено в {{libraryName}}", "recently_added_in": "Недавно добавлено в {{libraryName}}",
@@ -103,13 +93,13 @@
"suggested_episodes": "Предложенные серии", "suggested_episodes": "Предложенные серии",
"intro": { "intro": {
"welcome_to_streamyfin": "Добро пожаловать в Streamyfin", "welcome_to_streamyfin": "Добро пожаловать в Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Бесплатный клиент для Jellyfin с открытым кодом.", "a_free_and_open_source_client_for_jellyfin": "Бесплатный клиент для Jellyfin с открытым кодом",
"features_title": "Функции", "features_title": "Функции",
"features_description": "Streamyfin имеет множество функций и интегрируется с широким спектром программ, которое вы можете найти в меню настроек:", "features_description": "Streamyfin имеет множество функций и интегрируется с широким спектром программ, которое вы можете найти в меню настроек:",
"jellyseerr_feature_description": "Подключитесь к Jellyseerr и запрашивайте фильмы прямо в приложении.", "jellyseerr_feature_description": "Подключитесь к Jellyseerr и запрашивайте фильмы прямо в приложении.",
"downloads_feature_title": "Загрузки", "downloads_feature_title": "Загрузки",
"downloads_feature_description": "Скачивайте фильмы и сериалы для просмотра без интернета. Используйте стандартный способ или установите сервер оптимизации для загрузки файлов в фоновом режиме.", "downloads_feature_description": "Скачивайте фильмы и сериалы для просмотра без интернета. Используйте стандартный способ или установите сервер оптимизации для загрузки файлов в фоновом режиме.",
"chromecast_feature_description": "Транслируйте фильмы и сериалы на ваши устройства с поддержкой Chromecast.", "chromecast_feature_description": "Транслируйте фильмы и сериалы на ваши устройста с поддержкой Chromecast.",
"centralised_settings_plugin_title": "Плагин для централизованной настройки", "centralised_settings_plugin_title": "Плагин для централизованной настройки",
"centralised_settings_plugin_description": "Настраивайте параметры из централизованного места на сервере Jellyfin. Все настройки клиента для всех пользователей будут синхронизированы автоматически.", "centralised_settings_plugin_description": "Настраивайте параметры из централизованного места на сервере Jellyfin. Все настройки клиента для всех пользователей будут синхронизированы автоматически.",
"done_button": "Готово", "done_button": "Готово",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Настройки", "settings_title": "Настройки",
"log_out_button": "Выйти", "log_out_button": "Выйти",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Категории" "title": "Категории"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Внешний вид", "title": "Внешний вид",
"merge_next_up_continue_watching": "Объединить «Продолжить» и «Далее»", "merge_next_up_continue_watching": "Объединить «Продолжить» и «Далее»",
"hide_remote_session_button": "Скрыть кнопку «Удалённый сеанс»", "hide_remote_session_button": "Скрыть кнопку «Удалённый сеанс»"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Сеть", "title": "Сеть",
@@ -154,7 +129,7 @@
"auto_switch_enabled": "Переключаться дома автоматически", "auto_switch_enabled": "Переключаться дома автоматически",
"auto_switch_description": "Автоматически переключаться на локальный URL при присоединении к домашней WiFi сети", "auto_switch_description": "Автоматически переключаться на локальный URL при присоединении к домашней WiFi сети",
"local_url": "Локальный URL", "local_url": "Локальный URL",
"local_url_hint": "Введите локальный URL вашего сервера (например, http://192.168.1.100:8096)", "local_url_hint": "Введите локальный URL вашего сервера (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": "Домашние WiFi сети",
"add_current_network": "Добавить \"{{ssid}}\"", "add_current_network": "Добавить \"{{ssid}}\"",
@@ -185,44 +160,28 @@
}, },
"quick_connect": { "quick_connect": {
"quick_connect_title": "Быстрое подключение", "quick_connect_title": "Быстрое подключение",
"authorize_button": "Авторизовать через быстрое подключение", "authorize_button": "Авторизировать через быстрое подключение",
"enter_the_quick_connect_code": "Введите код для быстрого подключения...", "enter_the_quick_connect_code": "Введите код для быстрого подключения...",
"success": "Успех", "success": "Успех",
"quick_connect_autorized": "Быстрое подключение авторизовано", "quick_connect_autorized": "Быстрое подключение авторизовано",
"error": "Ошибка", "error": "Ошибка",
"invalid_code": "Неверный код", "invalid_code": "Неверный код",
"authorize": "Авторизовать" "authorize": "Авторизировать"
}, },
"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"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"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_description": "Скрывает индикатор громкости в плеере", "hide_volume_slider_description": "Скрывает индикатор громкости в плеере",
"hide_brightness_slider": "Скрыть индикатор яркости", "hide_brightness_slider": "Скрыть индикатор яркости",
@@ -246,7 +205,7 @@
}, },
"subtitles": { "subtitles": {
"subtitle_title": "Субтитры", "subtitle_title": "Субтитры",
"subtitle_hint": "Настройки отображения субтитров.", "subtitle_hint": "Настройки отображения субтитров",
"subtitle_language": "Язык субтитров", "subtitle_language": "Язык субтитров",
"subtitle_mode": "Режим субтитров", "subtitle_mode": "Режим субтитров",
"set_subtitle_track": "Устанавливать субтитры из предыдущего элемента", "set_subtitle_track": "Устанавливать субтитры из предыдущего элемента",
@@ -297,23 +256,7 @@
"subtitle_font": "Шрифт субтитров", "subtitle_font": "Шрифт субтитров",
"ksplayer_title": "Настройки KSPlayer", "ksplayer_title": "Настройки KSPlayer",
"hardware_decode": "Аппаратное декодирование", "hardware_decode": "Аппаратное декодирование",
"hardware_decode_description": "Использовать аппаратное ускорение для декодирования видео. Выключите, если наблюдаете проблемы с воспроизведением.", "hardware_decode_description": "Использовать аппаратное ускорение для декодирования видео. Выключите, если наблюдаете проблемы с воспроизведением."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "Настройки субтитров в VLC", "title": "Настройки субтитров в VLC",
@@ -328,9 +271,9 @@
"margin": "Отступ снизу" "margin": "Отступ снизу"
}, },
"video_player": { "video_player": {
"title": "Видео плеер", "title": "Видеоплеер",
"video_player": "Видео плеер", "video_player": "Видеоплеер",
"video_player_description": "Выберите видео плеер в iOS.", "video_player_description": "Выберите видеоплеер в iOS.",
"ksplayer": "KSPlayer", "ksplayer": "KSPlayer",
"vlc": "VLC" "vlc": "VLC"
}, },
@@ -351,12 +294,12 @@
"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": "Показывать большую карусель (beta)",
"hide_libraries": "Скрыть библиотеки", "hide_libraries": "Скрыть библиотеки",
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.", "select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
@@ -364,7 +307,7 @@
"default_quality": "Качество по умолчанию", "default_quality": "Качество по умолчанию",
"default_playback_speed": "Скорость воспроизведения по умолчанию", "default_playback_speed": "Скорость воспроизведения по умолчанию",
"auto_play_next_episode": "Автоматически воспроизводить следующий эпизод", "auto_play_next_episode": "Автоматически воспроизводить следующий эпизод",
"max_auto_play_episode_count": "Максимальное количество авто воспроизводимых эпизодов", "max_auto_play_episode_count": "Максимальное количество автовоспроизведения эпизодов",
"disabled": "Отключено" "disabled": "Отключено"
}, },
"downloads": { "downloads": {
@@ -376,9 +319,9 @@
"playback_description": "Настройте воспроизведение музыки.", "playback_description": "Настройте воспроизведение музыки.",
"prefer_downloaded": "Предпочитать скачанные песни", "prefer_downloaded": "Предпочитать скачанные песни",
"caching_title": "Кеширование", "caching_title": "Кеширование",
"caching_description": "Автоматически кешировать следующие треки для стабильного воспроизведения.", "caching_description": "Автоматически предкешировать следующие треки для стабильного воспроизведения.",
"lookahead_enabled": "Включить предкеширование", "lookahead_enabled": "Включить предкеширование",
"lookahead_count": "Сколько треков предкешировать", "lookahead_count": "Сколько предкешировать",
"max_cache_size": "Максимальное число предкешированных треков" "max_cache_size": "Максимальное число предкешированных треков"
}, },
"plugins": { "plugins": {
@@ -386,8 +329,8 @@
"jellyseerr": { "jellyseerr": {
"jellyseerr_warning": "Эта интеграция находится на ранней стадии. Ожидайте изменений.", "jellyseerr_warning": "Эта интеграция находится на ранней стадии. Ожидайте изменений.",
"server_url": "URL сервера", "server_url": "URL сервера",
"server_url_hint": "Пример: http(s)://your-host.url\n(добавьте порт если необходимо)", "server_url_hint": "Пример: http(s)://your-host.url\n(Добавьте порт если необходимо)",
"server_url_placeholder": "Seerr URL...", "server_url_placeholder": "Jellyseerr URL...",
"password": "Пароль", "password": "Пароль",
"password_placeholder": "Введите пароль для пользователя Jellyfin {{username}}", "password_placeholder": "Введите пароль для пользователя Jellyfin {{username}}",
"login_button": "Войти", "login_button": "Войти",
@@ -406,7 +349,7 @@
} }
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Включить Marlin Search", "enable_marlin_search": "Включить 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 для Marlin сервера. URL должен включать http or https и опционально порт.", "marlin_search_hint": "Введите URL для Marlin сервера. URL должен включать http or https и опционально порт.",
@@ -456,20 +399,14 @@
"size_used": "{{used}} из {{total}} использовано", "size_used": "{{used}} из {{total}} использовано",
"delete_all_downloaded_files": "Удалить все загруженные файлы", "delete_all_downloaded_files": "Удалить все загруженные файлы",
"music_cache_title": "Кеш музыки", "music_cache_title": "Кеш музыки",
"music_cache_description": "Автоматически кешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета", "music_cache_description": "Автоматически прекешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета",
"enable_music_cache": "Кешировать музыку", "enable_music_cache": "Кешировать музыку",
"clear_music_cache": "Очистить кеш музыки", "clear_music_cache": "Очистить кеш музыки",
"music_cache_size": "Кешировано: {{size}}", "music_cache_size": "{{size}} кешировано",
"music_cache_cleared": "Кеш музыки очищен", "music_cache_cleared": "Кеш музыки очищен",
"delete_all_downloaded_songs": "Удалить все скачанные песни", "delete_all_downloaded_songs": "Удалить все скачанные песни",
"downloaded_songs_size": "Скачано: {{size}}", "downloaded_songs_size": "{{size}} скачано",
"downloaded_songs_deleted": "Скачанные песни удалены", "downloaded_songs_deleted": "Скачанные песни удалены"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Вступление", "title": "Вступление",
@@ -478,7 +415,7 @@
}, },
"logs": { "logs": {
"logs_title": "Логи", "logs_title": "Логи",
"export_logs": "Сохранить логи", "export_logs": "Экспорт журналов",
"click_for_more_info": "Нажмите для получения дополнительной информации", "click_for_more_info": "Нажмите для получения дополнительной информации",
"level": "Уровень", "level": "Уровень",
"no_logs_available": "Логи не доступны", "no_logs_available": "Логи не доступны",
@@ -493,21 +430,6 @@
"error_deleting_files": "Ошибка при удалении файлов", "error_deleting_files": "Ошибка при удалении файлов",
"background_downloads_enabled": "Фоновая загрузка включена", "background_downloads_enabled": "Фоновая загрузка включена",
"background_downloads_disabled": "Фоновая загрузка отключена" "background_downloads_disabled": "Фоновая загрузка отключена"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -531,10 +453,9 @@
"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": "Удалить",
"delete_download": "Delete Download",
"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}}",
@@ -544,59 +465,53 @@
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов", "failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
"deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!", "deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!",
"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_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}} уже загружается",
"all_files_deleted": "Все загрузки удалены", "all_files_deleted": "Все загрузки удалены",
"files_deleted_by_type": "Удалено: {{count}} {{type}}", "files_deleted_by_type": "{{count}} {{type}} удалён(о)",
"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}} удалён"
} }
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Выбрать", "select": "Выбрать",
"no_trailer_available": "Трейлер недоступен", "no_trailer_available": "Трейлер недоступен",
"video": "Видео", "video": "Видео",
"audio": "Звук", "audio": "Звук",
"subtitle": "Субтитры", "subtitle": "Субтитры",
"play": "Воспроизвести", "play": "Воспроизвести",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Отсутствует", "none": "Отсутствует",
"track": "Трек", "track": "Трек",
"cancel": "Отмена", "cancel": "Отмена",
"stop": "Stop",
"delete": "Удалить", "delete": "Удалить",
"ok": "ОК", "ok": "ОК",
"remove": "Удалить", "remove": "Удалить",
"next": "Вперед", "next": "Вперед",
"back": "Назад", "back": "Назад",
"continue": "Продолжить", "continue": "Продолжить",
"verifying": "Проверка...", "verifying": "Проверка..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Поиск...", "search": "Поиск...",
"x_items": "Элементов: {{count}}", "x_items": "{{count}} элементов",
"library": "Библиотека", "library": "Библиотека",
"discover": "Найти новое", "discover": "Найти новое",
"no_results": "Ничего не найдено", "no_results": "Ничего не найдено",
@@ -614,14 +529,14 @@
"request_series": "Запросить сериалы", "request_series": "Запросить сериалы",
"recently_added": "Недавно добавлено", "recently_added": "Недавно добавлено",
"recent_requests": "Недавно запрошено", "recent_requests": "Недавно запрошено",
"plex_watchlist": "Список просмотра Plex", "plex_watchlist": "Список просмотра с Plex",
"trending": "В тренде", "trending": "В тренде",
"popular_movies": "Популярные фильмы", "popular_movies": "Популярные фильмы",
"movie_genres": "Популярные жанры", "movie_genres": "Популярные жанры",
"upcoming_movies": "Предстоящие фильмы", "upcoming_movies": "Предстоящие фильмы",
"studios": "Студии", "studios": "Студии",
"popular_tv": "Популярные сериалы", "popular_tv": "Популярные сериалы",
"tv_genres": "Жанры сериалов", "tv_genres": "жанры сериалов",
"upcoming_tv": "Предстоящие сериалы", "upcoming_tv": "Предстоящие сериалы",
"networks": "Сети", "networks": "Сети",
"tmdb_movie_keyword": "TMDB Ключевые слова фильмов", "tmdb_movie_keyword": "TMDB Ключевые слова фильмов",
@@ -641,8 +556,7 @@
"movies": "Фильмы", "movies": "Фильмы",
"series": "Сериалы", "series": "Сериалы",
"boxsets": "Коллекции", "boxsets": "Коллекции",
"playlists": "Playlists", "items": "элементы"
"items": "Элементы"
}, },
"options": { "options": {
"display": "Отображать", "display": "Отображать",
@@ -651,9 +565,8 @@
"image_style": "Стиль изображения", "image_style": "Стиль изображения",
"poster": "Постер", "poster": "Постер",
"cover": "Обложка", "cover": "Обложка",
"show_titles": "Показывать заголовки", "show_titles": "Показывать загаловки",
"show_stats": "Показывать статистику", "show_stats": "Показывать статистику"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Жанры", "genres": "Жанры",
@@ -661,11 +574,7 @@
"sort_by": "Сортировка", "sort_by": "Сортировка",
"filter_by": "Фильтр", "filter_by": "Фильтр",
"sort_order": "Порядок", "sort_order": "Порядок",
"tags": "Теги", "tags": "Тэги"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Нет ссылок" "no_links": "Нет ссылок"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Ошибка", "error": "Ошибка",
"failed_to_get_stream_url": "Не удалось получить URL потока", "failed_to_get_stream_url": "Не удалось получить URL потока",
"an_error_occured_while_playing_the_video": "Возникла Неожиданная ошибка во время воспроизведения. Проверьте логи в настройках.", "an_error_occured_while_playing_the_video": "Возникла Неожиданная ошибка во время воспроизведения. Проверьте логи в настройках.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Хотите воспроизвести скачанный файл?", "downloaded_file_message": "Хотите воспроизвести скачанный файл?",
"downloaded_file_yes": "Да", "downloaded_file_yes": "Да",
"downloaded_file_no": "Нет", "downloaded_file_no": "Нет",
"downloaded_file_cancel": "Отмена", "downloaded_file_cancel": "Отмена"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Далее", "next_up": "Далее",
@@ -738,11 +617,6 @@
"series": "Серии", "series": "Серии",
"seasons": "Сезоны", "seasons": "Сезоны",
"season": "Сезон", "season": "Сезон",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "В этом сезоне нет серий", "no_episodes_for_this_season": "В этом сезоне нет серий",
"overview": "Обзор", "overview": "Обзор",
"more_with": "Больше с {{name}}", "more_with": "Больше с {{name}}",
@@ -750,24 +624,13 @@
"no_similar_items_found": "Похожие элементы не найдены", "no_similar_items_found": "Похожие элементы не найдены",
"video": "Видео", "video": "Видео",
"more_details": "Больше деталей", "more_details": "Больше деталей",
"media_options": "Опции медиа", "media_options": "Media Options",
"quality": "Качество", "quality": "Качество",
"audio": "Звук", "audio": "Звук",
"subtitles": { "subtitles": "Субтитры",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Показать больше", "show_more": "Показать больше",
"show_less": "Показать меньше", "show_less": "Показать меньше",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Появлялся в", "appeared_in": "Появлялся в",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Не удалось загрузить элемент", "could_not_load_item": "Не удалось загрузить элемент",
"none": "Отсутствует", "none": "Отсутствует",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Загрузить {{item_count}} элементов", "download_x_item": "Загрузить {{item_count}} элементов",
"download_unwatched_only": "Только непросмотренные", "download_unwatched_only": "Только непросмотренные",
"download_button": "Загрузить" "download_button": "Загрузить"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Далее", "next": "Далее",
@@ -795,18 +652,7 @@
"movies": "Фильмы", "movies": "Фильмы",
"sports": "Спорт", "sports": "Спорт",
"for_kids": "Для детей", "for_kids": "Для детей",
"news": "Новости", "news": "Новости"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Подтвердить", "confirm": "Подтвердить",
@@ -839,32 +685,26 @@
"currently_streaming_on": "Сейчас доступно на", "currently_streaming_on": "Сейчас доступно на",
"advanced": "Продвинутое", "advanced": "Продвинутое",
"request_as": "Запросить как", "request_as": "Запросить как",
"tags": "Теги", "tags": "Тэги",
"quality_profile": "Профиль качества", "quality_profile": "Профиль качества",
"root_folder": "Корневая папка", "root_folder": "Корневая папка",
"season_all": "Сезон (все)", "season_all": "Сезон (все)",
"season_number": "Сезон {{season_number}}", "season_number": "Сезон {{season_number}}",
"number_episodes": "Серий: {{episode_number}}", "number_episodes": "{{episode_number}} серий",
"born": "Рожден", "born": "Рожден",
"appearances": "Появления", "appearances": "Появления",
"approve": "Одобрить", "approve": "Одобрить",
"decline": "Отклонить", "decline": "Отклонить",
"requested_by": "Запрошено {{user}}", "requested_by": "Запрошено {{user}}",
"unknown_user": "Неизвестный пользователь", "unknown_user": "Неизвестный пользователь",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не соответствует минимальным требованиям версии! Пожалуйста, обновите до версии не ниже 2.0.0", "jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не соответствует минимальным требованиям версии! Пожалуйста, обновите до версии не ниже 2.0.0",
"jellyseerr_test_failed": "Тест Jellyseerr не пройден. Попробуйте еще раз.", "jellyseerr_test_failed": "Тест Jellyseerr не пройден. Попробуйте еще раз.",
"failed_to_test_jellyseerr_server_url": "Не удалось проверить URL-адрес сервера Seerr", "failed_to_test_jellyseerr_server_url": "Не удалось проверить URL-адрес сервера jellyseerr",
"issue_submitted": "Проблема отправлена!", "issue_submitted": "Проблема отправлена!",
"requested_item": "Запрошено {{item}}!", "requested_item": "Запрошено {{item}}!",
"you_dont_have_permission_to_request": "У вас нет разрешения на запрос!", "you_dont_have_permission_to_request": "У вас нет разрешения на запрос!",
"something_went_wrong_requesting_media": "Что-то пошло не так при запросе медиа!", "something_went_wrong_requesting_media": "Что-то пошло не так при запросе медиафайлов!",
"request_approved": "Запрос одобрен!", "request_approved": "Запрос одобрен!",
"request_declined": "Запрос отклонён!", "request_declined": "Запрос отклонён!",
"failed_to_approve_request": "Не удалось одобрить запрос", "failed_to_approve_request": "Не удалось одобрить запрос",
@@ -876,8 +716,7 @@
"search": "Поиск", "search": "Поиск",
"library": "Библиотека", "library": "Библиотека",
"custom_links": "Ссылки", "custom_links": "Ссылки",
"favorites": "Избранное", "favorites": "Избранное"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Музыка", "title": "Музыка",
@@ -962,7 +801,7 @@
"name_label": "Название", "name_label": "Название",
"name_placeholder": "Введите название списка", "name_placeholder": "Введите название списка",
"description_label": "Описание", "description_label": "Описание",
"description_placeholder": "Введите описание (необязательно)", "description_placeholder": "Введите описание (не обязательно)",
"is_public_label": "Публичный", "is_public_label": "Публичный",
"is_public_description": "Разрешить остальным пользователям видеть этот список", "is_public_description": "Разрешить остальным пользователям видеть этот список",
"allowed_type_label": "Тип контента", "allowed_type_label": "Тип контента",
@@ -1002,36 +841,5 @@
"show": "Ко всему сериалу", "show": "Ко всему сериалу",
"all": "Ко всем файлам (по умолчанию)" "all": "Ко всем файлам (по умолчанию)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Fel", "error_title": "Fel",
"login_title": "Logga in", "login_title": "Logga in",
"login_to_title": "Logga in till", "login_to_title": "Logga in till",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Användarnamn", "username_placeholder": "Användarnamn",
"password_placeholder": "Lösenord", "password_placeholder": "Lösenord",
"login_button": "Logga in", "login_button": "Logga in",
@@ -47,11 +44,7 @@
"add_account": "Lägg till konto", "add_account": "Lägg till konto",
"remove_account_description": "Detta kommer att ta bort de sparade uppgifterna för {{username}}.", "remove_account_description": "Detta kommer att ta bort de sparade uppgifterna för {{username}}.",
"remove_server": "Ta bort server", "remove_server": "Ta bort server",
"remove_server_description": "Detta kommer att ta bort {{server}} och alla sparade konton från din lista.", "remove_server_description": "Detta kommer att ta bort {{server}} och alla sparade konton från din lista."
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Spara konto", "title": "Spara konto",
@@ -119,12 +112,6 @@
"settings": { "settings": {
"settings_title": "Inställningar", "settings_title": "Inställningar",
"log_out_button": "Logga ut", "log_out_button": "Logga ut",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Kategorier" "title": "Kategorier"
}, },
@@ -141,12 +128,12 @@
"show_home_backdrop": "Dynamisk hembakgrund", "show_home_backdrop": "Dynamisk hembakgrund",
"show_hero_carousel": "Hjältekarusell", "show_hero_carousel": "Hjältekarusell",
"show_series_poster_on_episode": "Visa serieaffisch på avsnitt", "show_series_poster_on_episode": "Visa serieaffisch på avsnitt",
"theme_music": "Temamusik",
"display_size": "Visningsstorlek", "display_size": "Visningsstorlek",
"display_size_small": "Liten", "display_size_small": "Liten",
"display_size_default": "Standard", "display_size_default": "Standard",
"display_size_large": "Stor", "display_size_large": "Stor",
"display_size_extra_large": "Extra stor" "display_size_extra_large": "Extra stor",
"theme_music": "Temamusik"
}, },
"network": { "network": {
"title": "Nätverk", "title": "Nätverk",
@@ -209,12 +196,6 @@
"max_cache_size": "Max cachestorlek", "max_cache_size": "Max cachestorlek",
"max_backward_cache": "Max bakåtcache" "max_backward_cache": "Max bakåtcache"
}, },
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Gestkontroller", "gesture_controls_title": "Gestkontroller",
"horizontal_swipe_skip": "Horisontell Svepning för att Hoppa Fram/Bak", "horizontal_swipe_skip": "Horisontell Svepning för att Hoppa Fram/Bak",
@@ -390,7 +371,7 @@
"server_url_placeholder": "Seerr URL", "server_url_placeholder": "Seerr URL",
"password": "Lösenord", "password": "Lösenord",
"password_placeholder": "Ange lösenord för Jellyfin användare {{username}}", "password_placeholder": "Ange lösenord för Jellyfin användare {{username}}",
"login_button": "Login", "login_button": "Logga in",
"total_media_requests": "Totalt antal mediaförfrågningar", "total_media_requests": "Totalt antal mediaförfrågningar",
"movie_quota_limit": "Gräns för filmkvot", "movie_quota_limit": "Gräns för filmkvot",
"movie_quota_days": "Filmkvot Dagar", "movie_quota_days": "Filmkvot Dagar",
@@ -463,13 +444,7 @@
"music_cache_cleared": "Musikcache rensad", "music_cache_cleared": "Musikcache rensad",
"delete_all_downloaded_songs": "Ta bort alla nerladdade filer", "delete_all_downloaded_songs": "Ta bort alla nerladdade filer",
"downloaded_songs_size": "{{size}} nedladdad", "downloaded_songs_size": "{{size}} nedladdad",
"downloaded_songs_deleted": "Nedladdade låtar raderade", "downloaded_songs_deleted": "Nedladdade låtar raderade"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Introduktion", "title": "Introduktion",
@@ -493,21 +468,6 @@
"error_deleting_files": "Fel Vid Borttagning Av Filer", "error_deleting_files": "Fel Vid Borttagning Av Filer",
"background_downloads_enabled": "Bakgrundsnedladdningar aktiverade", "background_downloads_enabled": "Bakgrundsnedladdningar aktiverade",
"background_downloads_disabled": "Bakgrundsnedladdningar inaktiverade" "background_downloads_disabled": "Bakgrundsnedladdningar inaktiverade"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +494,6 @@
"new_app_version_requires_re_download_description": "Den nya uppdateringen kräver att innehållet laddas ner igen. Ta bort allt nedladdat innehåll och försök igen.", "new_app_version_requires_re_download_description": "Den nya uppdateringen kräver att innehållet laddas ner igen. Ta bort allt nedladdat innehåll och försök igen.",
"back": "Tillbaka", "back": "Tillbaka",
"delete": "Radera", "delete": "Radera",
"delete_download": "Delete Download",
"something_went_wrong": "Något Gick Fel", "something_went_wrong": "Något Gick Fel",
"could_not_get_stream_url_from_jellyfin": "Det gick inte att hämta strömadressen från Jellyfin", "could_not_get_stream_url_from_jellyfin": "Det gick inte att hämta strömadressen från Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -578,8 +537,6 @@
"audio": "Ljud", "audio": "Ljud",
"subtitle": "Undertext", "subtitle": "Undertext",
"play": "Spela", "play": "Spela",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Ingen", "none": "Ingen",
"track": "Spår", "track": "Spår",
"cancel": "Avbryt", "cancel": "Avbryt",
@@ -592,7 +549,8 @@
"continue": "Fortsätt", "continue": "Fortsätt",
"verifying": "Verifierar...", "verifying": "Verifierar...",
"login": "Logga in", "login": "Logga in",
"refresh": "Uppdatera" "refresh": "Uppdatera",
"seeAll": "Visa alla"
}, },
"search": { "search": {
"search": "Sök...", "search": "Sök...",
@@ -652,8 +610,7 @@
"poster": "Affisch", "poster": "Affisch",
"cover": "Omslag", "cover": "Omslag",
"show_titles": "Visa Titlar", "show_titles": "Visa Titlar",
"show_stats": "Visa Statistik", "show_stats": "Visa Statistik"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genrer", "genres": "Genrer",
@@ -683,7 +640,6 @@
}, },
"player": { "player": {
"live": "LIVE", "live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Fel", "error": "Fel",
"failed_to_get_stream_url": "Kunde inte hämta stream-URL", "failed_to_get_stream_url": "Kunde inte hämta stream-URL",
"an_error_occured_while_playing_the_video": "Ett fel uppstod vid uppspelning av videon. Kontrollera loggarna i inställningarna.", "an_error_occured_while_playing_the_video": "Ett fel uppstod vid uppspelning av videon. Kontrollera loggarna i inställningarna.",
@@ -725,12 +681,6 @@
"stopPlayingConfirm": "Är du säker på att du vill stoppa uppspelningen?", "stopPlayingConfirm": "Är du säker på att du vill stoppa uppspelningen?",
"downloaded": "Nedladdad" "downloaded": "Nedladdad"
}, },
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
},
"item_card": { "item_card": {
"next_up": "Näst på tur", "next_up": "Näst på tur",
"no_items_to_display": "Inga Artiklar Att Visa", "no_items_to_display": "Inga Artiklar Att Visa",
@@ -843,7 +793,7 @@
"quality_profile": "Kvalitetsprofil", "quality_profile": "Kvalitetsprofil",
"root_folder": "Rotkatalog", "root_folder": "Rotkatalog",
"season_all": "Säsong (alla)", "season_all": "Säsong (alla)",
"season_number": "Säsong {{seasonNumber}}", "season_number": "Säsong {{season_number}}",
"number_episodes": "{{episode_number}} Avsnitt", "number_episodes": "{{episode_number}} Avsnitt",
"born": "Född", "born": "Född",
"appearances": "Framträdanden", "appearances": "Framträdanden",
@@ -1002,36 +952,5 @@
"show": "Denna serie", "show": "Denna serie",
"all": "Alla media (standard)" "all": "Alla media (standard)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Error", "error_title": "Error",
"login_title": "Log In", "login_title": "Log In",
"login_to_title": "Log in to", "login_to_title": "Log in to",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Username", "username_placeholder": "Username",
"password_placeholder": "Password", "password_placeholder": "Password",
"login_button": "Log In", "login_button": "Log In",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "Oops!", "oops": "Oops!",
"error_message": "Something went wrong.\nPlease log out and in again.", "error_message": "Something went wrong.\nPlease log out and in again.",
"continue_watching": "Continue Watching", "continue_watching": "Continue Watching",
"continue": "Continue",
"next_up": "Next Up", "next_up": "Next Up",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "Recently Added in {{libraryName}}", "recently_added_in": "Recently Added in {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Settings", "settings_title": "Settings",
"log_out_button": "Log Out", "log_out_button": "Log Out",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categories" "title": "Categories"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "ปรับแต่งลักษณะภายนอก", "title": "ปรับแต่งลักษณะภายนอก",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "Rewind Length", "rewind_length": "Rewind Length",
"seconds_unit": "s" "seconds_unit": "s"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Gesture Controls", "gesture_controls_title": "Gesture Controls",
"horizontal_swipe_skip": "Horizontal Swipe to Skip", "horizontal_swipe_skip": "Horizontal Swipe to Skip",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Error Deleting Files", "error_deleting_files": "Error Deleting Files",
"background_downloads_enabled": "Background downloads enabled", "background_downloads_enabled": "Background downloads enabled",
"background_downloads_disabled": "Background downloads disabled" "background_downloads_disabled": "Background downloads disabled"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.", "new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.",
"back": "Back", "back": "Back",
"delete": "Delete", "delete": "Delete",
"delete_download": "Delete Download",
"something_went_wrong": "Something Went Wrong", "something_went_wrong": "Something Went Wrong",
"could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Select", "select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
"subtitle": "Subtitle", "subtitle": "Subtitle",
"play": "Play", "play": "Play",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Search...", "search": "Search...",
@@ -641,7 +556,6 @@
"movies": "Movies", "movies": "Movies",
"series": "Series", "series": "Series",
"boxsets": "Box Sets", "boxsets": "Box Sets",
"playlists": "Playlists",
"items": "Items" "items": "Items"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Poster", "poster": "Poster",
"cover": "Cover", "cover": "Cover",
"show_titles": "Show Titles", "show_titles": "Show Titles",
"show_stats": "Show Stats", "show_stats": "Show Stats"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Genres", "genres": "Genres",
@@ -661,11 +574,7 @@
"sort_by": "Sort By", "sort_by": "Sort By",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Sort Order", "sort_order": "Sort Order",
"tags": "Tags", "tags": "Tags"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "No Links" "no_links": "No Links"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Error", "error": "Error",
"failed_to_get_stream_url": "Failed to get the stream URL", "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.", "an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Next Up", "next_up": "Next Up",
@@ -738,11 +617,6 @@
"series": "Series", "series": "Series",
"seasons": "Seasons", "seasons": "Seasons",
"season": "Season", "season": "Season",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "No episodes for this season", "no_episodes_for_this_season": "No episodes for this season",
"overview": "Overview", "overview": "Overview",
"more_with": "More with {{name}}", "more_with": "More with {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Media Options", "media_options": "Media Options",
"quality": "Quality", "quality": "Quality",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": "Subtitle",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Show More", "show_more": "Show More",
"show_less": "Show Less", "show_less": "Show Less",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Appeared In", "appeared_in": "Appeared In",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Could Not Load Item", "could_not_load_item": "Could Not Load Item",
"none": "None", "none": "None",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "Download {{item_count}} Items", "download_x_item": "Download {{item_count}} Items",
"download_unwatched_only": "Unwatched Only", "download_unwatched_only": "Unwatched Only",
"download_button": "Download" "download_button": "Download"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Next", "next": "Next",
@@ -795,18 +652,7 @@
"movies": "Movies", "movies": "Movies",
"sports": "Sports", "sports": "Sports",
"for_kids": "For Kids", "for_kids": "For Kids",
"news": "News", "news": "News"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Confirm", "confirm": "Confirm",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0", "jellyseer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0",
"jellyseerr_test_failed": "Seerr test failed. Please try again.", "jellyseerr_test_failed": "Seerr test failed. Please try again.",
@@ -876,8 +716,7 @@
"search": "Search", "search": "Search",
"library": "Library", "library": "Library",
"custom_links": "Custom Links", "custom_links": "Custom Links",
"favorites": "Favorites", "favorites": "Favorites"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "ghIq", "error_title": "ghIq",
"login_title": "lut 'el", "login_title": "lut 'el",
"login_to_title": "lut 'el", "login_to_title": "lut 'el",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "tlhIngan", "username_placeholder": "tlhIngan",
"password_placeholder": "ngoq De'", "password_placeholder": "ngoq De'",
"login_button": "yI'el!", "login_button": "yI'el!",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} accounts", "accounts_count": "{{count}} accounts",
"select_account": "Select Account", "select_account": "Select Account",
"add_account": "Add Account", "add_account": "Add Account",
"remove_account_description": "This will remove the saved credentials for {{username}}.", "remove_account_description": "This will remove the saved credentials for {{username}}."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Save Account", "title": "Save Account",
@@ -95,7 +86,6 @@
"oops": "QI'ya!", "oops": "QI'ya!",
"error_message": "Doch rurbe'.\nyIQo' 'ej yI'elqa'.", "error_message": "Doch rurbe'.\nyIQo' 'ej yI'elqa'.",
"continue_watching": "tlhol yIHaDqa'", "continue_watching": "tlhol yIHaDqa'",
"continue": "Continue",
"next_up": "wej", "next_up": "wej",
"continue_and_next_up": "Continue & Next Up", "continue_and_next_up": "Continue & Next Up",
"recently_added_in": "num tu'lu' {{libraryName}}", "recently_added_in": "num tu'lu' {{libraryName}}",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "men", "settings_title": "men",
"log_out_button": "yIQo'", "log_out_button": "yIQo'",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Categories" "title": "Categories"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Appearance", "title": "Appearance",
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up", "merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
"hide_remote_session_button": "Hide Remote Session Button", "hide_remote_session_button": "Hide Remote Session Button"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Network", "title": "Network",
@@ -199,22 +174,6 @@
"rewind_length": "bavHom vum", "rewind_length": "bavHom vum",
"seconds_unit": "tera' rep" "seconds_unit": "tera' rep"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "QavwI' 'ej Qap", "gesture_controls_title": "QavwI' 'ej Qap",
"horizontal_swipe_skip": "SaS mup loSmeH", "horizontal_swipe_skip": "SaS mup loSmeH",
@@ -297,23 +256,7 @@
"subtitle_font": "Subtitle Font", "subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings", "ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding", "hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Subtitle Settings", "title": "VLC Subtitle Settings",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Music cache cleared", "music_cache_cleared": "Music cache cleared",
"delete_all_downloaded_songs": "Delete All Downloaded Songs", "delete_all_downloaded_songs": "Delete All Downloaded Songs",
"downloaded_songs_size": "{{size}} downloaded", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Downloaded songs deleted", "downloaded_songs_deleted": "Downloaded songs deleted"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -493,21 +430,6 @@
"error_deleting_files": "Qaw' ghIq", "error_deleting_files": "Qaw' ghIq",
"background_downloads_enabled": "tlhegh Qaw' chu'", "background_downloads_enabled": "tlhegh Qaw' chu'",
"background_downloads_disabled": "tlhegh Qaw' QIj" "background_downloads_disabled": "tlhegh Qaw' QIj"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "wej chu' Doch Qaw'qa' DaneH. Hoch Qaw' Doch yIQaw' 'ej yIHaDqa'.", "new_app_version_requires_re_download_description": "wej chu' Doch Qaw'qa' DaneH. Hoch Qaw' Doch yIQaw' 'ej yIHaDqa'.",
"back": "yIbav", "back": "yIbav",
"delete": "yIQaw'", "delete": "yIQaw'",
"delete_download": "Delete Download",
"something_went_wrong": "Doch rurbe'", "something_went_wrong": "Doch rurbe'",
"could_not_get_stream_url_from_jellyfin": "Jellyfin tlhol ret URL tu'laHbe'", "could_not_get_stream_url_from_jellyfin": "Jellyfin tlhol ret URL tu'laHbe'",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Select", "select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "mu'tlhegh", "video": "mu'tlhegh",
"audio": "QoQ", "audio": "QoQ",
"subtitle": "De' chu'", "subtitle": "De' chu'",
"play": "Play", "play": "Play",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",
"stop": "Stop",
"delete": "Delete", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Remove", "remove": "Remove",
"next": "Next", "next": "Next",
"back": "Back", "back": "Back",
"continue": "Continue", "continue": "Continue",
"verifying": "Verifying...", "verifying": "Verifying..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "yISam...", "search": "yISam...",
@@ -641,7 +556,6 @@
"movies": "DIS", "movies": "DIS",
"series": "Hem", "series": "Hem",
"boxsets": "Hem ghom", "boxsets": "Hem ghom",
"playlists": "Playlists",
"items": "Doch" "items": "Doch"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "nagh", "poster": "nagh",
"cover": "nagh chop", "cover": "nagh chop",
"show_titles": "pab HoS yIHoch", "show_titles": "pab HoS yIHoch",
"show_stats": "chIm De' yIHoch", "show_stats": "chIm De' yIHoch"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "qorDu'", "genres": "qorDu'",
@@ -661,11 +574,7 @@
"sort_by": "yIwIv", "sort_by": "yIwIv",
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "wIv mIw", "sort_order": "wIv mIw",
"tags": "De'Hom", "tags": "De'Hom"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "ret pagh" "no_links": "ret pagh"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "ghIq", "error": "ghIq",
"failed_to_get_stream_url": "tlhol ret URL tu'laHbe'", "failed_to_get_stream_url": "tlhol ret URL tu'laHbe'",
"an_error_occured_while_playing_the_video": "mu'tlhegh tlholDI' ghIq. menDaq De' qon mej.", "an_error_occured_while_playing_the_video": "mu'tlhegh tlholDI' ghIq. menDaq De' qon mej.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "Do you want to play the downloaded file?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Yes", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel", "downloaded_file_cancel": "Cancel"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "wej", "next_up": "wej",
@@ -738,11 +617,6 @@
"series": "Hem", "series": "Hem",
"seasons": "muv", "seasons": "muv",
"season": "muv", "season": "muv",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "muvvam HemHom pagh", "no_episodes_for_this_season": "muvvam HemHom pagh",
"overview": "Hoch Sov", "overview": "Hoch Sov",
"more_with": "{{name}} latlh", "more_with": "{{name}} latlh",
@@ -753,21 +627,10 @@
"media_options": "Media Options", "media_options": "Media Options",
"quality": "luj", "quality": "luj",
"audio": "QoQ", "audio": "QoQ",
"subtitles": { "subtitles": "De' chu'",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "latlh yIHoch", "show_more": "latlh yIHoch",
"show_less": "Hom yIHoch", "show_less": "Hom yIHoch",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "tlholvam", "appeared_in": "tlholvam",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Doch tlha'laHbe'", "could_not_load_item": "Doch tlha'laHbe'",
"none": "pagh", "none": "pagh",
"download": { "download": {
@@ -778,13 +641,7 @@
"download_x_item": "{{item_count}} Doch yIQaw'", "download_x_item": "{{item_count}} Doch yIQaw'",
"download_unwatched_only": "Unwatched Only", "download_unwatched_only": "Unwatched Only",
"download_button": "yIQaw'" "download_button": "yIQaw'"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "wej", "next": "wej",
@@ -795,18 +652,7 @@
"movies": "DIS", "movies": "DIS",
"sports": "QI'", "sports": "QI'",
"for_kids": "puqbeq", "for_kids": "puqbeq",
"news": "De'", "news": "De'"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "yInej", "confirm": "yInej",
@@ -851,12 +697,6 @@
"decline": "Decline", "decline": "Decline",
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Jellyseerr Ho'Do' veS wej law'be'! 2.0.0 yIchu'!", "jellyseer_does_not_meet_requirements": "Jellyseerr Ho'Do' veS wej law'be'! 2.0.0 yIchu'!",
"jellyseerr_test_failed": "Jellyseerr nejlaHbe'. yIHaDqa'.", "jellyseerr_test_failed": "Jellyseerr nejlaHbe'. yIHaDqa'.",
@@ -876,8 +716,7 @@
"search": "Sam", "search": "Sam",
"library": "De'wI' bom", "library": "De'wI' bom",
"custom_links": "teqlu' ret", "custom_links": "teqlu' ret",
"favorites": "wIv Doch", "favorites": "wIv Doch"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
@@ -1002,36 +841,5 @@
"show": "This show", "show": "This show",
"all": "All media (default)" "all": "All media (default)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -4,9 +4,6 @@
"error_title": "Hata", "error_title": "Hata",
"login_title": "Giriş yap", "login_title": "Giriş yap",
"login_to_title": " 'e giriş yap", "login_to_title": " 'e giriş yap",
"select_user": "Select a user to log in",
"add_user_to_login": "Add a user to log in",
"add_user": "Add User",
"username_placeholder": "Kullanıcı adı", "username_placeholder": "Kullanıcı adı",
"password_placeholder": "Şifre", "password_placeholder": "Şifre",
"login_button": "Giriş yap", "login_button": "Giriş yap",
@@ -45,13 +42,7 @@
"accounts_count": "{{count}} hesap", "accounts_count": "{{count}} hesap",
"select_account": "Hesap Seç", "select_account": "Hesap Seç",
"add_account": "Hesap Ekle", "add_account": "Hesap Ekle",
"remove_account_description": "{{username}} için kayıtlı bilgiler kaldırılacaktır.", "remove_account_description": "{{username}} için kayıtlı bilgiler kaldırılacaktır."
"remove_server": "Remove Server",
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Select Your Server",
"add_server_to_get_started": "Add a server to get started",
"add_server": "Add Server",
"change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Hesabı Kaydet", "title": "Hesabı Kaydet",
@@ -95,7 +86,6 @@
"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ıp tekrar giriş yapın.",
"continue_watching": "İzlemeye Devam Et", "continue_watching": "İzlemeye Devam Et",
"continue": "Continue",
"next_up": "Sonraki", "next_up": "Sonraki",
"continue_and_next_up": "İzlemeye Devam Et & Sıradakiler", "continue_and_next_up": "İzlemeye Devam Et & Sıradakiler",
"recently_added_in": "{{libraryName}} Kütüphanesine Son Eklenenler", "recently_added_in": "{{libraryName}} Kütüphanesine Son Eklenenler",
@@ -119,12 +109,6 @@
"settings": { "settings": {
"settings_title": "Ayarlar", "settings_title": "Ayarlar",
"log_out_button": ıkış Yap", "log_out_button": ıkış Yap",
"switch_user": {
"title": "Switch User",
"account": "Account",
"switch_user": "Switch User on This Server",
"current": "current"
},
"categories": { "categories": {
"title": "Kategoriler" "title": "Kategoriler"
}, },
@@ -137,16 +121,7 @@
"appearance": { "appearance": {
"title": "Görünüm", "title": "Görünüm",
"merge_next_up_continue_watching": "İzlemeye Devam Et & Sıradakiler'i birleştir", "merge_next_up_continue_watching": "İzlemeye Devam Et & Sıradakiler'i birleştir",
"hide_remote_session_button": "Uzak Oturum Butonunu Gizle", "hide_remote_session_button": "Uzak Oturum Butonunu Gizle"
"show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Theme Music",
"display_size": "Display Size",
"display_size_small": "Small",
"display_size_default": "Default",
"display_size_large": "Large",
"display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Ağ", "title": "Ağ",
@@ -199,22 +174,6 @@
"rewind_length": "Geri Sarma Uzunluğu", "rewind_length": "Geri Sarma Uzunluğu",
"seconds_unit": "sn" "seconds_unit": "sn"
}, },
"buffer": {
"title": "Buffer Settings",
"cache_mode": "Cache Mode",
"cache_auto": "Auto",
"cache_yes": "Enabled",
"cache_no": "Disabled",
"buffer_duration": "Buffer Duration",
"max_cache_size": "Max Cache Size",
"max_backward_cache": "Max Backward Cache"
},
"vo_driver": {
"title": "Video Output",
"vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommended)",
"gpu": "gpu"
},
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Hareketle Kontrol", "gesture_controls_title": "Hareketle Kontrol",
"horizontal_swipe_skip": "Atlamak için yatay kaydırma", "horizontal_swipe_skip": "Atlamak için yatay kaydırma",
@@ -297,23 +256,7 @@
"subtitle_font": "Altyazı Yazı Tipi", "subtitle_font": "Altyazı Yazı Tipi",
"ksplayer_title": "KSPlayer Ayarları", "ksplayer_title": "KSPlayer Ayarları",
"hardware_decode": "Donanımsal Kod Çözme", "hardware_decode": "Donanımsal Kod Çözme",
"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": "Video kod çözme için donanımsal hızlandırma kullan. Oynatma sorunları yaşıyorsanız devre dışı bırakın."
"opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align",
"align": {
"left": "Left",
"center": "Center",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
}
}, },
"vlc_subtitles": { "vlc_subtitles": {
"title": "VLC Altyazı Ayarları", "title": "VLC Altyazı Ayarları",
@@ -463,13 +406,7 @@
"music_cache_cleared": "Müzik ön belleği temizlendi", "music_cache_cleared": "Müzik ön belleği temizlendi",
"delete_all_downloaded_songs": "Tüm İndirilen Müzikleri Sil", "delete_all_downloaded_songs": "Tüm İndirilen Müzikleri Sil",
"downloaded_songs_size": "{{size}} indirildi", "downloaded_songs_size": "{{size}} indirildi",
"downloaded_songs_deleted": "İndirilen müzikler silindi", "downloaded_songs_deleted": "İndirilen müzikler silindi"
"clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
"clear_all_cache_success": "Cache Cleared",
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Giriş", "title": "Giriş",
@@ -493,21 +430,6 @@
"error_deleting_files": "Dosyalar silinirken hata oluştu", "error_deleting_files": "Dosyalar silinirken hata oluştu",
"background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi", "background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi",
"background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı" "background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı"
},
"security": {
"title": "Security",
"inactivity_timeout": {
"title": "Inactivity Timeout",
"description": "Auto logout after inactivity",
"disabled": "Disabled",
"1_minute": "1 minute",
"5_minutes": "5 minutes",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
"4_hours": "4 hours",
"24_hours": "24 hours"
}
} }
}, },
"sessions": { "sessions": {
@@ -534,7 +456,6 @@
"new_app_version_requires_re_download_description": "Yeni güncelleme, içeriğin yeniden indirilmesini gerektiriyor. Lütfen tüm indirilen içerikleri kaldırıp tekrar deneyin.", "new_app_version_requires_re_download_description": "Yeni güncelleme, içeriğin yeniden indirilmesini gerektiriyor. Lütfen tüm indirilen içerikleri kaldırıp tekrar deneyin.",
"back": "Geri", "back": "Geri",
"delete": "Sil", "delete": "Sil",
"delete_download": "Delete Download",
"something_went_wrong": "Bir şeyler ters gitti", "something_went_wrong": "Bir şeyler ters gitti",
"could_not_get_stream_url_from_jellyfin": "Jellyfin'den yayın URL'si alınamadı", "could_not_get_stream_url_from_jellyfin": "Jellyfin'den yayın URL'si alınamadı",
"eta": "Tahmini Süre {{eta}}", "eta": "Tahmini Süre {{eta}}",
@@ -571,28 +492,22 @@
} }
}, },
"common": { "common": {
"no_results": "No Results",
"select": "Seç", "select": "Seç",
"no_trailer_available": "Fragman mevcut değil", "no_trailer_available": "Fragman mevcut değil",
"video": "Video", "video": "Video",
"audio": "Ses", "audio": "Ses",
"subtitle": "Altyazı", "subtitle": "Altyazı",
"play": "Oynat", "play": "Oynat",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "Hiçbiri", "none": "Hiçbiri",
"track": "Parça", "track": "Parça",
"cancel": "Vazgeç", "cancel": "Vazgeç",
"stop": "Stop",
"delete": "Sil", "delete": "Sil",
"ok": "Tamam", "ok": "Tamam",
"remove": "Kaldır", "remove": "Kaldır",
"next": "Sonraki", "next": "Sonraki",
"back": "Geri", "back": "Geri",
"continue": "Devam", "continue": "Devam",
"verifying": "Doğrulanıyor...", "verifying": "Doğrulanıyor..."
"login": "Login",
"refresh": "Refresh"
}, },
"search": { "search": {
"search": "Ara...", "search": "Ara...",
@@ -641,7 +556,6 @@
"movies": "filmler", "movies": "filmler",
"series": "diziler", "series": "diziler",
"boxsets": "koleksiyonlar", "boxsets": "koleksiyonlar",
"playlists": "Playlists",
"items": "ögeler" "items": "ögeler"
}, },
"options": { "options": {
@@ -652,8 +566,7 @@
"poster": "Poster", "poster": "Poster",
"cover": "Kapak", "cover": "Kapak",
"show_titles": "Başlıkları göster", "show_titles": "Başlıkları göster",
"show_stats": "İstatistikleri göster", "show_stats": "İstatistikleri göster"
"options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Türler", "genres": "Türler",
@@ -661,11 +574,7 @@
"sort_by": "Sırala", "sort_by": "Sırala",
"filter_by": "Filtrele", "filter_by": "Filtrele",
"sort_order": "Sıralama düzeni", "sort_order": "Sıralama düzeni",
"tags": "Etiketler", "tags": "Etiketler"
"all": "All",
"reset": "Reset",
"asc": "Ascending",
"desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -682,8 +591,6 @@
"no_links": "Bağlantı yok" "no_links": "Bağlantı yok"
}, },
"player": { "player": {
"live": "LIVE",
"mpv_player_title": "MPV Player",
"error": "Hata", "error": "Hata",
"failed_to_get_stream_url": "Yayın URL'si alınamadı", "failed_to_get_stream_url": "Yayın URL'si alınamadı",
"an_error_occured_while_playing_the_video": "Video oynatılırken bir hata oluştu. Ayarlardaki günlüklere bakın.", "an_error_occured_while_playing_the_video": "Video oynatılırken bir hata oluştu. Ayarlardaki günlüklere bakın.",
@@ -701,35 +608,7 @@
"downloaded_file_message": "İndirilmiş dosyayı oynatmak ister misiniz?", "downloaded_file_message": "İndirilmiş dosyayı oynatmak ister misiniz?",
"downloaded_file_yes": "Evet", "downloaded_file_yes": "Evet",
"downloaded_file_no": "Hayır", "downloaded_file_no": "Hayır",
"downloaded_file_cancel": "Vazgeç", "downloaded_file_cancel": "Vazgeç"
"swipe_down_settings": "Swipe down for settings",
"ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download",
"download": "Download",
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server",
"language": "Language",
"results": "Results",
"searching": "Searching...",
"search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found",
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro",
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Downloaded"
},
"chapters": {
"title": "Chapters",
"chapter_number": "Chapter {{number}}",
"open": "Open chapters",
"close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Sıradaki", "next_up": "Sıradaki",
@@ -738,11 +617,6 @@
"series": "Dizi", "series": "Dizi",
"seasons": "Sezonlar", "seasons": "Sezonlar",
"season": "Sezon", "season": "Sezon",
"from_this_series": "From This Series",
"more_from_this_season": "More from this Season",
"view_series": "View Series",
"view_season": "View Season",
"select_season": "Select Season",
"no_episodes_for_this_season": "Bu sezona ait bölüm yok", "no_episodes_for_this_season": "Bu sezona ait bölüm yok",
"overview": "Özet", "overview": "Özet",
"more_with": "Daha fazla {{name}}", "more_with": "Daha fazla {{name}}",
@@ -753,21 +627,10 @@
"media_options": "Medya Seçenekleri", "media_options": "Medya Seçenekleri",
"quality": "Kalite", "quality": "Kalite",
"audio": "Ses", "audio": "Ses",
"subtitles": { "subtitles": "Altyazı",
"label": "Subtitle",
"none": "None",
"tracks": "Tracks"
},
"show_more": "Daha fazla göster", "show_more": "Daha fazla göster",
"show_less": "Daha az göster", "show_less": "Daha az göster",
"left": "left",
"more_info": "More Info",
"director": "Director",
"cast": "Cast",
"technical_details": "Technical Details",
"appeared_in": "Şurada yer aldı", "appeared_in": "Şurada yer aldı",
"movies": "Movies",
"shows": "Shows",
"could_not_load_item": "Öge yüklenemedi", "could_not_load_item": "Öge yüklenemedi",
"none": "Hiçbiri", "none": "Hiçbiri",
"download": { "download": {
@@ -778,13 +641,7 @@
"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": "Yalnızca İzlenmemişler",
"download_button": "İndir" "download_button": "İndir"
}, }
"mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback",
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start",
"continue_from": "Continue from {{time}}"
}, },
"live_tv": { "live_tv": {
"next": "Sonraki", "next": "Sonraki",
@@ -795,18 +652,7 @@
"movies": "Filmler", "movies": "Filmler",
"sports": "Spor", "sports": "Spor",
"for_kids": "Çocuklar İçin", "for_kids": "Çocuklar İçin",
"news": "Haberler", "news": "Haberler"
"page_of": "Page {{current}} of {{total}}",
"no_programs": "No programs available",
"no_channels": "No channels available",
"tabs": {
"programs": "Programs",
"guide": "Guide",
"channels": "Channels",
"recordings": "Recordings",
"schedule": "Schedule",
"series": "Series"
}
}, },
"jellyseerr": { "jellyseerr": {
"confirm": "Onayla", "confirm": "Onayla",
@@ -851,12 +697,6 @@
"decline": "Reddet", "decline": "Reddet",
"requested_by": "{{user}} tarafından istendi", "requested_by": "{{user}} tarafından istendi",
"unknown_user": "Bilinmeyen Kullanıcı", "unknown_user": "Bilinmeyen Kullanıcı",
"select": "Select",
"request_all": "Request All",
"request_seasons": "Request Seasons",
"select_seasons": "Select Seasons",
"request_selected": "Request Selected",
"n_selected": "{{count}} selected",
"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.",
@@ -876,8 +716,7 @@
"search": "Ara", "search": "Ara",
"library": "Kütüphane", "library": "Kütüphane",
"custom_links": "Özel Bağlantılar", "custom_links": "Özel Bağlantılar",
"favorites": "Favoriler", "favorites": "Favoriler"
"settings": "Settings"
}, },
"music": { "music": {
"title": "Müzik", "title": "Müzik",
@@ -1002,36 +841,5 @@
"show": "Bu dizide", "show": "Bu dizide",
"all": "Bütün medyalarda (varsayılan)" "all": "Bütün medyalarda (varsayılan)"
} }
},
"companion_login": {
"title": "Pair with TV",
"align_qr": "Align the QR code within the frame",
"enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Pairing code",
"server": "Server",
"authorize_button": "Authorize",
"authorizing": "Authorizing...",
"scan_again": "Scan Again",
"done": "Done",
"success_title": "Authorization Sent",
"pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed",
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Log in as {{username}}?",
"on_server": "on {{server}}",
"use_different_user": "Use a different user",
"open_settings": "Open Settings"
},
"pairing": {
"pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV",
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
"waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Logging in...",
"logging_in_description": "Connecting to your server"
} }
} }

Some files were not shown because too many files have changed in this diff Show More