Compare commits

..

24 Commits

Author SHA1 Message Date
Gauvain
c5d39d40b8 Merge branch 'develop' into fix/subtitle-track-selection 2026-07-14 19:45:57 +02:00
Gauvain
b59759638c fix(subtitles): normalize local subtitle sentinel to -1 in stream re-negotiation URLs 2026-07-08 18:14:07 +02:00
Gauvain
8219e44b5b Merge branch 'develop' into fix/subtitle-track-selection 2026-07-07 23:27:23 +02:00
Gauvain
376ccd1f93 Merge branch 'develop' into fix/subtitle-track-selection 2026-07-07 00:33:51 +02:00
Gauvain
5221ed1963 fix(tv): only defer subtitle selection when it navigates
The close-first + runAfterInteractions pattern is needed only in the player,
where selecting a subtitle can navigate (replacePlayer for a burn-in switch
while transcoding). On the item detail page the selection just updates state, so
deferring it until after the modal is dismissed re-renders the page after focus
returns and yanks TV focus — leaving navigation stuck. Gate the deferral behind
deferApplyUntilDismissed (set by the in-player caller only); the detail page runs
the selection before closing, preserving focus.
2026-07-07 00:33:09 +02:00
Gauvain
461d055c7a chore(subtitles): address review nitpicks
Sort streams with a missing Index to the end of the menu (not to the top via 0,
which collides with a real Index 0). Reword the native ff-index comment to say
it is exposed for future/diagnostic use, not consumed by the current resolver.
Revert the incidental edit to the unused TVSubtitleSheet (removed separately in
the dead-code PR #1780).
2026-07-07 00:24:12 +02:00
Gauvain
58f3646477 fix(subtitles): teardown mpv before re-navigation and record the real local-sub index
Destroy the mpv instance before the track-change router.replace in
replaceWithTrackSelection and goToPreviousItem, matching goToNextItem — two
overlapping decoders/surfaces during the transition OOM low-RAM devices
(CodeRabbit). addSubtitleFile now sets the live index to the downloaded local
sub's real index (LOCAL_SUBTITLE_INDEX_START - position) instead of a blanket
-100, which collided with the first local sub and mis-recorded the selection
(Copilot); it still resolves to notFound on the onTracksReady re-apply so it
doesn't clobber the freshly selected track.
2026-07-07 00:23:38 +02:00
Gauvain
33ca07ca82 fix(tv): apply subtitle selection after the modal route is dismissed
The TV subtitle modal is a separate route; running the selection callback before
router.back() meant a navigating selection (replacePlayer for a burned-in switch
while transcoding) targeted the modal route and was swallowed. Close first, then
run the callback via InteractionManager.runAfterInteractions.
2026-07-06 23:48:49 +02:00
Gauvain
e5d56d6ad1 fix(subtitles): re-negotiate the stream for unresolvable and burned tracks
TV subtitle changes now honor the same transcode guard as the mobile menu
(switching to/from a burned-in image sub re-negotiates the stream instead of a
no-op track change). Menu-listed tracks the player can't select (server-burned
Encode, sidecars never sub-added) fall back to replacePlayer so the server
delivers them. Freshly downloaded local subs move the live index to the local
sentinel before the sub-add so onTracksReady doesn't clobber them. Shares the
external-URL builder and the LOCAL_SUBTITLE_INDEX_START constant.
2026-07-06 23:48:35 +02:00
Gauvain
80602ecd23 fix(mpv): re-emit tracksReady after FILE_LOADED to win the disable race
Initial subtitle selection now comes from JS on tracksReady; FILE_LOADED still
disables subtitles, which could wipe a JS selection that landed first on
embedded-only files (no later track-list change to re-fire). Re-emitting
tracksReady after the disable guarantees the idempotent JS re-apply always runs
last. Also guards the iOS track-id read against failure, matching Android.
2026-07-06 23:48:22 +02:00
Gauvain
14ea18e0d2 fix(subtitles): resolve duplicate-language and burned-in tracks by identity
Group-ordinal within the same-identity set (a preceding different-language
track no longer makes the first duplicate unreachable), exclude server-burned
(Encode) streams from the embedded ordinal and signal them as burnedIn, match
languages across ISO 639-1/639-2 B-T/IETF variants, and sort the menu by the
raw IsExternal flag so the order stays stable between direct play and transcode.
Adds getExternalSubtitleUrl as the single source of truth for the external-sub
URL, and short-circuits disable/burnedIn before enumerating the track list.
2026-07-06 23:48:01 +02:00
Gauvain
28fcb303c6 Merge branch 'develop' into fix/subtitle-track-selection 2026-06-30 14:36:11 +02:00
lance chant
90e9084949 Merge branch 'develop' into fix/subtitle-track-selection 2026-06-30 10:03:52 +02:00
Gauvain
115c163aeb Merge branch 'develop' into fix/subtitle-track-selection 2026-06-30 09:04:31 +02:00
Gauvain
a58a4da4f3 fix(subtitles): guard track-list effect against stale async runs
The track-building effect in VideoContext reruns once api?.basePath and
isCurrentSubImageBased settle. An earlier async run could resolve after a
rerun and overwrite subtitleTracks/audioTracks with setTrack callbacks bound
to a stale `api`, breaking external-subtitle identity matching.

Add a cancellation token and route every state commit through guarded
committers so all six commit points (offline-transcoded audio/subs,
burned-in, and the online audio/subs paths) drop writes from a dead run,
plus bail out right after the awaited getAudioTracks when cancelled.
2026-06-30 01:56:58 +02:00
Gauvain
c02baf2831 fix(subtitles): rebuild track callbacks when isCurrentSubImageBased changes
The setTrack/Disable callbacks close over isCurrentSubImageBased for the
transcode replacePlayer decision; add it to the track-building effect deps so
they rebuild when it flips (otherwise, in a transcoding session, callbacks could
stay on the MPV path after switching to/from a burned-in image sub and the
player would not refresh). Addresses CodeRabbit.
2026-06-30 01:41:21 +02:00
Gauvain
3848877021 fix(subtitles): only show the burned-in entry for image subs in offline transcoded
When an image subtitle was burned into a transcoded download it lives in the
video pixels and can't be disabled or swapped. Show only that '(burned in)'
entry instead of advertising Disable/text controls whose handlers can't affect
it (which would let the UI show a different selection than what's on screen).
Addresses CodeRabbit.
2026-06-30 01:32:43 +02:00
Gauvain
1f54ccc52c fix(subtitles): rebuild VideoContext track callbacks when api.basePath changes
The setTrack callbacks build external-sub URLs from api?.basePath; add it to the
track-building effect deps so the list rebuilds once the API is ready (otherwise
online externals could resolve with undefined → notFound). Addresses CodeRabbit.
2026-06-30 01:19:18 +02:00
Gauvain
08efa1b0f7 Merge branch 'develop' into fix/subtitle-track-selection 2026-06-30 01:10:59 +02:00
Gauvain
90ea934548 fix(subtitles): address CodeRabbit review
- Unify external detection: isExternalSubtitle drops the bare-DeliveryUrl case
  (an Hls-delivered sub has a DeliveryUrl but isn't sub-add-ed) so sorting,
  loading and resolution agree; compareTracksForMenu now uses it.
- applyMpvSubtitleSelection wraps player calls in try/catch — fire-and-forget
  call sites no longer risk unhandled rejections.
- VideoContext offline-transcoded branch: treat missing IsTextSubtitleStream as
  text (use !isImageBasedSubtitle), matching the shared helper.
- ItemContent.tv refreshSubtitleTracks: apply compareTracksForMenu like the
  initial list.
- Tests: use the @/ alias; rework the embedded cases to actually exercise
  identity (reversed player order) and the ordinal fallback (same-language,
  no title).
2026-06-30 01:08:50 +02:00
Gauvain
1c158dea4e fix(subtitles): order detail-page & TV subtitle menus like jellyfin-web
The detail-page selector (MediaSourceButton, the #1176 replacement for
TrackSheet) and the TV detail/refresh paths (ItemContent.tv, Controls.tv
refreshSubtitleTracks) still listed subtitles in raw MediaStreams order
(externals first). Apply compareTracksForMenu there too so every menu
matches web. The in-player TV modal was already covered (fed from the
sorted VideoContext tracks).
2026-06-30 00:39:29 +02:00
Gauvain
9a7b9c9de2 fix(subtitles): select subtitles by identity across all player paths
direct-player resolves the selection on onTracksReady (online + offline, init +
runtime), VideoContext does the same for the mobile menu (incl. offline-transcoded),
and the menus (SubtitleTrackSelector, VideoContext, TVSubtitleSheet) now order
tracks like jellyfin-web. Fixes wrong-subtitle/wrong-language selection.

Fixes #954
2026-06-30 00:11:45 +02:00
Gauvain
ceeacda7f9 feat(subtitles): identity-based track resolver + jellyfin-web menu order
resolveSubtitleTrack matches a Jellyfin subtitle against the player's real
track list (external by external-filename, embedded by language/title) instead
of positional counting, which mis-selects when externals/embedded are reordered
or the server hides embedded subs. applyMpvSubtitleSelection is the shared entry
point (reusable for the cast backend). compareTracksForMenu mirrors web's
itemHelper.sortTracks. Drops dead getMpvSubtitleId/isSubtitleInMpv. 14 unit tests.
2026-06-30 00:11:12 +02:00
Gauvain
b8780f34ec feat(mpv): expose external/external-filename/ff-index/codec on subtitle track-list
These identity fields let the JS layer map a Jellyfin subtitle to the real
MPV track instead of guessing a positional sid.
2026-06-30 00:10:57 +02:00
67 changed files with 2453 additions and 1725 deletions

View File

@@ -71,7 +71,7 @@ export default function AppearanceHideLibrariesPage() {
))} ))}
</ListGroup> </ListGroup>
<Text className='px-4 text-xs text-neutral-500 mt-1'> <Text className='px-4 text-xs text-neutral-500 mt-1'>
{t("home.settings.other.select_libraries_you_want_to_hide")} {t("home.settings.other.select_liraries_you_want_to_hide")}
</Text> </Text>
</DisabledSetting> </DisabledSetting>
</ScrollView> </ScrollView>

View File

@@ -60,7 +60,7 @@ export default function HideLibrariesPage() {
))} ))}
</ListGroup> </ListGroup>
<Text className='px-4 text-xs text-neutral-500 mt-1'> <Text className='px-4 text-xs text-neutral-500 mt-1'>
{t("home.settings.other.select_libraries_you_want_to_hide")} {t("home.settings.other.select_liraries_you_want_to_hide")}
</Text> </Text>
</DisabledSetting> </DisabledSetting>
); );

View File

@@ -7,7 +7,6 @@ import {
RepeatMode, RepeatMode,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import { import {
getMediaInfoApi,
getPlaystateApi, getPlaystateApi,
getUserLibraryApi, getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api"; } from "@jellyfin/sdk/lib/utils/api";
@@ -26,6 +25,10 @@ import { Controls } from "@/components/video-player/controls/Controls";
import { Controls as TVControls } from "@/components/video-player/controls/Controls.tv"; import { Controls as TVControls } from "@/components/video-player/controls/Controls.tv";
import { PlayerProvider } from "@/components/video-player/controls/contexts/PlayerContext"; import { PlayerProvider } from "@/components/video-player/controls/contexts/PlayerContext";
import { VideoProvider } from "@/components/video-player/controls/contexts/VideoContext"; import { VideoProvider } from "@/components/video-player/controls/contexts/VideoContext";
import {
LOCAL_SUBTITLE_INDEX_START,
toServerSubtitleIndex,
} from "@/components/video-player/controls/types";
import { import {
PlaybackSpeedScope, PlaybackSpeedScope,
updatePlaybackSpeedSettings, updatePlaybackSpeedSettings,
@@ -50,15 +53,16 @@ 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 { 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";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { import {
applyMpvSubtitleSelection,
getExternalSubtitleUrl,
getMpvAudioId, getMpvAudioId,
getMpvSubtitleId, isImageBasedSubtitle,
} from "@/utils/jellyfin/subtitleUtils"; } from "@/utils/jellyfin/subtitleUtils";
import { writeToLog } from "@/utils/log"; import { writeToLog } from "@/utils/log";
import { msToTicks, ticksToSeconds } from "@/utils/time"; import { msToTicks, ticksToSeconds } from "@/utils/time";
@@ -311,37 +315,6 @@ export default function DirectPlayerPage() {
// Ref to store the stream fetch function for refreshing subtitle tracks // Ref to store the stream fetch function for refreshing subtitle tracks
const refetchStreamRef = useRef<(() => Promise<Stream | null>) | null>(null); const refetchStreamRef = useRef<(() => Promise<Stream | null>) | null>(null);
// Live TV opens a server-side live stream via autoOpenLiveStream. If it is
// never closed, Jellyfin's M3U tuner limit fills up and every channel then
// fails with "simultaneous stream limit has been reached". reportPlaybackStopped
// handles a clean stop, but a channel switch (the player re-fetches in place)
// or an unmount after an error bypass it, so track the open live stream and
// release it on those paths too.
const apiRef = useRef(api);
useEffect(() => {
apiRef.current = api;
}, [api]);
const releaseLiveStream = useCallback(
(liveStreamId: string | null) => {
if (!liveStreamId || !apiRef.current || offline) return;
// Best effort: a failed close must not break teardown, and the slot is
// also freed by the server's reap as a backstop.
getMediaInfoApi(apiRef.current)
.closeLiveStream({ liveStreamId })
.catch(() => {});
},
[offline],
);
// The effect cleanup releases the live stream both when it changes (channel
// switch, which re-runs the effect) and when the player unmounts, so no
// manual previous-id tracking is needed.
useEffect(() => {
const liveStreamId = stream?.mediaSource?.LiveStreamId ?? null;
return () => releaseLiveStream(liveStreamId);
}, [stream?.mediaSource?.LiveStreamId, releaseLiveStream]);
useEffect(() => { useEffect(() => {
const fetchStreamData = async (): Promise<Stream | null> => { const fetchStreamData = async (): Promise<Stream | null> => {
setStreamStatus({ isLoading: true, isError: false }); setStreamStatus({ isLoading: true, isError: false });
@@ -477,12 +450,6 @@ export default function DirectPlayerPage() {
MediaSourceId: mediaSourceId, MediaSourceId: mediaSourceId,
PositionTicks: currentTimeInTicks, PositionTicks: currentTimeInTicks,
PlaySessionId: stream.sessionId, PlaySessionId: stream.sessionId,
// Release the server-side live stream (and its tuner slot) on stop.
// Jellyfin only closes a live stream opened via autoOpenLiveStream when
// the stop report carries its LiveStreamId; without it the stream leaks
// and Live TV eventually fails for everyone with "M3U simultaneous
// stream limit has been reached". Undefined for non-live items (no-op).
LiveStreamId: stream.mediaSource?.LiveStreamId ?? undefined,
}, },
}); });
}, [api, item, mediaSourceId, stream, progress, offline]); }, [api, item, mediaSourceId, stream, progress, offline]);
@@ -657,32 +624,20 @@ export default function DirectPlayerPage() {
const mediaSource = stream.mediaSource; const mediaSource = stream.mediaSource;
const isTranscoding = Boolean(mediaSource?.TranscodingUrl); const isTranscoding = Boolean(mediaSource?.TranscodingUrl);
// Get external subtitle URLs // Get external subtitle URLs — getExternalSubtitleUrl is the shared source
// - Online: prepend API base path to server URLs // of truth with identity matching (online: basePath + DeliveryUrl unless
// - Offline: use local file paths (stored in DeliveryUrl during download) // IsExternalUrl; offline: local file path stored in DeliveryUrl).
let externalSubs: string[] | undefined; const externalSubs = mediaSource?.MediaStreams?.filter(
if (!offline && api?.basePath) { (s) => s.Type === "Subtitle" && s.DeliveryMethod === "External",
externalSubs = mediaSource?.MediaStreams?.filter( )
(s) => .map((s) =>
s.Type === "Subtitle" && getExternalSubtitleUrl(s, { offline, basePath: api?.basePath }),
s.DeliveryMethod === "External" && )
s.DeliveryUrl, .filter((u): u is string => !!u);
).map((s) => `${api.basePath}${s.DeliveryUrl}`);
} else if (offline) {
externalSubs = mediaSource?.MediaStreams?.filter(
(s) =>
s.Type === "Subtitle" &&
s.DeliveryMethod === "External" &&
s.DeliveryUrl,
).map((s) => s.DeliveryUrl!);
}
// Calculate track IDs for initial selection // Audio maps positionally (audio tracks aren't reordered or hidden like
const initialSubtitleId = getMpvSubtitleId( // subtitles). The subtitle selection is applied later, once MPV's real track
mediaSource, // list is known — see applySubtitleSelection / onTracksReady.
subtitleIndex,
isTranscoding,
);
const initialAudioId = getMpvAudioId( const initialAudioId = getMpvAudioId(
mediaSource, mediaSource,
audioIndex, audioIndex,
@@ -700,7 +655,6 @@ export default function DirectPlayerPage() {
url: stream.url, url: stream.url,
startPosition: startPos, startPosition: startPos,
autoplay: true, autoplay: true,
initialSubtitleId,
initialAudioId, initialAudioId,
// Pass cache/buffer settings from user preferences // Pass cache/buffer settings from user preferences
cacheConfig: { cacheConfig: {
@@ -748,7 +702,6 @@ export default function DirectPlayerPage() {
playbackPositionFromUrl, playbackPositionFromUrl,
api?.basePath, api?.basePath,
api?.accessToken, api?.accessToken,
subtitleIndex,
audioIndex, audioIndex,
offline, offline,
settings.mpvCacheEnabled, settings.mpvCacheEnabled,
@@ -938,7 +891,9 @@ export default function DirectPlayerPage() {
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
itemId: item?.Id ?? "", itemId: item?.Id ?? "",
audioIndex: String(index), audioIndex: String(index),
subtitleIndex: String(currentSubtitleIndex), // A local (client-downloaded) sub only exists in the dying mpv
// instance — the server must be asked for "none" (-1) instead.
subtitleIndex: String(toServerSubtitleIndex(currentSubtitleIndex)),
mediaSourceId: stream?.mediaSource?.Id ?? "", mediaSourceId: stream?.mediaSource?.Id ?? "",
bitrateValue: bitrateValue?.toString() ?? "", bitrateValue: bitrateValue?.toString() ?? "",
playbackPosition: msToTicks(progress.get()).toString(), playbackPosition: msToTicks(progress.get()).toString(),
@@ -974,30 +929,103 @@ export default function DirectPlayerPage() {
); );
// TV subtitle track change handler // TV subtitle track change handler
/**
* Resolve a Jellyfin subtitle index against MPV's *real* track list and apply
* it. Identity-based (external by filename, embedded by language/title) so it
* stays correct across external/embedded reordering and server-hidden embedded
* subs — unlike positional mapping. Reused for initial selection (onTracksReady,
* fired again after each external sub-add) and runtime changes.
*/
const applySubtitleSelection = useCallback(
async (jellyfinSubtitleIndex: number) => {
const subtitleStreams = stream?.mediaSource?.MediaStreams?.filter(
(s) => s.Type === "Subtitle",
);
return applyMpvSubtitleSelection(videoRef.current, {
subtitleStreams,
jellyfinSubtitleIndex,
getExpectedExternalUrl: (s) =>
getExternalSubtitleUrl(s, { offline, basePath: api?.basePath }),
});
},
[stream?.mediaSource, offline, api?.basePath],
);
// Re-negotiate the stream with new track params (server re-processes it,
// e.g. to burn an image sub in or out). Same-item mirror of VideoContext's
// replacePlayer, resuming at the live position.
const replaceWithTrackSelection = useCallback(
(params: { subtitleIndex?: string; audioIndex?: string }) => {
const queryParams = new URLSearchParams({
itemId: item?.Id ?? "",
audioIndex: params.audioIndex ?? String(currentAudioIndex ?? ""),
subtitleIndex:
params.subtitleIndex ??
String(toServerSubtitleIndex(currentSubtitleIndex)),
mediaSourceId: stream?.mediaSource?.Id ?? "",
bitrateValue: bitrateValue?.toString() ?? "",
playbackPosition: msToTicks(progress.get()).toString(),
}).toString();
// Destroy the current mpv instance before re-navigating, same rationale as
// goToNextItem: Expo Router briefly holds two players during the
// transition and two decoders/surfaces OOM-kill low-RAM devices.
videoRef.current?.destroy().catch(() => {});
router.replace(`player/direct-player?${queryParams}` as any);
},
[
item?.Id,
currentAudioIndex,
currentSubtitleIndex,
stream?.mediaSource?.Id,
bitrateValue,
router,
progress,
],
);
// TV/mobile subtitle track change handler
const handleSubtitleIndexChange = useCallback( const handleSubtitleIndexChange = useCallback(
async (index: number) => { async (index: number) => {
setCurrentSubtitleIndex(index); // Local (client-downloaded) subs are loaded via addSubtitleFile, not
// resolvable against server streams — just track the live index.
if (index <= LOCAL_SUBTITLE_INDEX_START) {
setCurrentSubtitleIndex(index);
return;
}
// Check if we're transcoding const subs = stream?.mediaSource?.MediaStreams?.filter(
(s) => s.Type === "Subtitle",
);
const isTranscoding = Boolean(stream?.mediaSource?.TranscodingUrl); const isTranscoding = Boolean(stream?.mediaSource?.TranscodingUrl);
const target = subs?.find((s) => s.Index === index);
const current = subs?.find((s) => s.Index === currentSubtitleIndex);
// Burned-in subs are pixels, not tracks: switching TO one and switching
// AWAY from an active one both need a server re-process (same guard as
// VideoContext's needsReplace on the mobile menu path).
const needsReplace =
isTranscoding &&
((target && isImageBasedSubtitle(target)) ||
(current && isImageBasedSubtitle(current)));
if (needsReplace) {
replaceWithTrackSelection({ subtitleIndex: String(index) });
return;
}
if (index === -1) { setCurrentSubtitleIndex(index);
// Disable subtitles const result = await applySubtitleSelection(index);
await videoRef.current?.disableSubtitles?.(); // Safety net: a menu-listed sub the player can't select (server-burned
} else { // Encode, sidecar never sub-added) needs the server to re-process the
// Convert Jellyfin index to MPV track ID // stream with it.
const mpvTrackId = getMpvSubtitleId( if (result.kind === "notFound" || result.kind === "burnedIn") {
stream?.mediaSource, replaceWithTrackSelection({ subtitleIndex: String(index) });
index,
isTranscoding,
);
if (mpvTrackId !== undefined && mpvTrackId !== -1) {
await videoRef.current?.setSubtitleTrack?.(mpvTrackId);
}
} }
}, },
[stream?.mediaSource], [
applySubtitleSelection,
replaceWithTrackSelection,
stream?.mediaSource,
currentSubtitleIndex,
],
); );
// Technical info toggle handler // Technical info toggle handler
@@ -1116,6 +1144,10 @@ export default function DirectPlayerPage() {
previousItem.UserData?.PlaybackPositionTicks?.toString() ?? "", previousItem.UserData?.PlaybackPositionTicks?.toString() ?? "",
}).toString(); }).toString();
// Free the current mpv instance before navigating, matching goToNextItem —
// otherwise two decoders/surfaces overlap during the transition and can
// OOM-kill low-RAM devices.
videoRef.current?.destroy().catch(() => {});
router.replace(`player/direct-player?${queryParams}` as any); router.replace(`player/direct-player?${queryParams}` as any);
}, [ }, [
previousItem, previousItem,
@@ -1128,9 +1160,24 @@ export default function DirectPlayerPage() {
]); ]);
// TV: Add subtitle file to player (for client-side downloaded subtitles) // TV: Add subtitle file to player (for client-side downloaded subtitles)
const addSubtitleFile = useCallback(async (path: string) => { const addSubtitleFile = useCallback(
await videoRef.current?.addSubtitleFile?.(path, true); async (path: string) => {
}, []); // Set the live index to the new local sub's REAL index BEFORE the add.
// Local subs are keyed LOCAL_SUBTITLE_INDEX_START - position, so use the
// downloaded path's position (not a blanket sentinel, which would collide
// with the first local sub at -100 and mis-record the selection). Any
// local index resolves to notFound on the onTracksReady re-apply, so it
// still doesn't clobber the freshly selected track; carry-over now keeps
// the correct local sub.
const locals = itemId ? getSubtitlesForItem(itemId) : [];
const pos = locals.findIndex((s) => s.filePath === path);
setCurrentSubtitleIndex(
LOCAL_SUBTITLE_INDEX_START - (pos >= 0 ? pos : 0),
);
await videoRef.current?.addSubtitleFile?.(path, true);
},
[itemId],
);
// TV: Refresh subtitle tracks after server-side subtitle download // TV: Refresh subtitle tracks after server-side subtitle download
// Re-fetches the media source to pick up newly downloaded subtitles // Re-fetches the media source to pick up newly downloaded subtitles
@@ -1356,12 +1403,16 @@ export default function DirectPlayerPage() {
console.error("Video Error:", e.nativeEvent); console.error("Video Error:", e.nativeEvent);
Alert.alert( Alert.alert(
t("player.error"), t("player.error"),
t("player.an_error_occurred_while_playing_the_video"), t("player.an_error_occured_while_playing_the_video"),
); );
writeToLog("ERROR", "Video Error", e.nativeEvent); writeToLog("ERROR", "Video Error", e.nativeEvent);
}} }}
onTracksReady={() => { onTracksReady={() => {
setTracksReady(true); setTracksReady(true);
// Fired after embedded tracks enumerate and again after each
// external sub-add; re-resolve so the final fire (full track
// list) selects the right track by identity.
void applySubtitleSelection(currentSubtitleIndex);
}} }}
/> />
{!hasPlaybackStarted && ( {!hasPlaybackStarted && (

View File

@@ -13,6 +13,7 @@ import {
ActivityIndicator, ActivityIndicator,
Animated, Animated,
Easing, Easing,
InteractionManager,
Pressable, Pressable,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
@@ -645,10 +646,23 @@ export default function TVSubtitleModal() {
const handleTrackSelect = useCallback( const handleTrackSelect = useCallback(
(option: { setTrack?: () => void }) => { (option: { setTrack?: () => void }) => {
if (modalState?.deferApplyUntilDismissed) {
// Player: setTrack can navigate (replacePlayer for a burn-in switch
// while transcoding); a router.replace fired while this modal is the
// active route targets the MODAL and is swallowed. Close FIRST, apply
// after dismissal.
handleClose();
InteractionManager.runAfterInteractions(() => option.setTrack?.());
return;
}
// Detail page: setTrack only updates state. Run it BEFORE closing so the
// re-render happens while the modal is up; deferring it until after
// dismissal re-renders the detail page after focus returns and yanks TV
// focus, leaving navigation stuck.
option.setTrack?.(); option.setTrack?.();
handleClose(); handleClose();
}, },
[handleClose], [handleClose, modalState?.deferApplyUntilDismissed],
); );
const handleDownload = useCallback( const handleDownload = useCallback(

View File

@@ -27,7 +27,6 @@ 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 { WebSocketProvider } from "@/providers/WebSocketProvider"; import { WebSocketProvider } from "@/providers/WebSocketProvider";
import { WifiSsidProvider } from "@/providers/WifiSsidProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { import {
BACKGROUND_FETCH_TASK, BACKGROUND_FETCH_TASK,
@@ -86,8 +85,7 @@ configureReanimatedLogger({
if (!Platform.isTV) { if (!Platform.isTV) {
Notifications.setNotificationHandler({ Notifications.setNotificationHandler({
handleNotification: async () => ({ handleNotification: async () => ({
shouldShowBanner: true, shouldShowAlert: true,
shouldShowList: true,
shouldPlaySound: true, shouldPlaySound: true,
shouldSetBadge: false, shouldSetBadge: false,
}), }),
@@ -352,12 +350,9 @@ function Layout() {
notificationListener.current = notificationListener.current =
Notifications?.addNotificationReceivedListener( Notifications?.addNotificationReceivedListener(
(notification: Notification) => { (notification: Notification) => {
// Log only the title — serializing the whole notification touches
// the deprecated dataString getter (deprecation warning) and dumps
// noisy payloads into the console.
console.log( console.log(
"Notification received while app running:", "Notification received while app running",
notification.request.content.title, notification,
); );
}, },
); );
@@ -426,146 +421,143 @@ function Layout() {
> >
<JellyfinProvider> <JellyfinProvider>
<InactivityProvider> <InactivityProvider>
<WifiSsidProvider> <ServerUrlProvider>
<ServerUrlProvider> <NetworkStatusProvider>
<NetworkStatusProvider> <PlaySettingsProvider>
<PlaySettingsProvider> <LogProvider>
<LogProvider> <WebSocketProvider>
<WebSocketProvider> <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> }}
</WebSocketProvider> />
</LogProvider> <Stack.Screen
</PlaySettingsProvider> name='(auth)/now-playing'
</NetworkStatusProvider> options={{
</ServerUrlProvider> headerShown: false,
</WifiSsidProvider> 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>
</LogProvider>
</PlaySettingsProvider>
</NetworkStatusProvider>
</ServerUrlProvider>
</InactivityProvider> </InactivityProvider>
</JellyfinProvider> </JellyfinProvider>
</PersistQueryClientProvider> </PersistQueryClientProvider>

View File

@@ -35,10 +35,8 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`; return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
} }
if (item.Type === "Episode") { if (item.Type === "Episode") {
// Matched pair: the parent that owns the Thumb (ParentThumbItemId), not the if (item.ParentBackdropItemId && item.ParentThumbImageTag) {
// backdrop owner — otherwise the Thumb tag is requested on the wrong item → black. return `${api?.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ParentThumbImageTag}`;
if (item.ParentThumbItemId && item.ParentThumbImageTag) {
return `${api?.basePath}/Items/${item.ParentThumbItemId}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ParentThumbImageTag}`;
} }
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`; return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
@@ -63,8 +61,7 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
} }
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`; return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
// useEpisodePoster in deps so flipping the prop re-computes the URL live. }, [item]);
}, [api, item, useEpisodePoster]);
if (!url) if (!url)
return <View className='aspect-video border border-neutral-800 w-44' />; return <View className='aspect-video border border-neutral-800 w-44' />;

View File

@@ -40,7 +40,10 @@ import {
TVSeriesNavigation, TVSeriesNavigation,
TVTechnicalDetails, TVTechnicalDetails,
} from "@/components/tv"; } from "@/components/tv";
import type { Track } from "@/components/video-player/controls/types"; import {
LOCAL_SUBTITLE_INDEX_START,
type Track,
} from "@/components/video-player/controls/types";
import { useScaledTVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings"; import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
@@ -56,6 +59,7 @@ import { useSettings } from "@/utils/atoms/settings";
import type { TVOptionItem } from "@/utils/atoms/tvOptionModal"; import type { TVOptionItem } from "@/utils/atoms/tvOptionModal";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils";
import { formatDuration, runtimeTicksToMinutes } from "@/utils/time"; import { formatDuration, runtimeTicksToMinutes } from "@/utils/time";
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window"); const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
@@ -232,12 +236,13 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
return streams ?? []; return streams ?? [];
}, [selectedOptions?.mediaSource]); }, [selectedOptions?.mediaSource]);
// Get available subtitle tracks (raw MediaStream[] for label lookup) // Get available subtitle tracks (raw MediaStream[] for label lookup),
// ordered like jellyfin-web (embedded first, externals last, forced/default up).
const subtitleStreams = useMemo(() => { const subtitleStreams = useMemo(() => {
const streams = selectedOptions?.mediaSource?.MediaStreams?.filter( const streams = selectedOptions?.mediaSource?.MediaStreams?.filter(
(s) => s.Type === "Subtitle", (s) => s.Type === "Subtitle",
); );
return streams ?? []; return streams ? [...streams].sort(compareTracksForMenu) : [];
}, [selectedOptions?.mediaSource]); }, [selectedOptions?.mediaSource]);
// Store handleSubtitleChange in a ref for stable callback reference // Store handleSubtitleChange in a ref for stable callback reference
@@ -248,9 +253,6 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
// State to trigger refresh of local subtitles list // State to trigger refresh of local subtitles list
const [localSubtitlesRefreshKey, setLocalSubtitlesRefreshKey] = useState(0); const [localSubtitlesRefreshKey, setLocalSubtitlesRefreshKey] = useState(0);
// Starting index for local (client-downloaded) subtitles
const LOCAL_SUBTITLE_INDEX_START = -100;
// Convert MediaStream[] to Track[] for the modal (with setTrack callbacks) // Convert MediaStream[] to Track[] for the modal (with setTrack callbacks)
// Also includes locally downloaded subtitles from OpenSubtitles // Also includes locally downloaded subtitles from OpenSubtitles
const subtitleTracksForModal = useMemo((): Track[] => { const subtitleTracksForModal = useMemo((): Track[] => {
@@ -411,11 +413,13 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
) )
: freshItem.MediaSources?.[0]; : freshItem.MediaSources?.[0];
// Get subtitle streams from the fresh data // Get subtitle streams from the fresh data, ordered like jellyfin-web
const streams = // (embedded first, externals last) — same as the initial list.
mediaSource?.MediaStreams?.filter( const streams = [
...(mediaSource?.MediaStreams?.filter(
(s: MediaStream) => s.Type === "Subtitle", (s: MediaStream) => s.Type === "Subtitle",
) ?? []; ) ?? []),
].sort(compareTracksForMenu);
// Convert to Track[] with setTrack callbacks // Convert to Track[] with setTrack callbacks
const tracks: Track[] = streams.map((stream) => ({ const tracks: Track[] = streams.map((stream) => ({

View File

@@ -7,6 +7,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import type { ThemeColors } from "@/hooks/useImageColorsReturn"; import type { ThemeColors } from "@/hooks/useImageColorsReturn";
import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils";
import { BITRATES } from "./BitRateSheet"; import { BITRATES } from "./BitRateSheet";
import type { SelectedOptions } from "./ItemContent"; import type { SelectedOptions } from "./ItemContent";
import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown"; import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
@@ -63,9 +64,12 @@ export const MediaSourceButton: React.FC<Props> = ({
const subtitleStreams = useMemo( const subtitleStreams = useMemo(
() => () =>
selectedOptions.mediaSource?.MediaStreams?.filter( // Order like jellyfin-web (embedded first, externals last, forced/default up).
(x) => x.Type === "Subtitle", [
) || [], ...(selectedOptions.mediaSource?.MediaStreams?.filter(
(x) => x.Type === "Subtitle",
) || []),
].sort(compareTracksForMenu),
[selectedOptions.mediaSource], [selectedOptions.mediaSource],
); );

View File

@@ -2,6 +2,7 @@ import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models"
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View } from "react-native"; import { Platform, TouchableOpacity, View } from "react-native";
import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils";
import { tc } from "@/utils/textTools"; import { tc } from "@/utils/textTools";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown"; import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
@@ -22,7 +23,9 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const subtitleStreams = useMemo(() => { const subtitleStreams = useMemo(() => {
return source?.MediaStreams?.filter((x) => x.Type === "Subtitle"); const subs = source?.MediaStreams?.filter((x) => x.Type === "Subtitle");
// Order like jellyfin-web (embedded first, externals last, forced/default up).
return subs ? [...subs].sort(compareTracksForMenu) : subs;
}, [source]); }, [source]);
const selectedSubtitleSteam = useMemo( const selectedSubtitleSteam = useMemo(

View File

@@ -16,12 +16,9 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
const { showActionSheetWithOptions } = useActionSheet(); const { showActionSheetWithOptions } = useActionSheet();
const router = useRouter(); const router = useRouter();
// Keyed on SeriesId so recycled FlashList cells re-read the correct poster
// instead of freezing the first-rendered series' image (empty deps bug).
const base64Image = useMemo(() => { const base64Image = useMemo(() => {
const seriesId = items[0]?.SeriesId; return storage.getString(items[0].SeriesId!);
return seriesId ? storage.getString(seriesId) : undefined; }, []);
}, [items[0]?.SeriesId]);
const deleteSeries = useCallback( const deleteSeries = useCallback(
async () => async () =>

View File

@@ -65,11 +65,10 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
const posterUrl = useMemo(() => { const posterUrl = useMemo(() => {
if (!api) return null; if (!api) return null;
// For episodes, always use series thumb. // For episodes, always use series thumb
// Matched pair: ParentThumbItemId owns the Thumb tag, not ParentBackdropItemId.
if (item.Type === "Episode") { if (item.Type === "Episode") {
if (item.ParentThumbItemId && item.ParentThumbImageTag) { if (item.ParentThumbImageTag) {
return `${api.basePath}/Items/${item.ParentThumbItemId}/Images/Thumb?fillHeight=400&quality=80&tag=${item.ParentThumbImageTag}`; return `${api.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=400&quality=80&tag=${item.ParentThumbImageTag}`;
} }
if (item.SeriesId) { if (item.SeriesId) {
return `${api.basePath}/Items/${item.SeriesId}/Images/Thumb?fillHeight=400&quality=80`; return `${api.basePath}/Items/${item.SeriesId}/Images/Thumb?fillHeight=400&quality=80`;

View File

@@ -146,7 +146,7 @@ export const Login: React.FC = () => {
} else { } else {
Alert.alert( Alert.alert(
t("login.connection_failed"), t("login.connection_failed"),
t("login.an_unexpected_error_occurred"), t("login.an_unexpected_error_occured"),
); );
} }
} finally { } finally {

View File

@@ -437,7 +437,7 @@ export const TVLogin: React.FC = () => {
} else { } else {
Alert.alert( Alert.alert(
t("login.connection_failed"), t("login.connection_failed"),
t("login.an_unexpected_error_occurred"), t("login.an_unexpected_error_occured"),
); );
} }
} finally { } finally {
@@ -499,7 +499,7 @@ export const TVLogin: React.FC = () => {
const message = const message =
error instanceof Error error instanceof Error
? error.message ? error.message
: t("login.an_unexpected_error_occurred"); : t("login.an_unexpected_error_occured");
Alert.alert(t("login.connection_failed"), message); Alert.alert(t("login.connection_failed"), message);
goToQRScreen(); goToQRScreen();
} finally { } finally {
@@ -523,7 +523,7 @@ export const TVLogin: React.FC = () => {
} else { } else {
Alert.alert( Alert.alert(
t("login.connection_failed"), t("login.connection_failed"),
t("login.an_unexpected_error_occurred"), t("login.an_unexpected_error_occured"),
); );
} }
} finally { } finally {
@@ -768,7 +768,7 @@ export const TVLogin: React.FC = () => {
const message = const message =
error instanceof Error error instanceof Error
? error.message ? error.message
: t("login.an_unexpected_error_occurred"); : t("login.an_unexpected_error_occured");
Alert.alert(t("login.connection_failed"), message); Alert.alert(t("login.connection_failed"), message);
goToQRScreen(); goToQRScreen();
}); });

View File

@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import { Switch, TouchableOpacity, View } from "react-native"; import { Switch, TouchableOpacity, View } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { useWifiSSID } from "@/hooks/useWifiSSID"; import { useWifiSSID } from "@/hooks/useWifiSSID";
import { openLocationSettings } from "@/modules/wifi-ssid";
import { useServerUrl } from "@/providers/ServerUrlProvider"; import { useServerUrl } from "@/providers/ServerUrlProvider";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
import { import {
@@ -27,26 +26,16 @@ const DEFAULT_CONFIG: LocalNetworkConfig = {
interface StatusDisplayProps { interface StatusDisplayProps {
currentSSID: string | null; currentSSID: string | null;
connectedToWifi: boolean;
isUsingLocalUrl: boolean; isUsingLocalUrl: boolean;
locationBlocked: boolean;
onOpenLocationSettings: () => void;
t: (key: string) => string; t: (key: string) => string;
} }
function StatusDisplay({ function StatusDisplay({
currentSSID, currentSSID,
connectedToWifi,
isUsingLocalUrl, isUsingLocalUrl,
locationBlocked,
onOpenLocationSettings,
t, t,
}: StatusDisplayProps): React.ReactElement { }: StatusDisplayProps): React.ReactElement {
const wifiStatus = currentSSID const wifiStatus = currentSSID ?? t("home.settings.network.not_connected");
? currentSSID
: connectedToWifi
? t("home.settings.network.ssid_hidden")
: t("home.settings.network.not_connected");
const urlType = isUsingLocalUrl const urlType = isUsingLocalUrl
? t("home.settings.network.local") ? t("home.settings.network.local")
: t("home.settings.network.remote"); : t("home.settings.network.remote");
@@ -66,23 +55,6 @@ function StatusDisplay({
</Text> </Text>
<Text className={urlTypeColor}>{urlType}</Text> <Text className={urlTypeColor}>{urlType}</Text>
</View> </View>
{locationBlocked && (
<View className='mt-2 pt-2 border-t border-neutral-800'>
<Text className='text-xs text-amber-400'>
{t("home.settings.network.location_off_description")}
</Text>
<TouchableOpacity
onPress={onOpenLocationSettings}
className='mt-2 self-start'
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Text className='text-xs text-blue-400 font-semibold'>
{t("home.settings.network.open_location_settings")}
</Text>
</TouchableOpacity>
</View>
)}
</View> </View>
); );
} }
@@ -90,17 +62,7 @@ function StatusDisplay({
export function LocalNetworkSettings(): React.ReactElement | null { export function LocalNetworkSettings(): React.ReactElement | null {
const { t } = useTranslation(); const { t } = useTranslation();
const { permissionStatus, requestPermission } = useWifiSSID(); const { permissionStatus, requestPermission } = useWifiSSID();
const { isUsingLocalUrl, currentSSID, connectedToWifi, refreshUrlState } = const { isUsingLocalUrl, currentSSID, refreshUrlState } = useServerUrl();
useServerUrl();
// Connected to Wi-Fi and have permission, but the OS won't reveal the SSID —
// on Android this means device Location services are off.
const locationBlocked =
permissionStatus === "granted" && connectedToWifi && !currentSSID;
const handleOpenLocationSettings = useCallback(() => {
openLocationSettings();
}, []);
const remoteUrl = storage.getString("serverUrl"); const remoteUrl = storage.getString("serverUrl");
const [config, setConfig] = useState<LocalNetworkConfig>(DEFAULT_CONFIG); const [config, setConfig] = useState<LocalNetworkConfig>(DEFAULT_CONFIG);
@@ -233,23 +195,18 @@ export function LocalNetworkSettings(): React.ReactElement | null {
)} )}
</ListGroup> </ListGroup>
{!locationBlocked && ( <View className='py-2'>
<View className='py-2'> <Button
<Button onPress={handleAddCurrentNetwork}
onPress={handleAddCurrentNetwork} disabled={!currentSSID || permissionStatus !== "granted"}
disabled={!currentSSID || permissionStatus !== "granted"} >
> {addNetworkButtonText}
{addNetworkButtonText} </Button>
</Button> </View>
</View>
)}
<StatusDisplay <StatusDisplay
currentSSID={currentSSID} currentSSID={currentSSID}
connectedToWifi={connectedToWifi}
isUsingLocalUrl={isUsingLocalUrl} isUsingLocalUrl={isUsingLocalUrl}
locationBlocked={locationBlocked}
onOpenLocationSettings={handleOpenLocationSettings}
t={t} t={t}
/> />
</View> </View>

View File

@@ -58,7 +58,7 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
successHapticFeedback(); successHapticFeedback();
Alert.alert( Alert.alert(
t("home.settings.quick_connect.success"), t("home.settings.quick_connect.success"),
t("home.settings.quick_connect.quick_connect_authorized"), t("home.settings.quick_connect.quick_connect_autorized"),
); );
setQuickConnectCode(undefined); setQuickConnectCode(undefined);
bottomSheetModalRef?.current?.close(); bottomSheetModalRef?.current?.close();

View File

@@ -1,6 +1,6 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Alert, Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
@@ -12,7 +12,6 @@ import { ListItem } from "../list/ListItem";
export const StorageSettings = () => { export const StorageSettings = () => {
const { deleteAllFiles, appSizeUsage } = useDownload(); const { deleteAllFiles, appSizeUsage } = useDownload();
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient();
const successHapticFeedback = useHaptic("success"); const successHapticFeedback = useHaptic("success");
const errorHapticFeedback = useHaptic("error"); const errorHapticFeedback = useHaptic("error");
@@ -28,38 +27,16 @@ export const StorageSettings = () => {
used: (app.total - app.remaining) / app.total, used: (app.total - app.remaining) / app.total,
}; };
}, },
// Keep the bar moving while a download is writing to disk.
refetchInterval: 10 * 1000,
}); });
const onDeleteClicked = () => { const onDeleteClicked = async () => {
Alert.alert( try {
t("home.settings.storage.delete_all_downloaded_files_confirm"), await deleteAllFiles();
t("home.settings.storage.delete_all_downloaded_files_confirm_desc"), successHapticFeedback();
[ } catch (_e) {
{ errorHapticFeedback();
text: t("common.cancel"), toast.error(t("home.settings.toasts.error_deleting_files"));
style: "cancel", }
},
{
text: t("common.ok"),
style: "destructive",
onPress: async () => {
try {
await deleteAllFiles();
successHapticFeedback();
} catch (_e) {
errorHapticFeedback();
toast.error(t("home.settings.toasts.error_deleting_files"));
} finally {
// Reflect the freed space immediately instead of waiting for
// the next poll.
queryClient.invalidateQueries({ queryKey: ["appSize"] });
}
},
},
],
);
}; };
const calculatePercentage = (value: number, total: number) => { const calculatePercentage = (value: number, total: number) => {

View File

@@ -139,10 +139,9 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
if (orientation === "horizontal") { if (orientation === "horizontal") {
// Episode: prefer series thumb image for consistent look (like hero section) // Episode: prefer series thumb image for consistent look (like hero section)
if (item.Type === "Episode") { if (item.Type === "Episode") {
// First try parent/series thumb (horizontal series artwork). // First try parent/series thumb (horizontal series artwork)
// Matched pair: ParentThumbItemId owns the Thumb tag, not ParentBackdropItemId. if (item.ParentBackdropItemId && item.ParentThumbImageTag) {
if (item.ParentThumbItemId && item.ParentThumbImageTag) { return `${api.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=700&quality=80&tag=${item.ParentThumbImageTag}`;
return `${api.basePath}/Items/${item.ParentThumbItemId}/Images/Thumb?fillHeight=700&quality=80&tag=${item.ParentThumbImageTag}`;
} }
// Fall back to episode's own primary image // Fall back to episode's own primary image
if (item.ImageTags?.Primary) { if (item.ImageTags?.Primary) {

View File

@@ -51,6 +51,7 @@ import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import type { TVOptionItem } from "@/utils/atoms/tvOptionModal"; import type { TVOptionItem } from "@/utils/atoms/tvOptionModal";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils";
import { formatTimeString, msToTicks, ticksToMs } from "@/utils/time"; import { formatTimeString, msToTicks, ticksToMs } from "@/utils/time";
import { CONTROLS_CONSTANTS } from "./constants"; import { CONTROLS_CONSTANTS } from "./constants";
import { useVideoContext } from "./contexts/VideoContext"; import { useVideoContext } from "./contexts/VideoContext";
@@ -317,8 +318,10 @@ export const Controls: FC<Props> = ({
try { try {
const streams = (await onRefreshSubtitleTracks?.()) ?? []; const streams = (await onRefreshSubtitleTracks?.()) ?? [];
// Skip streams without a real index: `?? -1` would alias them to the // Skip streams without a real index: `?? -1` would alias them to the
// "disable subtitles" sentinel and mis-route selection. // "disable subtitles" sentinel and mis-route selection. Order like
return streams // jellyfin-web (embedded first, externals last, forced/default up).
return [...streams]
.sort(compareTracksForMenu)
.filter((stream) => typeof stream.Index === "number") .filter((stream) => typeof stream.Index === "number")
.map((stream) => { .map((stream) => {
const index = stream.Index as number; const index = stream.Index as number;
@@ -601,6 +604,9 @@ export const Controls: FC<Props> = ({
mediaSourceId: mediaSource?.Id, mediaSourceId: mediaSource?.Id,
subtitleTracks: tracksWithoutDisable, subtitleTracks: tracksWithoutDisable,
currentSubtitleIndex: subtitleIndex ?? -1, currentSubtitleIndex: subtitleIndex ?? -1,
// In-player selection can navigate (replacePlayer for burn-in switches);
// apply it after the modal route is dismissed so it isn't swallowed.
deferApplyUntilDismissed: true,
onDisableSubtitles: () => { onDisableSubtitles: () => {
// Find and call the "Disable" track's setTrack from VideoContext // Find and call the "Disable" track's setTrack from VideoContext
const disableTrack = videoContextSubtitleTracks?.find( const disableTrack = videoContextSubtitleTracks?.find(

View File

@@ -23,32 +23,29 @@
* - Used to report playback state to Jellyfin server * - Used to report playback state to Jellyfin server
* - Value of -1 means disabled/none * - Value of -1 means disabled/none
* *
* 2. MPV INDEX (track.mpvIndex) * 2. PLAYER TRACK (selected by IDENTITY, not position)
* - MPV's internal track ID * - Selection resolves the server Index against MPV's REAL track list via
* - MPV orders tracks as: [all embedded, then all external] * applyMpvSubtitleSelection: externals matched by external-filename,
* - IDs: 1..embeddedCount for embedded, embeddedCount+1.. for external * embedded by language/title. `track.mpvIndex` is no longer used to select
* - Value of -1 means track needs replacePlayer() (e.g., burned-in sub) * (kept -1) — positional mapping mis-selected when externals/embedded were
* reordered or the server hid embedded subs (#954 et al.).
* *
* ============================================================================ * ============================================================================
* SUBTITLE HANDLING * SUBTITLE HANDLING
* ============================================================================ * ============================================================================
* *
* Embedded (DeliveryMethod.Embed): * Embedded & External:
* - Already in MPV's track list * - Selected via applyMpvSubtitleSelection (identity match against the live
* - Select via setSubtitleTrack(mpvId) * track list). Menu order matches jellyfin-web (compareTracksForMenu:
* * embedded first, externals last, forced/default float up).
* External (DeliveryMethod.External):
* - Loaded into MPV on video start
* - Select via setSubtitleTrack(embeddedCount + externalPosition + 1)
* *
* Image-based during transcoding: * Image-based during transcoding:
* - Burned into video by Jellyfin, not in MPV * - Burned into video by Jellyfin, not in MPV → replacePlayer() to change.
* - Requires replacePlayer() to change
*/ */
import { SubtitleDeliveryMethod } from "@jellyfin/sdk/lib/generated-client";
import { File } from "expo-file-system"; import { File } from "expo-file-system";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { useAtomValue } from "jotai";
import type React from "react"; import type React from "react";
import { import {
createContext, createContext,
@@ -61,16 +58,18 @@ import {
import { Platform } from "react-native"; import { Platform } from "react-native";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import type { MpvAudioTrack } from "@/modules"; import type { MpvAudioTrack } from "@/modules";
import { apiAtom } from "@/providers/JellyfinProvider";
import { useOfflineMode } from "@/providers/OfflineModeProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles"; import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
import { isImageBasedSubtitle } from "@/utils/jellyfin/subtitleUtils"; import {
import type { Track } from "../types"; applyMpvSubtitleSelection,
compareTracksForMenu,
getExternalSubtitleUrl,
isImageBasedSubtitle,
} from "@/utils/jellyfin/subtitleUtils";
import { LOCAL_SUBTITLE_INDEX_START, type Track } from "../types";
import { usePlayerContext, usePlayerControls } from "./PlayerContext"; import { usePlayerContext, usePlayerControls } from "./PlayerContext";
// Starting index for local (client-downloaded) subtitles
// Uses negative indices to avoid collision with Jellyfin indices
const LOCAL_SUBTITLE_INDEX_START = -100;
interface VideoContextProps { interface VideoContextProps {
subtitleTracks: Track[] | null; subtitleTracks: Track[] | null;
audioTracks: Track[] | null; audioTracks: Track[] | null;
@@ -87,6 +86,7 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
const { tracksReady, mediaSource, downloadedItem } = usePlayerContext(); const { tracksReady, mediaSource, downloadedItem } = usePlayerContext();
const playerControls = usePlayerControls(); const playerControls = usePlayerControls();
const offline = useOfflineMode(); const offline = useOfflineMode();
const api = useAtomValue(apiAtom);
const router = useRouter(); const router = useRouter();
const { itemId, audioIndex, bitrateValue, subtitleIndex, playbackPosition } = const { itemId, audioIndex, bitrateValue, subtitleIndex, playbackPosition } =
@@ -126,10 +126,17 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
audioIndex?: string; audioIndex?: string;
subtitleIndex?: string; subtitleIndex?: string;
}) => { }) => {
// The URL param can hold a local-sub sentinel (selected via setParams) that
// only exists in the dying player — the server must get "none" (-1) instead.
// NaN (missing/blank param) fails the comparison and passes through as-is.
const fallbackSubtitleIndex =
Number.parseInt(subtitleIndex, 10) <= LOCAL_SUBTITLE_INDEX_START
? "-1"
: subtitleIndex;
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
itemId: itemId ?? "", itemId: itemId ?? "",
audioIndex: params.audioIndex ?? audioIndex, audioIndex: params.audioIndex ?? audioIndex,
subtitleIndex: params.subtitleIndex ?? subtitleIndex, subtitleIndex: params.subtitleIndex ?? fallbackSubtitleIndex,
mediaSourceId: mediaSource?.Id ?? "", mediaSourceId: mediaSource?.Id ?? "",
bitrateValue: bitrateValue, bitrateValue: bitrateValue,
playbackPosition: playbackPosition, playbackPosition: playbackPosition,
@@ -141,6 +148,19 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
useEffect(() => { useEffect(() => {
if (!tracksReady) return; if (!tracksReady) return;
// Guard every state commit against stale runs: api?.basePath /
// isCurrentSubImageBased can flip mid-run and restart this effect, and an
// earlier async run (which captured an old `api`) must not finish later and
// overwrite the fresh track list with callbacks bound to stale closures.
// The cleanup flips `cancelled`, so any late commit from a dead run is dropped.
let cancelled = false;
const commitSubtitleTracks = (next: Track[]) => {
if (!cancelled) setSubtitleTracks(next);
};
const commitAudioTracks = (next: Track[]) => {
if (!cancelled) setAudioTracks(next);
};
const fetchTracks = async () => { const fetchTracks = async () => {
// Check if this is offline transcoded content // Check if this is offline transcoded content
// For transcoded offline content, only ONE audio track exists in the file // For transcoded offline content, only ONE audio track exists in the file
@@ -166,10 +186,10 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
}, },
}, },
]; ];
setAudioTracks(audio); commitAudioTracks(audio);
} else { } else {
// Fallback: show no audio tracks if the stored track wasn't found // Fallback: show no audio tracks if the stored track wasn't found
setAudioTracks([]); commitAudioTracks([]);
} }
// For subtitles in transcoded offline content: // For subtitles in transcoded offline content:
@@ -179,6 +199,24 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
downloadedItem.userData.subtitleStreamIndex; downloadedItem.userData.subtitleStreamIndex;
const subs: Track[] = []; const subs: Track[] = [];
// If an IMAGE subtitle was burned into the transcoded download it's in the
// video pixels — it can't be turned off or swapped. Show only that entry
// instead of advertising "Disable"/text controls that can't affect it.
const burnedInSub = allSubs.find(
(s) => s.Index === downloadedSubtitleIndex,
);
if (burnedInSub && isImageBasedSubtitle(burnedInSub)) {
commitSubtitleTracks([
{
name: `${burnedInSub.DisplayTitle || "Unknown"} (burned in)`,
index: burnedInSub.Index ?? -1,
mpvIndex: -1,
setTrack: () => {},
},
]);
return;
}
// Add "Disable" option // Add "Disable" option
subs.push({ subs.push({
name: "Disable", name: "Disable",
@@ -190,123 +228,84 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
}, },
}); });
// For text-based subs, they should still be available in the file // Text subs are muxed into the transcoded file and switchable; resolve by
let subIdx = 1; // identity against MPV's real track list (same as online). Order matches web.
for (const sub of allSubs) { // Image subs aren't in the transcoded file (only the burned one was, handled
if (sub.IsTextSubtitleStream) { // above), so skip them here.
for (const sub of [...allSubs].sort(compareTracksForMenu)) {
if (!isImageBasedSubtitle(sub)) {
subs.push({ subs.push({
name: sub.DisplayTitle || "Unknown", name: sub.DisplayTitle || "Unknown",
index: sub.Index ?? -1, index: sub.Index ?? -1,
mpvIndex: subIdx, mpvIndex: -1,
setTrack: () => { setTrack: () => {
playerControls.setSubtitleTrack(subIdx);
router.setParams({ subtitleIndex: String(sub.Index) });
},
});
subIdx++;
} else if (sub.Index === downloadedSubtitleIndex) {
// This image-based sub was burned in - show it but indicate it's active
subs.push({
name: `${sub.DisplayTitle || "Unknown"} (burned in)`,
index: sub.Index ?? -1,
mpvIndex: -1, // Can't be changed
setTrack: () => {
// Already burned in, just update params
router.setParams({ subtitleIndex: String(sub.Index) }); router.setParams({ subtitleIndex: String(sub.Index) });
void applyMpvSubtitleSelection(playerControls, {
subtitleStreams: allSubs,
jellyfinSubtitleIndex: sub.Index ?? -1,
getExpectedExternalUrl: (s) => {
if (!s.DeliveryUrl) return undefined;
if (offline) return s.DeliveryUrl;
return api?.basePath
? `${api.basePath}${s.DeliveryUrl}`
: undefined;
},
});
}, },
}); });
} }
} }
setSubtitleTracks(subs.sort((a, b) => a.index - b.index)); commitSubtitleTracks(subs);
return; return;
} }
// MPV track handling // MPV track handling
const audioData = await playerControls.getAudioTracks().catch(() => null); const audioData = await playerControls.getAudioTracks().catch(() => null);
if (cancelled) return;
const playerAudio = (audioData as MpvAudioTrack[]) ?? []; const playerAudio = (audioData as MpvAudioTrack[]) ?? [];
// Separate embedded vs external subtitles from Jellyfin's list
// MPV orders tracks as: [all embedded, then all external]
const embeddedSubs = allSubs.filter(
(s) => s.DeliveryMethod === SubtitleDeliveryMethod.Embed,
);
const externalSubs = allSubs.filter(
(s) => s.DeliveryMethod === SubtitleDeliveryMethod.External,
);
// Count embedded subs that will be in MPV
// (excludes image-based subs during transcoding as they're burned in)
const embeddedInPlayer = embeddedSubs.filter(
(s) => !isTranscoding || !isImageBasedSubtitle(s),
);
const subs: Track[] = []; const subs: Track[] = [];
// Process all Jellyfin subtitles // Process all Jellyfin subtitles. Selection resolves against MPV's real
for (const sub of allSubs) { // track list by identity (applyMpvSubtitleSelection) — never positional
const isEmbedded = sub.DeliveryMethod === SubtitleDeliveryMethod.Embed; // index math, which mis-selects across external/embedded reordering and
const isExternal = // server-hidden embedded subs (#954/#1690/#618/#1467/#976/#1451).
sub.DeliveryMethod === SubtitleDeliveryMethod.External; // Order matches jellyfin-web (embedded first, externals last, forced/default up).
for (const sub of [...allSubs].sort(compareTracksForMenu)) {
// For image-based subs during transcoding, need to refresh player // Image-based subs during transcoding are burned into the video by the
if (isTranscoding && isImageBasedSubtitle(sub)) { // server; both switching TO one and switching AWAY from a currently
subs.push({ // active one require a player refresh (re-transcode), not a track change.
name: sub.DisplayTitle || "Unknown", const needsReplace =
index: sub.Index ?? -1, isTranscoding &&
mpvIndex: -1, (isImageBasedSubtitle(sub) || isCurrentSubImageBased);
setTrack: () => {
replacePlayer({ subtitleIndex: String(sub.Index) });
},
});
continue;
}
// Calculate MPV track ID based on type
// MPV IDs: [1..embeddedCount] for embedded, [embeddedCount+1..] for external
let mpvId = -1;
if (isEmbedded) {
// Find position among embedded subs that are in player
const embeddedPosition = embeddedInPlayer.findIndex(
(s) => s.Index === sub.Index,
);
if (embeddedPosition !== -1) {
mpvId = embeddedPosition + 1; // 1-based ID
}
} else if (isExternal) {
// Find position among external subs, offset by embedded count
const externalPosition = externalSubs.findIndex(
(s) => s.Index === sub.Index,
);
if (externalPosition !== -1) {
mpvId = embeddedInPlayer.length + externalPosition + 1;
}
}
subs.push({ subs.push({
name: sub.DisplayTitle || "Unknown", name: sub.DisplayTitle || "Unknown",
index: sub.Index ?? -1, index: sub.Index ?? -1,
mpvIndex: mpvId, mpvIndex: -1,
setTrack: () => { setTrack: () => {
// Transcoding + switching to/from image-based sub if (needsReplace) {
if (
isTranscoding &&
(isImageBasedSubtitle(sub) || isCurrentSubImageBased)
) {
replacePlayer({ subtitleIndex: String(sub.Index) }); replacePlayer({ subtitleIndex: String(sub.Index) });
return; return;
} }
router.setParams({ subtitleIndex: String(sub.Index) });
// Direct switch in player void applyMpvSubtitleSelection(playerControls, {
if (mpvId !== -1) { subtitleStreams: allSubs,
playerControls.setSubtitleTrack(mpvId); jellyfinSubtitleIndex: sub.Index ?? -1,
router.setParams({ subtitleIndex: String(sub.Index) }); // Mirror how external subs are loaded into MPV (online: basePath +
return; // DeliveryUrl, offline: local DeliveryUrl) so identity matching by
} // external-filename lines up.
getExpectedExternalUrl: (s) =>
// Fallback - refresh player getExternalSubtitleUrl(s, { offline, basePath: api?.basePath }),
replacePlayer({ subtitleIndex: String(sub.Index) }); }).then((result) => {
// Safety net: a menu-listed sub the player can't select (server-
// burned Encode, sidecar never sub-added) only shows up after the
// server re-processes the stream with it.
if (result.kind === "notFound" || result.kind === "burnedIn") {
replacePlayer({ subtitleIndex: String(sub.Index) });
}
});
}, },
}); });
} }
@@ -374,12 +373,29 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
} }
} }
setSubtitleTracks(subs.sort((a, b) => a.index - b.index)); // Already in jellyfin-web order (sorted iteration above); "Disable" stays
setAudioTracks(audio); // at the front (unshifted), local downloaded subs at the end.
commitSubtitleTracks(subs);
commitAudioTracks(audio);
}; };
fetchTracks(); fetchTracks();
}, [tracksReady, mediaSource, offline, downloadedItem, itemId]); return () => {
cancelled = true;
};
// api?.basePath: setTrack builds external-sub URLs from it — rebuild once the
// API is ready so online externals don't resolve with undefined.
// isCurrentSubImageBased: setTrack closes over it for the transcode replacePlayer
// decision — rebuild when it flips so we refresh the stream when we should.
}, [
tracksReady,
mediaSource,
offline,
downloadedItem,
itemId,
api?.basePath,
isCurrentSubImageBased,
]);
return ( return (
<VideoContext.Provider value={{ subtitleTracks, audioTracks }}> <VideoContext.Provider value={{ subtitleTracks, audioTracks }}>

View File

@@ -28,4 +28,20 @@ type Track = {
localPath?: string; localPath?: string;
}; };
/**
* Synthetic `Track.index` base for client-side downloaded subtitles (loaded via
* `addSubtitleFile`, no matching Jellyfin MediaStream). Local sub k gets
* `LOCAL_SUBTITLE_INDEX_START - k`, so `index <= LOCAL_SUBTITLE_INDEX_START`
* identifies a local track.
*/
export const LOCAL_SUBTITLE_INDEX_START = -100;
/**
* Map a client-side subtitle index to what the Jellyfin stream API understands.
* Local sentinel indexes have no matching MediaStream on the server, which
* expects -1 ("no subtitles") when re-negotiating a stream.
*/
export const toServerSubtitleIndex = (index: number): number =>
index <= LOCAL_SUBTITLE_INDEX_START ? -1 : index;
export type { EmbeddedSubtitle, ExternalSubtitle, Track, TranscodedSubtitle }; export type { EmbeddedSubtitle, ExternalSubtitle, Track, TranscodedSubtitle };

View File

@@ -14,6 +14,7 @@ interface ShowSubtitleModalParams {
onServerSubtitleDownloaded?: () => void; onServerSubtitleDownloaded?: () => void;
onLocalSubtitleDownloaded?: (path: string) => void; onLocalSubtitleDownloaded?: (path: string) => void;
refreshSubtitleTracks?: () => Promise<Track[]>; refreshSubtitleTracks?: () => Promise<Track[]>;
deferApplyUntilDismissed?: boolean;
} }
export const useTVSubtitleModal = () => { export const useTVSubtitleModal = () => {
@@ -30,6 +31,7 @@ export const useTVSubtitleModal = () => {
onServerSubtitleDownloaded: params.onServerSubtitleDownloaded, onServerSubtitleDownloaded: params.onServerSubtitleDownloaded,
onLocalSubtitleDownloaded: params.onLocalSubtitleDownloaded, onLocalSubtitleDownloaded: params.onLocalSubtitleDownloaded,
refreshSubtitleTracks: params.refreshSubtitleTracks, refreshSubtitleTracks: params.refreshSubtitleTracks,
deferApplyUntilDismissed: params.deferApplyUntilDismissed,
}); });
router.push("/(auth)/tv-subtitle-modal"); router.push("/(auth)/tv-subtitle-modal");
}, },

View File

@@ -1,5 +1,125 @@
export { import { useCallback, useEffect, useState } from "react";
type PermissionStatus, import { Platform } from "react-native";
type UseWifiSSIDReturn, import { getSSID } from "@/modules/wifi-ssid";
useWifiSsid as useWifiSSID,
} from "@/providers/WifiSsidProvider"; export type PermissionStatus =
| "granted"
| "denied"
| "undetermined"
| "unavailable";
export interface UseWifiSSIDReturn {
ssid: string | null;
permissionStatus: PermissionStatus;
requestPermission: () => Promise<boolean>;
isLoading: boolean;
}
// WiFi SSID is not available on tvOS
if (Platform.isTV) {
// Export a stub hook for tvOS
module.exports = {
useWifiSSID: (): UseWifiSSIDReturn => ({
ssid: null,
permissionStatus: "unavailable" as PermissionStatus,
requestPermission: async () => false,
isLoading: false,
}),
};
}
// Only import Location on non-TV platforms
const Location = Platform.isTV ? null : require("expo-location");
function mapLocationStatus(status: number | undefined): PermissionStatus {
if (!Location) return "unavailable";
switch (status) {
case Location.PermissionStatus?.GRANTED:
return "granted";
case Location.PermissionStatus?.DENIED:
return "denied";
default:
return "undetermined";
}
}
export function useWifiSSID(): UseWifiSSIDReturn {
const [ssid, setSSID] = useState<string | null>(null);
const [permissionStatus, setPermissionStatus] = useState<PermissionStatus>(
Platform.isTV ? "unavailable" : "undetermined",
);
const [isLoading, setIsLoading] = useState(!Platform.isTV);
const fetchSSID = useCallback(async () => {
if (Platform.isTV) return;
const result = await getSSID();
setSSID(result);
}, []);
const requestPermission = useCallback(async (): Promise<boolean> => {
if (Platform.isTV || !Location) {
setPermissionStatus("unavailable");
return false;
}
try {
const { status } = await Location.requestForegroundPermissionsAsync();
const newStatus = mapLocationStatus(status);
setPermissionStatus(newStatus);
if (newStatus === "granted") {
await fetchSSID();
}
return newStatus === "granted";
} catch {
setPermissionStatus("unavailable");
return false;
}
}, [fetchSSID]);
useEffect(() => {
if (Platform.isTV || !Location) {
setIsLoading(false);
return;
}
async function initialize() {
setIsLoading(true);
try {
const { status } = await Location.getForegroundPermissionsAsync();
const mappedStatus = mapLocationStatus(status);
setPermissionStatus(mappedStatus);
if (mappedStatus === "granted") {
await fetchSSID();
}
} catch {
setPermissionStatus("unavailable");
}
setIsLoading(false);
}
initialize();
}, [fetchSSID]);
// Refresh SSID when permission status changes to granted
useEffect(() => {
if (Platform.isTV) return;
if (permissionStatus === "granted") {
fetchSSID();
// Also set up an interval to periodically check SSID
const interval = setInterval(fetchSSID, 10000); // Check every 10 seconds
return () => clearInterval(interval);
}
}, [permissionStatus, fetchSSID]);
return {
ssid,
permissionStatus,
requestPermission,
isLoading,
};
}

View File

@@ -532,16 +532,29 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
val trackId = mpv?.getPropertyInt("track-list/$i/id") ?: continue val trackId = mpv?.getPropertyInt("track-list/$i/id") ?: continue
val track = mutableMapOf<String, Any>("id" to trackId) val track = mutableMapOf<String, Any>("id" to trackId)
mpv?.getPropertyString("track-list/$i/title")?.let { track["title"] = it } mpv?.getPropertyString("track-list/$i/title")?.let { track["title"] = it }
mpv?.getPropertyString("track-list/$i/lang")?.let { track["lang"] = it } mpv?.getPropertyString("track-list/$i/lang")?.let { track["lang"] = it }
mpv?.getPropertyString("track-list/$i/codec")?.let { track["codec"] = it }
// Identity fields used to map a Jellyfin subtitle to the real track
// (instead of fragile positional counting). `external` + `external-filename`
// uniquely identify a sub-added sidecar. `ff-index` is exposed for
// diagnostics / potential future exact-index matching; the current
// resolver matches embedded tracks by language/title, not ff-index.
val external = mpv?.getPropertyBoolean("track-list/$i/external") ?: false
track["external"] = external
mpv?.getPropertyString("track-list/$i/external-filename")?.let {
track["externalFilename"] = it
}
mpv?.getPropertyInt("track-list/$i/ff-index")?.let { track["ffIndex"] = it }
val selected = mpv?.getPropertyBoolean("track-list/$i/selected") ?: false val selected = mpv?.getPropertyBoolean("track-list/$i/selected") ?: false
track["selected"] = selected track["selected"] = selected
tracks.add(track) tracks.add(track)
} }
return tracks return tracks
} }
@@ -840,6 +853,13 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
initialAudioId?.let { if (it > 0) setAudioTrack(it) } initialAudioId?.let { if (it > 0) setAudioTrack(it) }
initialSubtitleId?.let { setSubtitleTrack(it) } ?: disableSubtitles() initialSubtitleId?.let { setSubtitleTrack(it) } ?: disableSubtitles()
// The disable above can race a JS-side identity selection that
// landed before FILE_LOADED (JS no longer passes an initial sid).
// Re-emit tracksReady so the idempotent JS re-apply always runs
// after it — for embedded-only files this is the only
// post-FILE_LOADED fire.
mainHandler.post { delegate?.onTracksReady() }
if (!isReadyToSeek) { if (!isReadyToSeek) {
isReadyToSeek = true isReadyToSeek = true
mainHandler.post { delegate?.onReadyToSeek() } mainHandler.post { delegate?.onReadyToSeek() }

View File

@@ -508,6 +508,15 @@ final class MPVLayerRenderer {
} else { } else {
disableSubtitles() disableSubtitles()
} }
// The disable above can race a JS-side identity selection that
// landed before FILE_LOADED (JS no longer passes an initial sid).
// Re-emit tracksReady so the idempotent JS re-apply always runs
// after it for embedded-only files this is the only
// post-FILE_LOADED fire.
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.delegate?.renderer(self, didBecomeTracksReady: true)
}
if !isReadyToSeek { if !isReadyToSeek {
isReadyToSeek = true isReadyToSeek = true
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
@@ -759,8 +768,8 @@ final class MPVLayerRenderer {
trackType == "sub" else { continue } trackType == "sub" else { continue }
var trackId: Int64 = 0 var trackId: Int64 = 0
getProperty(handle: handle, name: "track-list/\(i)/id", format: MPV_FORMAT_INT64, value: &trackId) guard getProperty(handle: handle, name: "track-list/\(i)/id", format: MPV_FORMAT_INT64, value: &trackId) >= 0 else { continue }
var track: [String: Any] = ["id": Int(trackId)] var track: [String: Any] = ["id": Int(trackId)]
if let title = getStringProperty(handle: handle, name: "track-list/\(i)/title") { if let title = getStringProperty(handle: handle, name: "track-list/\(i)/title") {
@@ -770,12 +779,34 @@ final class MPVLayerRenderer {
if let lang = getStringProperty(handle: handle, name: "track-list/\(i)/lang") { if let lang = getStringProperty(handle: handle, name: "track-list/\(i)/lang") {
track["lang"] = lang track["lang"] = lang
} }
if let codec = getStringProperty(handle: handle, name: "track-list/\(i)/codec") {
track["codec"] = codec
}
// Identity fields used to map a Jellyfin subtitle to the real track
// (instead of fragile positional counting). `external` + `external-filename`
// uniquely identify a sub-added sidecar. `ff-index` is exposed for
// diagnostics / potential future exact-index matching; the current
// resolver matches embedded tracks by language/title, not ff-index.
var external: Int32 = 0
getProperty(handle: handle, name: "track-list/\(i)/external", format: MPV_FORMAT_FLAG, value: &external)
track["external"] = external != 0
if let extFilename = getStringProperty(handle: handle, name: "track-list/\(i)/external-filename") {
track["externalFilename"] = extFilename
}
var ffIndex: Int64 = 0
if getProperty(handle: handle, name: "track-list/\(i)/ff-index", format: MPV_FORMAT_INT64, value: &ffIndex) >= 0 {
track["ffIndex"] = Int(ffIndex)
}
var selected: Int32 = 0 var selected: Int32 = 0
getProperty(handle: handle, name: "track-list/\(i)/selected", format: MPV_FORMAT_FLAG, value: &selected) getProperty(handle: handle, name: "track-list/\(i)/selected", format: MPV_FORMAT_FLAG, value: &selected)
track["selected"] = selected != 0 track["selected"] = selected != 0
Logger.shared.log("getSubtitleTracks: found sub track id=\(trackId), title=\(track["title"] ?? "none"), lang=\(track["lang"] ?? "none")", type: "Info") Logger.shared.log("getSubtitleTracks: found sub track id=\(trackId), title=\(track["title"] ?? "none"), lang=\(track["lang"] ?? "none"), external=\(external != 0)", type: "Info")
tracks.append(track) tracks.append(track)
} }
@@ -872,8 +903,8 @@ final class MPVLayerRenderer {
trackType == "audio" else { continue } trackType == "audio" else { continue }
var trackId: Int64 = 0 var trackId: Int64 = 0
getProperty(handle: handle, name: "track-list/\(i)/id", format: MPV_FORMAT_INT64, value: &trackId) guard getProperty(handle: handle, name: "track-list/\(i)/id", format: MPV_FORMAT_INT64, value: &trackId) >= 0 else { continue }
var track: [String: Any] = ["id": Int(trackId)] var track: [String: Any] = ["id": Int(trackId)]
if let title = getStringProperty(handle: handle, name: "track-list/\(i)/title") { if let title = getStringProperty(handle: handle, name: "track-list/\(i)/title") {

View File

@@ -141,6 +141,14 @@ export type SubtitleTrack = {
id: number; id: number;
title?: string; title?: string;
lang?: string; lang?: string;
/** Subtitle codec (mpv `codec`), e.g. "subrip", "ass", "hdmv_pgs_subtitle". */
codec?: string;
/** True if loaded from a separate file via `sub-add` (mpv `external`). */
external?: boolean;
/** For external tracks: the exact URL/path it was loaded from (mpv `external-filename`). */
externalFilename?: string;
/** FFmpeg stream index (mpv `ff-index`); not guaranteed for non-lavf demuxers. */
ffIndex?: number;
selected?: boolean; selected?: boolean;
}; };

View File

@@ -1,19 +0,0 @@
apply plugin: 'expo-module-gradle-plugin'
group = 'expo.modules.wifissid'
version = '1.0.0'
expoModule {
canBePublished false
}
android {
namespace "expo.modules.wifissid"
defaultConfig {
versionCode 1
versionName "1.0.0"
}
}
dependencies {
}

View File

@@ -1,9 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Reading the connected Wi-Fi SSID requires ACCESS_WIFI_STATE, and (since
Android 8) ACCESS_FINE_LOCATION granted at runtime. Location is requested
at runtime by the app via expo-location; the manifest declares the
Wi-Fi/network permissions this module needs directly. -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>

View File

@@ -1,121 +0,0 @@
package expo.modules.wifissid
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import android.provider.Settings
import android.util.Log
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class WifiSsidModule : Module() {
companion object {
private const val TAG = "WifiSsid"
private const val UNKNOWN_SSID = "<unknown ssid>"
}
private val context
get() = requireNotNull(appContext.reactContext)
override fun definition() = ModuleDefinition {
Name("WifiSsid")
AsyncFunction("getSSID") {
val (ssid, connectedToWifi) = readWifi()
mapOf(
"ssid" to ssid,
"connectedToWifi" to connectedToWifi,
)
}
Function("getSSIDSync") {
val (ssid, connectedToWifi) = readWifi()
mapOf(
"ssid" to ssid,
"connectedToWifi" to connectedToWifi,
)
}
Function("openLocationSettings") {
openLocationSettings()
}
}
/**
* Returns the current Wi-Fi SSID (or null when it can't be read) and whether
* the device is connected to a Wi-Fi network. When the device is connected but
* the SSID is null, the OS is withholding the network name — on Android this
* typically means device Location services are off (the SSID is treated as
* location-sensitive).
*/
private fun readWifi(): Pair<String?, Boolean> {
val androidContext = context.applicationContext
var connectedToWifi = false
var ssid: String? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
val cm = androidContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
val caps = cm?.let { manager ->
manager.activeNetwork?.let { net -> manager.getNetworkCapabilities(net) }
}
connectedToWifi = caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
if (hasFineLocationPermission()) {
ssid = (caps?.transportInfo as? WifiInfo)?.ssid?.sanitize()
}
} catch (e: Exception) {
Log.e(TAG, "Error reading Wi-Fi via ConnectivityManager", e)
}
} else {
// Pre-Q (API 2628): NetworkCapabilities/transportInfo aren't available, so
// detect a Wi-Fi connection via the legacy active-network API. Without this,
// connectedToWifi stays false on Android 8/9 even when connected, so the
// "connected but SSID hidden" state never surfaces there.
try {
val cm = androidContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
@Suppress("DEPRECATION")
connectedToWifi = cm?.activeNetworkInfo?.type == ConnectivityManager.TYPE_WIFI
} catch (e: Exception) {
Log.e(TAG, "Error reading Wi-Fi via ConnectivityManager (legacy)", e)
}
}
if (ssid == null && hasFineLocationPermission()) {
try {
val wm = androidContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
@Suppress("DEPRECATION")
ssid = wm?.connectionInfo?.ssid?.sanitize()
} catch (e: Exception) {
Log.e(TAG, "Error reading SSID via WifiManager", e)
}
}
return Pair(ssid, connectedToWifi)
}
private fun openLocationSettings() {
try {
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.applicationContext.startActivity(intent)
} catch (e: Exception) {
Log.e(TAG, "Unable to open Location settings", e)
}
}
private fun hasFineLocationPermission(): Boolean =
context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
private fun String.sanitize(): String? {
val cleaned = trim('"')
return if (cleaned.isBlank() || cleaned == UNKNOWN_SSID) null else cleaned
}
}

View File

@@ -1,11 +1,8 @@
{ {
"name": "wifi-ssid", "name": "wifi-ssid",
"version": "1.0.0", "version": "1.0.0",
"platforms": ["ios", "android"], "platforms": ["ios"],
"ios": { "ios": {
"modules": ["WifiSsidModule"] "modules": ["WifiSsidModule"]
},
"android": {
"modules": ["expo.modules.wifissid.WifiSsidModule"]
} }
} }

View File

@@ -1,92 +1,44 @@
import { Platform, requireNativeModule } from "expo-modules-core"; import { Platform, requireNativeModule } from "expo-modules-core";
import { Linking } from "react-native";
export interface WifiSsidResult { // Only load the native module on iOS
ssid: string | null; const WifiSsidModule =
/** Platform.OS === "ios" ? requireNativeModule("WifiSsid") : null;
* Whether the device is connected to a Wi-Fi network. When true but `ssid` is
* null, the OS is withholding the network name — on Android this happens when
* device Location services are off (the SSID is location-sensitive).
*/
connectedToWifi: boolean;
}
// Load the native module on iOS and Android (not TV/web). Wrapped so a
// missing/unlinked module degrades to null instead of crashing the bundle.
const WifiSsidModule = (() => {
if (Platform.OS !== "ios" && Platform.OS !== "android") return null;
try {
return requireNativeModule("WifiSsid");
} catch {
return null;
}
})();
/** /**
* Get the current WiFi SSID and whether the device is on a Wi-Fi network. * Get the current WiFi SSID on iOS.
* Returns null on Android or if not connected to WiFi.
* *
* iOS requirements: * Requires:
* - Location permission granted * - Location permission granted
* - com.apple.developer.networking.wifi-info entitlement * - com.apple.developer.networking.wifi-info entitlement
* * - Access WiFi Information capability enabled in Apple Developer Portal
* Android requirements:
* - ACCESS_FINE_LOCATION granted at runtime, and device Location services on
*/ */
export async function getSSID(): Promise<WifiSsidResult> { export async function getSSID(): Promise<string | null> {
if (!WifiSsidModule) { if (!WifiSsidModule) {
return { ssid: null, connectedToWifi: false }; return null;
} }
try { try {
return normalize(await WifiSsidModule.getSSID()); const ssid = await WifiSsidModule.getSSID();
return ssid ?? null;
} catch (error) { } catch (error) {
console.error("[WifiSsid] Error getting SSID:", error); console.error("[WifiSsid] Error getting SSID:", error);
return { ssid: null, connectedToWifi: false }; return null;
}
}
/** Synchronous version (uses the older CNCopyCurrentNetworkInfo API on iOS). */
export function getSSIDSync(): WifiSsidResult {
if (!WifiSsidModule) {
return { ssid: null, connectedToWifi: false };
}
try {
return normalize(WifiSsidModule.getSSIDSync());
} catch (error) {
console.error("[WifiSsid] Error getting SSID (sync):", error);
return { ssid: null, connectedToWifi: false };
} }
} }
/** /**
* Open the system Location settings so the user can enable Location services * Synchronous version - uses older CNCopyCurrentNetworkInfo API
* (required on Android for the OS to reveal the Wi-Fi SSID). Falls back to the
* app's settings page where there is no dedicated native action (e.g. iOS).
*/ */
export function openLocationSettings(): void { export function getSSIDSync(): string | null {
try { if (!WifiSsidModule) {
if (WifiSsidModule?.openLocationSettings) { return null;
WifiSsidModule.openLocationSettings();
return;
}
} catch (error) {
console.error("[WifiSsid] Error opening location settings:", error);
} }
Linking.openSettings();
}
// Both native modules return { ssid, connectedToWifi }. The string branch is a try {
// defensive fallback in case a build returns the older plain-string shape. return WifiSsidModule.getSSIDSync() ?? null;
function normalize(result: unknown): WifiSsidResult { } catch (error) {
if (typeof result === "string") { console.error("[WifiSsid] Error getting SSID (sync):", error);
const ssid = result || null; return null;
return { ssid, connectedToWifi: ssid !== null };
} }
if (result && typeof result === "object") {
const obj = result as { ssid?: string | null; connectedToWifi?: boolean };
return {
ssid: obj.ssid ?? null,
connectedToWifi: Boolean(obj.connectedToWifi),
};
}
return { ssid: null, connectedToWifi: false };
} }

View File

@@ -1,113 +1,50 @@
import ExpoModulesCore import ExpoModulesCore
#if !os(tvOS) #if !os(tvOS)
import Network
import NetworkExtension import NetworkExtension
import SystemConfiguration.CaptiveNetwork import SystemConfiguration.CaptiveNetwork
#endif #endif
import UIKit
public class WifiSsidModule: Module { public class WifiSsidModule: Module {
#if !os(tvOS)
// NWPathMonitor reports whether the active path uses Wi-Fi WITHOUT requiring
// the wifi-info entitlement, so we can tell "on Wi-Fi but can't read the SSID"
// apart from "not connected to Wi-Fi" even when fetchCurrent returns nil.
private let pathMonitor = NWPathMonitor()
private let monitorQueue = DispatchQueue(label: "expo.modules.wifissid.pathmonitor")
private let lock = NSLock()
private var cachedOnWifi = false
private var pathMonitorStarted = false
private var isOnWifi: Bool {
lock.lock()
defer { lock.unlock() }
return cachedOnWifi
}
private func startPathMonitorIfNeeded() {
lock.lock()
if pathMonitorStarted {
lock.unlock()
return
}
pathMonitorStarted = true
lock.unlock()
pathMonitor.pathUpdateHandler = { [weak self] path in
guard let self else { return }
let onWifi = path.status == .satisfied && path.usesInterfaceType(.wifi)
self.lock.lock()
self.cachedOnWifi = onWifi
self.lock.unlock()
}
pathMonitor.start(queue: monitorQueue)
}
deinit {
pathMonitor.cancel()
}
#endif
public func definition() -> ModuleDefinition { public func definition() -> ModuleDefinition {
Name("WifiSsid") Name("WifiSsid")
AsyncFunction("getSSID") { () -> [String: Any] in // Get current WiFi SSID using NEHotspotNetwork (iOS 14+)
// Not available on tvOS
AsyncFunction("getSSID") { () -> String? in
#if os(tvOS) #if os(tvOS)
return ["ssid": NSNull(), "connectedToWifi": false] return nil
#else #else
self.startPathMonitorIfNeeded() return await withCheckedContinuation { continuation in
let ssid = await self.resolveSSID() NEHotspotNetwork.fetchCurrent { network in
var result: [String: Any] = ["connectedToWifi": self.isOnWifi] if let ssid = network?.ssid {
if let ssid = ssid { print("[WifiSsid] Got SSID via NEHotspotNetwork: \(ssid)")
result["ssid"] = ssid continuation.resume(returning: ssid)
} else { } else {
result["ssid"] = NSNull() // Fallback to CNCopyCurrentNetworkInfo for older iOS
} print("[WifiSsid] NEHotspotNetwork returned nil, trying CNCopyCurrentNetworkInfo")
return result let ssid = self.getSSIDViaCNCopy()
#endif continuation.resume(returning: ssid)
} }
Function("getSSIDSync") { () -> [String: Any] in
#if os(tvOS)
return ["ssid": NSNull(), "connectedToWifi": false]
#else
self.startPathMonitorIfNeeded()
let ssid = self.getSSIDViaCNCopy()
var result: [String: Any] = ["connectedToWifi": self.isOnWifi]
if let ssid = ssid {
result["ssid"] = ssid
} else {
result["ssid"] = NSNull()
}
return result
#endif
}
Function("openLocationSettings") {
#if !os(tvOS)
DispatchQueue.main.async {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
} }
} }
#endif #endif
} }
// Synchronous version using only CNCopyCurrentNetworkInfo
// Not available on tvOS
Function("getSSIDSync") { () -> String? in
#if os(tvOS)
return nil
#else
return self.getSSIDViaCNCopy()
#endif
}
} }
#if !os(tvOS) #if !os(tvOS)
private func resolveSSID() async -> String? {
await withCheckedContinuation { continuation in
NEHotspotNetwork.fetchCurrent { network in
if let ssid = network?.ssid {
continuation.resume(returning: ssid)
} else {
continuation.resume(returning: self.getSSIDViaCNCopy())
}
}
}
}
private func getSSIDViaCNCopy() -> String? { private func getSSIDViaCNCopy() -> String? {
guard let interfaces = CNCopySupportedInterfaces() as? [String] else { guard let interfaces = CNCopySupportedInterfaces() as? [String] else {
print("[WifiSsid] CNCopySupportedInterfaces returned nil")
return nil return nil
} }
@@ -117,10 +54,12 @@ public class WifiSsidModule: Module {
} }
if let ssid = networkInfo[kCNNetworkInfoKeySSID as String] as? String { if let ssid = networkInfo[kCNNetworkInfoKeySSID as String] as? String {
print("[WifiSsid] Got SSID via CNCopyCurrentNetworkInfo: \(ssid)")
return ssid return ssid
} }
} }
print("[WifiSsid] No SSID found via CNCopyCurrentNetworkInfo")
return nil return nil
} }
#endif #endif

View File

@@ -96,24 +96,5 @@ export function getDownloadedItemSize(id: string): number {
*/ */
export function calculateTotalDownloadedSize(): number { export function calculateTotalDownloadedSize(): number {
const items = getAllDownloadedItems(); const items = getAllDownloadedItems();
return items.reduce((sum, item) => { return items.reduce((sum, item) => sum + (item.videoFileSize || 0), 0);
// Trickplay bytes count too — getDownloadedItemSize models per-item size
// as video + trickplay, the total must match.
const trickplaySize = item.trickPlayData?.size ?? 0;
// Read the live file size on disk so the total reflects actual usage and
// self-heals items whose stored videoFileSize is 0 (old schema, or
// `fileInfo.size` was undefined at download time). Fall back to the stored
// value if the file can't be stat'd.
if (item.videoFilePath) {
try {
const file = new File(filePathToUri(item.videoFilePath));
if (file.exists) {
return sum + (file.size ?? item.videoFileSize ?? 0) + trickplaySize;
}
} catch (error) {
console.warn("Failed to stat downloaded file for size:", error);
}
}
return sum + (item.videoFileSize ?? 0) + trickplaySize;
}, 0);
} }

View File

@@ -289,24 +289,7 @@ export function useDownloadOperations({
); );
const appSizeUsage = useCallback(async () => { const appSizeUsage = useCallback(async () => {
let totalSize = calculateTotalDownloadedSize(); const totalSize = calculateTotalDownloadedSize();
// Also count in-progress downloads (they write straight to their final
// path) so the growing file shows up as app usage instead of drifting
// into the generic device share until completion.
for (const process of processes) {
try {
const file = new File(
Paths.document,
`${generateFilename(process.item)}.mp4`,
);
if (file.exists) {
totalSize += file.size ?? 0;
}
} catch {
// File not created yet — ignore.
}
}
try { try {
const [freeDiskStorage, totalDiskCapacity] = await Promise.all([ const [freeDiskStorage, totalDiskCapacity] = await Promise.all([
@@ -327,7 +310,7 @@ export function useDownloadOperations({
appSize: totalSize, appSize: totalSize,
}; };
} }
}, [processes]); }, []);
return { return {
startBackgroundDownload, startBackgroundDownload,

View File

@@ -386,7 +386,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
default: default:
throw new Error( throw new Error(
t( t(
"login.an_unexpected_error_occurred_did_you_enter_the_correct_url", "login.an_unexpected_error_occured_did_you_enter_the_correct_url",
), ),
); );
} }

View File

@@ -6,7 +6,6 @@ import {
useCallback, useCallback,
useContext, useContext,
useEffect, useEffect,
useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
@@ -19,7 +18,6 @@ interface ServerUrlContextValue {
effectiveServerUrl: string | null; effectiveServerUrl: string | null;
isUsingLocalUrl: boolean; isUsingLocalUrl: boolean;
currentSSID: string | null; currentSSID: string | null;
connectedToWifi: boolean;
refreshUrlState: () => void; refreshUrlState: () => void;
} }
@@ -34,7 +32,7 @@ interface Props {
export function ServerUrlProvider({ children }: Props): React.ReactElement { export function ServerUrlProvider({ children }: Props): React.ReactElement {
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const { switchServerUrl } = useJellyfin(); const { switchServerUrl } = useJellyfin();
const { ssid, connectedToWifi, permissionStatus } = useWifiSSID(); const { ssid, permissionStatus } = useWifiSSID();
const [isUsingLocalUrl, setIsUsingLocalUrl] = useState(false); const [isUsingLocalUrl, setIsUsingLocalUrl] = useState(false);
const [effectiveServerUrl, setEffectiveServerUrl] = useState<string | null>( const [effectiveServerUrl, setEffectiveServerUrl] = useState<string | null>(
@@ -103,25 +101,15 @@ export function ServerUrlProvider({ children }: Props): React.ReactElement {
}; };
}, [ssid, permissionStatus, evaluateAndSwitchUrl]); }, [ssid, permissionStatus, evaluateAndSwitchUrl]);
const value = useMemo(
() => ({
effectiveServerUrl,
isUsingLocalUrl,
currentSSID: ssid,
connectedToWifi,
refreshUrlState,
}),
[
effectiveServerUrl,
isUsingLocalUrl,
ssid,
connectedToWifi,
refreshUrlState,
],
);
return ( return (
<ServerUrlContext.Provider value={value}> <ServerUrlContext.Provider
value={{
effectiveServerUrl,
isUsingLocalUrl,
currentSSID: ssid,
refreshUrlState,
}}
>
{children} {children}
</ServerUrlContext.Provider> </ServerUrlContext.Provider>
); );

View File

@@ -1,157 +0,0 @@
import type React from "react";
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { Platform } from "react-native";
import { getSSID } from "@/modules/wifi-ssid";
export type PermissionStatus =
| "granted"
| "denied"
| "undetermined"
| "unavailable";
export interface UseWifiSSIDReturn {
ssid: string | null;
connectedToWifi: boolean;
permissionStatus: PermissionStatus;
requestPermission: () => Promise<boolean>;
isLoading: boolean;
}
const WifiSsidContext = createContext<UseWifiSSIDReturn | null>(null);
// WiFi SSID is not available on tvOS, and expo-location isn't needed there.
const Location = Platform.isTV ? null : require("expo-location");
const POLL_INTERVAL_MS = 10_000;
function mapLocationStatus(status: number | undefined): PermissionStatus {
if (!Location) return "unavailable";
switch (status) {
case Location.PermissionStatus?.GRANTED:
return "granted";
case Location.PermissionStatus?.DENIED:
return "denied";
default:
return "undetermined";
}
}
interface Props {
children: ReactNode;
}
/**
* Single source of truth for the current WiFi SSID and the location permission
* that gates it. State is shared via context so that granting permission in one
* place (e.g. the network settings screen) is reflected everywhere else — most
* importantly ServerUrlProvider, which switches between the local and remote
* server URLs based on the connected SSID.
*
* Previously each consumer created its own useWifiSSID instance with independent
* state, so the provider never learned that permission had been granted later
* in the session and reported "not connected to WiFi" forever.
*/
export function WifiSsidProvider({ children }: Props): React.ReactElement {
const [ssid, setSSID] = useState<string | null>(null);
const [connectedToWifi, setConnectedToWifi] = useState(false);
const [permissionStatus, setPermissionStatus] = useState<PermissionStatus>(
Platform.isTV ? "unavailable" : "undetermined",
);
const [isLoading, setIsLoading] = useState(!Platform.isTV);
const fetchSSID = useCallback(async () => {
if (Platform.isTV) return;
const result = await getSSID();
setSSID(result.ssid);
setConnectedToWifi(result.connectedToWifi);
}, []);
const requestPermission = useCallback(async (): Promise<boolean> => {
if (Platform.isTV || !Location) {
setPermissionStatus("unavailable");
return false;
}
try {
const { status } = await Location.requestForegroundPermissionsAsync();
const newStatus = mapLocationStatus(status);
setPermissionStatus(newStatus);
return newStatus === "granted";
} catch {
setPermissionStatus("unavailable");
return false;
}
}, []);
// Check the location permission once on mount. The poll effect below takes
// over fetching the SSID whenever permission becomes "granted".
useEffect(() => {
if (Platform.isTV || !Location) {
setIsLoading(false);
return;
}
let cancelled = false;
async function initialize() {
setIsLoading(true);
try {
const { status } = await Location.getForegroundPermissionsAsync();
if (cancelled) return;
setPermissionStatus(mapLocationStatus(status));
} catch {
if (!cancelled) setPermissionStatus("unavailable");
}
if (!cancelled) setIsLoading(false);
}
initialize();
return () => {
cancelled = true;
};
}, []);
// Fetch the SSID immediately, then poll, whenever permission is granted.
// Because permissionStatus is shared, this also covers the case where
// permission was granted later (from another consumer) than this provider's
// mount time — the original root cause of the "not connected to WiFi" bug.
useEffect(() => {
if (Platform.isTV || permissionStatus !== "granted") return;
fetchSSID();
const interval = setInterval(fetchSSID, POLL_INTERVAL_MS);
return () => clearInterval(interval);
}, [permissionStatus, fetchSSID]);
const value = useMemo(
() => ({
ssid,
connectedToWifi,
permissionStatus,
requestPermission,
isLoading,
}),
[ssid, connectedToWifi, permissionStatus, requestPermission, isLoading],
);
return (
<WifiSsidContext.Provider value={value}>
{children}
</WifiSsidContext.Provider>
);
}
export function useWifiSsid(): UseWifiSSIDReturn {
const context = useContext(WifiSsidContext);
if (!context) {
throw new Error("useWifiSsid must be used within WifiSsidProvider");
}
return context;
}

View File

@@ -16,14 +16,14 @@
"got_it": "حسنًا", "got_it": "حسنًا",
"connection_failed": "فشل الاتصال", "connection_failed": "فشل الاتصال",
"could_not_connect_to_server": "تعذر الاتصال بالخادم. يرجى التحقق من الرابط واتصال الشبكة.", "could_not_connect_to_server": "تعذر الاتصال بالخادم. يرجى التحقق من الرابط واتصال الشبكة.",
"an_unexpected_error_occurred": "حدث خطأ غير متوقع", "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_occurred_did_you_enter_the_correct_url": "حدث خطأ غير متوقع. هل أدخلت رابط الخادم بشكل صحيح؟", "an_unexpected_error_occured_did_you_enter_the_correct_url": "حدث خطأ غير متوقع. هل أدخلت رابط الخادم بشكل صحيح؟",
"too_old_server_text": "تم اكتشاف خادم Jellyfin غير مدعوم", "too_old_server_text": "تم اكتشاف خادم Jellyfin غير مدعوم",
"too_old_server_description": "يرجى تحديث Jellyfin إلى أحدث إصدار" "too_old_server_description": "يرجى تحديث Jellyfin إلى أحدث إصدار"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "تفويض الدخول السريع", "authorize_button": "تفويض الدخول السريع",
"enter_the_quick_connect_code": "أدخل رمز الدخول السريع...", "enter_the_quick_connect_code": "أدخل رمز الدخول السريع...",
"success": "نجاح", "success": "نجاح",
"quick_connect_authorized": "تم تفويض الدخول السريع", "quick_connect_autorized": "تم تفويض الدخول السريع",
"error": "خطأ", "error": "خطأ",
"invalid_code": "رمز غير صالح", "invalid_code": "رمز غير صالح",
"authorize": "تفويض" "authorize": "تفويض"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "إظهار روابط القائمة المخصصة", "show_custom_menu_links": "إظهار روابط القائمة المخصصة",
"show_large_home_carousel": "إظهار شريط العرض الكبير (تجريبي)", "show_large_home_carousel": "إظهار شريط العرض الكبير (تجريبي)",
"hide_libraries": "إخفاء المكتبات", "hide_libraries": "إخفاء المكتبات",
"select_libraries_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": "سرعة التشغيل الافتراضية",
@@ -384,8 +384,6 @@
"device_usage": "الجهاز {{availableSpace}}%", "device_usage": "الجهاز {{availableSpace}}%",
"size_used": "تم استخدام {{used}} من {{total}}", "size_used": "تم استخدام {{used}} من {{total}}",
"delete_all_downloaded_files": "حذف جميع الملفات التي تم تنزيلها", "delete_all_downloaded_files": "حذف جميع الملفات التي تم تنزيلها",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "التخزين المؤقت للموسيقى", "music_cache_title": "التخزين المؤقت للموسيقى",
"music_cache_description": "تخزين الأغاني تلقائياً أثناء الاستماع لضمان تشغيل أكثر سلاسة ودعم الاستماع بدون اتصال", "music_cache_description": "تخزين الأغاني تلقائياً أثناء الاستماع لضمان تشغيل أكثر سلاسة ودعم الاستماع بدون اتصال",
"clear_music_cache": "مسح التخزين المؤقت للموسيقى", "clear_music_cache": "مسح التخزين المؤقت للموسيقى",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "اختر",
"no_trailer_available": "لا يوجد مقطع دعائي متوفر", "no_trailer_available": "لا يوجد مقطع دعائي متوفر",
"video": "فيديو", "video": "فيديو",
"audio": "الصوت", "audio": "الصوت",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "mpv_player_title": "MPV Player",
"error": "خطأ", "error": "خطأ",
"failed_to_get_stream_url": "فشل في الحصول على رابط البث", "failed_to_get_stream_url": "فشل في الحصول على رابط البث",
"an_error_occurred_while_playing_the_video": "حدث خطأ أثناء تشغيل الفيديو. تحقق من السجلات في الإعدادات.", "an_error_occured_while_playing_the_video": "حدث خطأ أثناء تشغيل الفيديو. تحقق من السجلات في الإعدادات.",
"client_error": "خطأ في المشغّل", "client_error": "خطأ في المشغّل",
"could_not_create_stream_for_chromecast": "تعذر إنشاء بث لـChromecast", "could_not_create_stream_for_chromecast": "تعذر إنشاء بث لـChromecast",
"message_from_server": "رسالة من الخادم: {{message}}", "message_from_server": "رسالة من الخادم: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Entesos", "got_it": "Entesos",
"connection_failed": "Ha fallat la connexió", "connection_failed": "Ha fallat la connexió",
"could_not_connect_to_server": "No s'ha pogut connectar amb el servidor. Comproveu l'URL i la connexió de xarxa.", "could_not_connect_to_server": "No s'ha pogut connectar amb el servidor. Comproveu l'URL i la connexió de xarxa.",
"an_unexpected_error_occurred": "S'ha produït un error inesperat", "an_unexpected_error_occured": "S'ha produït un error inesperat",
"change_server": "Canvia el servidor", "change_server": "Canvia el servidor",
"invalid_username_or_password": "Nom d'usuari o contrasenya incorrectes", "invalid_username_or_password": "Nom d'usuari o contrasenya incorrectes",
"user_does_not_have_permission_to_log_in": "L'usuari no té permís per iniciar sessió", "user_does_not_have_permission_to_log_in": "L'usuari no té permís per iniciar sessió",
"server_is_taking_too_long_to_respond_try_again_later": "El servidor triga massa a respondre, torneu-ho a provar més tard", "server_is_taking_too_long_to_respond_try_again_later": "El servidor triga massa a respondre, torneu-ho a provar més tard",
"server_received_too_many_requests_try_again_later": "El servidor ha rebut massa sol·licituds, torneu-ho a provar més tard.", "server_received_too_many_requests_try_again_later": "El servidor ha rebut massa sol·licituds, torneu-ho a provar més tard.",
"there_is_a_server_error": "Error del servidor", "there_is_a_server_error": "Error del servidor",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "S'ha produït un error inesperat. Heu introduït correctament l'URL del servidor?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "S'ha produït un error inesperat. Heu introduït correctament l'URL del servidor?",
"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"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Autoritza connexió ràpida", "authorize_button": "Autoritza connexió ràpida",
"enter_the_quick_connect_code": "Introdueix el codi de connexió ràpida...", "enter_the_quick_connect_code": "Introdueix el codi de connexió ràpida...",
"success": "Èxit", "success": "Èxit",
"quick_connect_authorized": "Connexió ràpida autoritzada", "quick_connect_autorized": "Connexió ràpida autoritzada",
"error": "Error", "error": "Error",
"invalid_code": "Codi invàlid", "invalid_code": "Codi invàlid",
"authorize": "Autoritza" "authorize": "Autoritza"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Mostrar enllaços del menú personalitzats", "show_custom_menu_links": "Mostrar enllaços del menú personalitzats",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Oculta biblioteques", "hide_libraries": "Oculta biblioteques",
"select_libraries_you_want_to_hide": "Seleccioneu les biblioteques que voleu ocultar de la pestanya Biblioteca i de les seccions de la pàgina d'inici.", "select_liraries_you_want_to_hide": "Seleccioneu les biblioteques que voleu ocultar de la pestanya Biblioteca i de les seccions de la pàgina d'inici.",
"disable_haptic_feedback": "Desactiva la resposta hàptica", "disable_haptic_feedback": "Desactiva la resposta hàptica",
"default_quality": "Qualitat per defecte", "default_quality": "Qualitat per defecte",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Dispositiu {{availableSpace}}%", "device_usage": "Dispositiu {{availableSpace}}%",
"size_used": "{{used}} de {{total}} utilitzat", "size_used": "{{used}} de {{total}} utilitzat",
"delete_all_downloaded_files": "Suprimeix tots els fitxers descarregats", "delete_all_downloaded_files": "Suprimeix tots els fitxers descarregats",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Vídeo", "video": "Vídeo",
"audio": "Àudio", "audio": "Àudio",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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ó.",
"client_error": "Error del client", "client_error": "Error del client",
"could_not_create_stream_for_chromecast": "No s'ha pogut crear un flux per a Chromecast", "could_not_create_stream_for_chromecast": "No s'ha pogut crear un flux per a Chromecast",
"message_from_server": "Missatge del servidor: {{message}}", "message_from_server": "Missatge del servidor: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Rozumím", "got_it": "Rozumím",
"connection_failed": "Připojení se nezdařilo", "connection_failed": "Připojení se nezdařilo",
"could_not_connect_to_server": "Nelze se připojit k serveru. Zkontrolujte adresu URL a síťové připojení.", "could_not_connect_to_server": "Nelze se připojit k serveru. Zkontrolujte adresu URL a síťové připojení.",
"an_unexpected_error_occurred": "Došlo k neočekávané chybě", "an_unexpected_error_occured": "Došlo k neočekávané chybě",
"change_server": "Změnit server", "change_server": "Změnit server",
"invalid_username_or_password": "Neplatné uživatelské jméno nebo heslo", "invalid_username_or_password": "Neplatné uživatelské jméno nebo heslo",
"user_does_not_have_permission_to_log_in": "Uživatel nemá oprávnění k přihlášení", "user_does_not_have_permission_to_log_in": "Uživatel nemá oprávnění k přihlášení",
"server_is_taking_too_long_to_respond_try_again_later": "Reakce na server trvá příliš dlouho, zkuste to znovu později", "server_is_taking_too_long_to_respond_try_again_later": "Reakce na server trvá příliš dlouho, zkuste to znovu později",
"server_received_too_many_requests_try_again_later": "Server obdržel příliš mnoho požadavků, opakujte akci později.", "server_received_too_many_requests_try_again_later": "Server obdržel příliš mnoho požadavků, opakujte akci později.",
"there_is_a_server_error": "Došlo k chybě serveru", "there_is_a_server_error": "Došlo k chybě serveru",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Došlo k neočekávané chybě. Vložili jste adresu URL serveru?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Došlo k neočekávané chybě. Vložili jste adresu URL serveru?",
"too_old_server_text": "Objeven nepodporovaný Jellyfin server", "too_old_server_text": "Objeven nepodporovaný Jellyfin server",
"too_old_server_description": "Prosím aktualizujte Jellyfin na nejnovější verzi" "too_old_server_description": "Prosím aktualizujte Jellyfin na nejnovější verzi"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Autorizovat rychlé připojení", "authorize_button": "Autorizovat rychlé připojení",
"enter_the_quick_connect_code": "Zadejte kód rychlého připojení...", "enter_the_quick_connect_code": "Zadejte kód rychlého připojení...",
"success": "Úspěšně", "success": "Úspěšně",
"quick_connect_authorized": "Oprávněné Rychlé připojení", "quick_connect_autorized": "Oprávněné Rychlé připojení",
"error": "Chyba", "error": "Chyba",
"invalid_code": "Neplatný kód", "invalid_code": "Neplatný kód",
"authorize": "Autorizovat" "authorize": "Autorizovat"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Zobrazit vlastní Menu odkazy", "show_custom_menu_links": "Zobrazit vlastní Menu odkazy",
"show_large_home_carousel": "Zobrazit velký přehled (beta)", "show_large_home_carousel": "Zobrazit velký přehled (beta)",
"hide_libraries": "Skrýt knihovny", "hide_libraries": "Skrýt knihovny",
"select_libraries_you_want_to_hide": "Vyberte knihovny, které chcete skrýt v záložce Knihovna a v sekcích domovské stránky.", "select_liraries_you_want_to_hide": "Vyberte knihovny, které chcete skrýt v záložce Knihovna a v sekcích domovské stránky.",
"disable_haptic_feedback": "Zakázat Haptickou zpětnou vazbu", "disable_haptic_feedback": "Zakázat Haptickou zpětnou vazbu",
"default_quality": "Výchozí kvalita", "default_quality": "Výchozí kvalita",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Zařízení {{availableSpace}}%", "device_usage": "Zařízení {{availableSpace}}%",
"size_used": "{{used}} z {{total}} využito", "size_used": "{{used}} z {{total}} využito",
"delete_all_downloaded_files": "Odstranit všechny stažené soubory", "delete_all_downloaded_files": "Odstranit všechny stažené soubory",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"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",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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í.",
"client_error": "Chyba klienta", "client_error": "Chyba klienta",
"could_not_create_stream_for_chromecast": "Nelze vytvořit stream pro Chromecast", "could_not_create_stream_for_chromecast": "Nelze vytvořit stream pro Chromecast",
"message_from_server": "Zpráva od serveru: {{message}}", "message_from_server": "Zpráva od serveru: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Forstået", "got_it": "Forstået",
"connection_failed": "Forbindelsen mislykkedes", "connection_failed": "Forbindelsen mislykkedes",
"could_not_connect_to_server": "Kunne ikke oprette forbindelse til serveren. Tjek venligst URL'en og din netværksforbindelse.", "could_not_connect_to_server": "Kunne ikke oprette forbindelse til serveren. Tjek venligst URL'en og din netværksforbindelse.",
"an_unexpected_error_occurred": "Der opstod en uventet fejl", "an_unexpected_error_occured": "Der opstod en uventet fejl",
"change_server": "Skift server", "change_server": "Skift server",
"invalid_username_or_password": "Ugyldigt brugernavn eller adgangskode", "invalid_username_or_password": "Ugyldigt brugernavn eller adgangskode",
"user_does_not_have_permission_to_log_in": "Brugeren har ikke tilladelse til at logge ind", "user_does_not_have_permission_to_log_in": "Brugeren har ikke tilladelse til at logge ind",
"server_is_taking_too_long_to_respond_try_again_later": "Serveren svarer for langsomt, prøv igen senere", "server_is_taking_too_long_to_respond_try_again_later": "Serveren svarer for langsomt, prøv igen senere",
"server_received_too_many_requests_try_again_later": "Serveren modtog for mange forespørgsler, prøv igen senere.", "server_received_too_many_requests_try_again_later": "Serveren modtog for mange forespørgsler, prøv igen senere.",
"there_is_a_server_error": "Der er en serverfejl", "there_is_a_server_error": "Der er en serverfejl",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Der opstod en uventet fejl. Har du indtastet serverens URL korrekt?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Der opstod en uventet fejl. Har du indtastet serverens URL korrekt?",
"too_old_server_text": "Ikke Understøttet Jellyfin Server Opdaget", "too_old_server_text": "Ikke Understøttet Jellyfin Server Opdaget",
"too_old_server_description": "Opdater venligst Jellyfin til den seneste version" "too_old_server_description": "Opdater venligst Jellyfin til den seneste version"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Autoriser Hurtigforbindelse", "authorize_button": "Autoriser Hurtigforbindelse",
"enter_the_quick_connect_code": "Indtast koden til hurtigforbindelse...", "enter_the_quick_connect_code": "Indtast koden til hurtigforbindelse...",
"success": "Succes", "success": "Succes",
"quick_connect_authorized": "Hurtigforbindelse autoriseret", "quick_connect_autorized": "Hurtigforbindelse autoriseret",
"error": "Fejl", "error": "Fejl",
"invalid_code": "Ugyldig kode", "invalid_code": "Ugyldig kode",
"authorize": "Autoriser" "authorize": "Autoriser"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Vis tilpassede menulinks", "show_custom_menu_links": "Vis tilpassede menulinks",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Skjul biblioteker", "hide_libraries": "Skjul biblioteker",
"select_libraries_you_want_to_hide": "Vælg de biblioteker, du ønsker at skjule fra fanen Bibliotek og startside sektionerne.", "select_liraries_you_want_to_hide": "Vælg de biblioteker, du ønsker at skjule fra fanen Bibliotek og startside sektionerne.",
"disable_haptic_feedback": "Deaktiver haptisk feedback", "disable_haptic_feedback": "Deaktiver haptisk feedback",
"default_quality": "Standard kvalitet", "default_quality": "Standard kvalitet",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Enhedsforbrug: {{availableSpace}}%", "device_usage": "Enhedsforbrug: {{availableSpace}}%",
"size_used": "{{used}} af {{total}} brugt", "size_used": "{{used}} af {{total}} brugt",
"delete_all_downloaded_files": "Slet alle downloadede filer", "delete_all_downloaded_files": "Slet alle downloadede filer",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"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",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Klientfejl", "client_error": "Klientfejl",
"could_not_create_stream_for_chromecast": "Kunne ikke oprette en stream til Chromecast", "could_not_create_stream_for_chromecast": "Kunne ikke oprette en stream til Chromecast",
"message_from_server": "Besked fra server: {{message}}", "message_from_server": "Besked fra server: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Verstanden", "got_it": "Verstanden",
"connection_failed": "Verbindung fehlgeschlagen", "connection_failed": "Verbindung fehlgeschlagen",
"could_not_connect_to_server": "Verbindung zum Server fehlgeschlagen. Bitte überprüf die URL und deine Netzwerkverbindung.", "could_not_connect_to_server": "Verbindung zum Server fehlgeschlagen. Bitte überprüf die URL und deine Netzwerkverbindung.",
"an_unexpected_error_occurred": "Ein unerwarteter Fehler ist aufgetreten", "an_unexpected_error_occured": "Ein unerwarteter Fehler ist aufgetreten",
"change_server": "Server wechseln", "change_server": "Server wechseln",
"invalid_username_or_password": "Ungültiger Benutzername oder Passwort", "invalid_username_or_password": "Ungültiger Benutzername oder Passwort",
"user_does_not_have_permission_to_log_in": "Benutzer hat keine Berechtigung, um sich anzumelden", "user_does_not_have_permission_to_log_in": "Benutzer hat keine Berechtigung, um sich anzumelden",
"server_is_taking_too_long_to_respond_try_again_later": "Der Server benötigt zu lange, um zu antworten. Bitte versuch es später erneut", "server_is_taking_too_long_to_respond_try_again_later": "Der Server benötigt zu lange, um zu antworten. Bitte versuch es später erneut",
"server_received_too_many_requests_try_again_later": "Der Server hat zu viele Anfragen erhalten. Bitte versuch es später erneut.", "server_received_too_many_requests_try_again_later": "Der Server hat zu viele Anfragen erhalten. Bitte versuch es später erneut.",
"there_is_a_server_error": "Es gibt einen Serverfehler", "there_is_a_server_error": "Es gibt einen Serverfehler",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Ein unerwarteter Fehler ist aufgetreten. Hast du die Server-URL korrekt eingegeben?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ein unerwarteter Fehler ist aufgetreten. Hast du die Server-URL korrekt eingegeben?",
"too_old_server_text": "Nicht unterstützter Jellyfin Server entdeckt", "too_old_server_text": "Nicht unterstützter Jellyfin Server entdeckt",
"too_old_server_description": "Bitte aktualisiere Jellyfin auf die neueste Version" "too_old_server_description": "Bitte aktualisiere Jellyfin auf die neueste Version"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Quick Connect autorisieren", "authorize_button": "Quick Connect autorisieren",
"enter_the_quick_connect_code": "Quick Connect-Code eingeben...", "enter_the_quick_connect_code": "Quick Connect-Code eingeben...",
"success": "Erfolgreich verbunden", "success": "Erfolgreich verbunden",
"quick_connect_authorized": "Quick Connect autorisiert", "quick_connect_autorized": "Quick Connect autorisiert",
"error": "Fehler", "error": "Fehler",
"invalid_code": "Ungültiger Code", "invalid_code": "Ungültiger Code",
"authorize": "Autorisieren" "authorize": "Autorisieren"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen", "show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen",
"show_large_home_carousel": "Zeige große Startseiten-Übersicht (Beta)", "show_large_home_carousel": "Zeige große Startseiten-Übersicht (Beta)",
"hide_libraries": "Bibliotheken ausblenden", "hide_libraries": "Bibliotheken ausblenden",
"select_libraries_you_want_to_hide": "Bibliotheken auswählen die aus dem Bibliothekstab und der Startseite ausgeblendet werden sollen.", "select_liraries_you_want_to_hide": "Bibliotheken auswählen die aus dem Bibliothekstab und der Startseite ausgeblendet werden sollen.",
"disable_haptic_feedback": "Haptisches Feedback deaktivieren", "disable_haptic_feedback": "Haptisches Feedback deaktivieren",
"default_quality": "Standardqualität", "default_quality": "Standardqualität",
"default_playback_speed": "Standard-Wiedergabegeschwindigkeit", "default_playback_speed": "Standard-Wiedergabegeschwindigkeit",
@@ -384,8 +384,6 @@
"device_usage": "Gerät {{availableSpace}}%", "device_usage": "Gerät {{availableSpace}}%",
"size_used": "{{used}} von {{total}} genutzt", "size_used": "{{used}} von {{total}} genutzt",
"delete_all_downloaded_files": "Alle heruntergeladenen Dateien löschen", "delete_all_downloaded_files": "Alle heruntergeladenen Dateien löschen",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Musik-Cache", "music_cache_title": "Musik-Cache",
"music_cache_description": "Beim Anhören Titel automatisch in den Cache laden um bessere Wiedergabe und Offline-Wiedergabe zu ermöglichen", "music_cache_description": "Beim Anhören Titel automatisch in den Cache laden um bessere Wiedergabe und Offline-Wiedergabe zu ermöglichen",
"clear_music_cache": "Musik-Cache leeren", "clear_music_cache": "Musik-Cache leeren",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Auswählen",
"no_trailer_available": "Kein Trailer verfügbar", "no_trailer_available": "Kein Trailer verfügbar",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Client-Fehler", "client_error": "Client-Fehler",
"could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen", "could_not_create_stream_for_chromecast": "Konnte keinen Stream für Chromecast erstellen",
"message_from_server": "Nachricht vom Server: {{message}}", "message_from_server": "Nachricht vom Server: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Sprache", "language": "Sprache",
"results": "Ergebnisse", "results": "Ergebnisse",
"searching": "Suche ...",
"search_failed": "Suche fehlgeschlagen", "search_failed": "Suche fehlgeschlagen",
"no_subtitle_provider": "Kein Untertitelanbieter auf dem Server konfiguriert", "no_subtitle_provider": "Kein Untertitelanbieter auf dem Server konfiguriert",
"no_subtitles_found": "Keine Untertitel gefunden", "no_subtitles_found": "Keine Untertitel gefunden",

View File

@@ -16,14 +16,14 @@
"got_it": "Το Κατάλαβα", "got_it": "Το Κατάλαβα",
"connection_failed": "Η Σύνδεση Απέτυχε", "connection_failed": "Η Σύνδεση Απέτυχε",
"could_not_connect_to_server": "Δεν ήταν δυνατή η σύνδεση με τον διακομιστή. Παρακαλώ ελέγξτε τη διεύθυνση URL και τη σύνδεση δικτύου σας.", "could_not_connect_to_server": "Δεν ήταν δυνατή η σύνδεση με τον διακομιστή. Παρακαλώ ελέγξτε τη διεύθυνση URL και τη σύνδεση δικτύου σας.",
"an_unexpected_error_occurred": "Παρουσιάστηκε Απροσδόκητο Σφάλμα", "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_occurred_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 στην τελευταία έκδοση"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Εξουσιοδότηση Γρήγορης Σύνδεσης", "authorize_button": "Εξουσιοδότηση Γρήγορης Σύνδεσης",
"enter_the_quick_connect_code": "Εισάγετε τον κωδικό γρήγορης σύνδεσης...", "enter_the_quick_connect_code": "Εισάγετε τον κωδικό γρήγορης σύνδεσης...",
"success": "Επιτυχία", "success": "Επιτυχία",
"quick_connect_authorized": "Γρήγορη Σύνδεση Εξουσιοδοτήθηκε", "quick_connect_autorized": "Γρήγορη Σύνδεση Εξουσιοδοτήθηκε",
"error": "Σφάλμα", "error": "Σφάλμα",
"invalid_code": "Μη Έγκυρος Κωδικός", "invalid_code": "Μη Έγκυρος Κωδικός",
"authorize": "Εξουσιοδότηση" "authorize": "Εξουσιοδότηση"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Εμφάνιση Προσαρμοσμένων Συνδέσμων Μενού", "show_custom_menu_links": "Εμφάνιση Προσαρμοσμένων Συνδέσμων Μενού",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Απόκρυψη Βιβλιοθηκών", "hide_libraries": "Απόκρυψη Βιβλιοθηκών",
"select_libraries_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": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Device {{availableSpace}}%", "device_usage": "Device {{availableSpace}}%",
"size_used": "{{used}} {{total}} Χρησιμοποιείται", "size_used": "{{used}} {{total}} Χρησιμοποιείται",
"delete_all_downloaded_files": "Διαγραφή Όλων Των Ληφθέντων Αρχείων", "delete_all_downloaded_files": "Διαγραφή Όλων Των Ληφθέντων Αρχείων",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Επιλογή",
"no_trailer_available": "Δεν υπάρχει διαθέσιμο ρυμουλκούμενο", "no_trailer_available": "Δεν υπάρχει διαθέσιμο ρυμουλκούμενο",
"video": "Βίντεο", "video": "Βίντεο",
"audio": "Ήχος", "audio": "Ήχος",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "mpv_player_title": "MPV Player",
"error": "Σφάλμα", "error": "Σφάλμα",
"failed_to_get_stream_url": "Αποτυχία λήψης του URL ροής", "failed_to_get_stream_url": "Αποτυχία λήψης του URL ροής",
"an_error_occurred_while_playing_the_video": "Παρουσιάστηκε σφάλμα κατά την αναπαραγωγή του βίντεο. Ελέγξτε τα αρχεία καταγραφής στις ρυθμίσεις.", "an_error_occured_while_playing_the_video": "Παρουσιάστηκε σφάλμα κατά την αναπαραγωγή του βίντεο. Ελέγξτε τα αρχεία καταγραφής στις ρυθμίσεις.",
"client_error": "Σφάλμα Πελάτη", "client_error": "Σφάλμα Πελάτη",
"could_not_create_stream_for_chromecast": "Αδυναμία δημιουργίας ροής για το Chromecast", "could_not_create_stream_for_chromecast": "Αδυναμία δημιουργίας ροής για το Chromecast",
"message_from_server": "Μήνυμα από το διακομιστή: {{message}}", "message_from_server": "Μήνυμα από το διακομιστή: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Got it", "got_it": "Got it",
"connection_failed": "Connection failed", "connection_failed": "Connection failed",
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.", "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
"an_unexpected_error_occurred": "An unexpected error occurred", "an_unexpected_error_occured": "An unexpected error occurred",
"change_server": "Change server", "change_server": "Change server",
"invalid_username_or_password": "Invalid username or password", "invalid_username_or_password": "Invalid username or password",
"user_does_not_have_permission_to_log_in": "User does not have permission to log in", "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_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later",
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
"there_is_a_server_error": "There is a server error", "there_is_a_server_error": "There is a server error",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
"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"
}, },
@@ -174,10 +174,7 @@
"network_already_added": "Network already added", "network_already_added": "Network already added",
"no_wifi_connected": "Not connected to Wi-Fi", "no_wifi_connected": "Not connected to Wi-Fi",
"permission_denied": "Location permission denied", "permission_denied": "Location permission denied",
"permission_denied_explanation": "Location permission is required to detect the Wi-Fi network for auto-switching. Please enable it in Settings.", "permission_denied_explanation": "Location permission is required to detect the Wi-Fi network for auto-switching. Please enable it in Settings."
"ssid_hidden": "SSID hidden",
"location_off_description": "Connected to Wi-Fi, but the network name is hidden. Enable device Location services and grant the app location permission to detect it.",
"open_location_settings": "Open Location settings"
}, },
"user_info": { "user_info": {
"user_info_title": "User info", "user_info_title": "User info",
@@ -191,7 +188,7 @@
"authorize_button": "Authorize Quick Connect", "authorize_button": "Authorize Quick Connect",
"enter_the_quick_connect_code": "Enter the Quick Connect code...", "enter_the_quick_connect_code": "Enter the Quick Connect code...",
"success": "Success", "success": "Success",
"quick_connect_authorized": "Quick Connect authorized", "quick_connect_autorized": "Quick Connect authorized",
"error": "Error", "error": "Error",
"invalid_code": "Invalid code", "invalid_code": "Invalid code",
"authorize": "Authorize" "authorize": "Authorize"
@@ -301,7 +298,7 @@
"show_custom_menu_links": "Show custom menu links", "show_custom_menu_links": "Show custom menu links",
"show_large_home_carousel": "Show large home carousel (beta)", "show_large_home_carousel": "Show large home carousel (beta)",
"hide_libraries": "Hide libraries", "hide_libraries": "Hide libraries",
"select_libraries_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",
"default_playback_speed": "Default playback speed", "default_playback_speed": "Default playback speed",
@@ -387,8 +384,6 @@
"device_usage": "Device {{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",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music cache", "music_cache_title": "Music cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear music cache", "clear_music_cache": "Clear music cache",
@@ -603,7 +598,7 @@
"mpv_player_title": "MPV player", "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_occurred_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.",
"client_error": "Client error", "client_error": "Client error",
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from server: {{message}}", "message_from_server": "Message from server: {{message}}",

View File

@@ -16,14 +16,14 @@
"got_it": "Entendido", "got_it": "Entendido",
"connection_failed": "Conexión fallida", "connection_failed": "Conexión fallida",
"could_not_connect_to_server": "No se pudo conectar al servidor. Por favor comprueba la URL y tu conexión de red.", "could_not_connect_to_server": "No se pudo conectar al servidor. Por favor comprueba la URL y tu conexión de red.",
"an_unexpected_error_occurred": "Ha ocurrido un error inesperado", "an_unexpected_error_occured": "Ha ocurrido un error inesperado",
"change_server": "Cambiar servidor", "change_server": "Cambiar servidor",
"invalid_username_or_password": "Usuario o contraseña inválidos", "invalid_username_or_password": "Usuario o contraseña inválidos",
"user_does_not_have_permission_to_log_in": "El usuario no tiene permiso para iniciar sesión", "user_does_not_have_permission_to_log_in": "El usuario no tiene permiso para iniciar sesión",
"server_is_taking_too_long_to_respond_try_again_later": "El servidor está tardando mucho en responder, inténtalo de nuevo más tarde.", "server_is_taking_too_long_to_respond_try_again_later": "El servidor está tardando mucho en responder, inténtalo de nuevo más tarde.",
"server_received_too_many_requests_try_again_later": "El servidor está recibiendo muchas peticiones, inténtalo de nuevo más tarde.", "server_received_too_many_requests_try_again_later": "El servidor está recibiendo muchas peticiones, inténtalo de nuevo más tarde.",
"there_is_a_server_error": "Hay un error en el servidor", "there_is_a_server_error": "Hay un error en el servidor",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Ha ocurrido un error inesperado. ¿Has introducido la URL correcta?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ha ocurrido un error inesperado. ¿Has introducido la URL correcta?",
"too_old_server_text": "Servidor Jellyfin no soportado descubierto", "too_old_server_text": "Servidor Jellyfin no soportado descubierto",
"too_old_server_description": "Por favor, actualiza Jellyfin a la última versión" "too_old_server_description": "Por favor, actualiza Jellyfin a la última versión"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Autorizar conexión rápida", "authorize_button": "Autorizar conexión rápida",
"enter_the_quick_connect_code": "Introduce el código de conexión rápida...", "enter_the_quick_connect_code": "Introduce el código de conexión rápida...",
"success": "Hecho", "success": "Hecho",
"quick_connect_authorized": "Conexión rápida autorizada", "quick_connect_autorized": "Conexión rápida autorizada",
"error": "Error", "error": "Error",
"invalid_code": "Código inválido", "invalid_code": "Código inválido",
"authorize": "Autorizar" "authorize": "Autorizar"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Mostrar enlaces de menú personalizados", "show_custom_menu_links": "Mostrar enlaces de menú personalizados",
"show_large_home_carousel": "Mostrar carrusel del menú principal grande (beta)", "show_large_home_carousel": "Mostrar carrusel del menú principal grande (beta)",
"hide_libraries": "Ocultar bibliotecas", "hide_libraries": "Ocultar bibliotecas",
"select_libraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.", "select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
"disable_haptic_feedback": "Desactivar feedback háptico", "disable_haptic_feedback": "Desactivar feedback háptico",
"default_quality": "Calidad por defecto", "default_quality": "Calidad por defecto",
"default_playback_speed": "Velocidad de reproducción predeterminada", "default_playback_speed": "Velocidad de reproducción predeterminada",
@@ -384,8 +384,6 @@
"device_usage": "Dispositivo {{availableSpace}}%", "device_usage": "Dispositivo {{availableSpace}}%",
"size_used": "{{used}} de {{total}} usado", "size_used": "{{used}} de {{total}} usado",
"delete_all_downloaded_files": "Eliminar todos los archivos descargados", "delete_all_downloaded_files": "Eliminar todos los archivos descargados",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Caché de música", "music_cache_title": "Caché de música",
"music_cache_description": "Cachear automáticamente las canciones mientras escuchas una reproducción más suave y soporte sin conexión", "music_cache_description": "Cachear automáticamente las canciones mientras escuchas una reproducción más suave y soporte sin conexión",
"clear_music_cache": "Borrar Caché de Música", "clear_music_cache": "Borrar Caché de Música",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"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",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Error del cliente", "client_error": "Error del cliente",
"could_not_create_stream_for_chromecast": "No se pudo crear el Steam para Chromecast", "could_not_create_stream_for_chromecast": "No se pudo crear el Steam para Chromecast",
"message_from_server": "Mensaje del servidor: {{message}}", "message_from_server": "Mensaje del servidor: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Okei", "got_it": "Okei",
"connection_failed": "Yhteys epäonnistui", "connection_failed": "Yhteys epäonnistui",
"could_not_connect_to_server": "Yhteyttä palvelimeen ei voitu muodostaa. Tarkista URL-osoite ja verkkoyhteytesi.", "could_not_connect_to_server": "Yhteyttä palvelimeen ei voitu muodostaa. Tarkista URL-osoite ja verkkoyhteytesi.",
"an_unexpected_error_occurred": "Odottamaton virhe tapahtui", "an_unexpected_error_occured": "Odottamaton virhe tapahtui",
"change_server": "Vaihda palvelinta", "change_server": "Vaihda palvelinta",
"invalid_username_or_password": "Virheellinen käyttäjätunnus tai salasana", "invalid_username_or_password": "Virheellinen käyttäjätunnus tai salasana",
"user_does_not_have_permission_to_log_in": "Käyttäjällä ei ole oikeuksia kirjautua sisään", "user_does_not_have_permission_to_log_in": "Käyttäjällä ei ole oikeuksia kirjautua sisään",
"server_is_taking_too_long_to_respond_try_again_later": "Palvelin reagoi liian hitaasti, yritä myöhemmin uudelleen", "server_is_taking_too_long_to_respond_try_again_later": "Palvelin reagoi liian hitaasti, yritä myöhemmin uudelleen",
"server_received_too_many_requests_try_again_later": "Palvelin sai liian monta pyyntöä, yritä myöhemmin uudelleen.", "server_received_too_many_requests_try_again_later": "Palvelin sai liian monta pyyntöä, yritä myöhemmin uudelleen.",
"there_is_a_server_error": "Palvelimessa on virhe", "there_is_a_server_error": "Palvelimessa on virhe",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Odottamaton virhe tapahtui. Syötitkö palvelimen URL-osoitteen oikein?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Odottamaton virhe tapahtui. Syötitkö palvelimen URL-osoitteen oikein?",
"too_old_server_text": "Ei-tuettu Jellyfin-palvelin löydetty", "too_old_server_text": "Ei-tuettu Jellyfin-palvelin löydetty",
"too_old_server_description": "Ole hyvä ja päivitä Jellyfin uusimpaan versioon" "too_old_server_description": "Ole hyvä ja päivitä Jellyfin uusimpaan versioon"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Valtuuta pikayhdistys", "authorize_button": "Valtuuta pikayhdistys",
"enter_the_quick_connect_code": "Syötä nopean yhteyden koodi...", "enter_the_quick_connect_code": "Syötä nopean yhteyden koodi...",
"success": "Onnistui", "success": "Onnistui",
"quick_connect_authorized": "Pikayhdistys valtuutettu", "quick_connect_autorized": "Pikayhdistys valtuutettu",
"error": "Virhe", "error": "Virhe",
"invalid_code": "Virheellinen koodi", "invalid_code": "Virheellinen koodi",
"authorize": "Valtuuta" "authorize": "Valtuuta"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Näytä mukautetut valikkolinkit", "show_custom_menu_links": "Näytä mukautetut valikkolinkit",
"show_large_home_carousel": "Näytä suuri kotikaruselli (beta)", "show_large_home_carousel": "Näytä suuri kotikaruselli (beta)",
"hide_libraries": "Piilota kirjastot", "hide_libraries": "Piilota kirjastot",
"select_libraries_you_want_to_hide": "Valitse kirjastot, jotka haluat piilottaa Kirjasto-välilehdeltä ja etusivun osioista.", "select_liraries_you_want_to_hide": "Valitse kirjastot, jotka haluat piilottaa Kirjasto-välilehdeltä ja etusivun osioista.",
"disable_haptic_feedback": "Poista haptinen palautteet käytöstä", "disable_haptic_feedback": "Poista haptinen palautteet käytöstä",
"default_quality": "Oletuslaatu", "default_quality": "Oletuslaatu",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Laitteen käyttö {{availableSpace}}%", "device_usage": "Laitteen käyttö {{availableSpace}}%",
"size_used": "{{used}} / {{total}} käytössä", "size_used": "{{used}} / {{total}} käytössä",
"delete_all_downloaded_files": "Poista kaikki ladatut tiedostot", "delete_all_downloaded_files": "Poista kaikki ladatut tiedostot",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Valitse",
"no_trailer_available": "Perävaunua ei saatavilla", "no_trailer_available": "Perävaunua ei saatavilla",
"video": "Video", "video": "Video",
"audio": "Ääni", "audio": "Ääni",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Asiakkaan Virhe", "client_error": "Asiakkaan Virhe",
"could_not_create_stream_for_chromecast": "Suoratoistoa ei voitu luoda Chromecastia varten", "could_not_create_stream_for_chromecast": "Suoratoistoa ei voitu luoda Chromecastia varten",
"message_from_server": "Viesti palvelimelta: {{message}}", "message_from_server": "Viesti palvelimelta: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -4,9 +4,9 @@
"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": "Sélectionnez un utilisateur pour vous connecter", "select_user": "Select a user to log in",
"add_user_to_login": "Ajouter un utilisateur pour se connecter", "add_user_to_login": "Add a user to log in",
"add_user": "Ajouter un utilisateur", "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",
@@ -16,14 +16,14 @@
"got_it": "D'accord", "got_it": "D'accord",
"connection_failed": "La connexion a échoué", "connection_failed": "La connexion a échoué",
"could_not_connect_to_server": "Impossible de se connecter au serveur. Veuillez vérifier lURL et votre connexion réseau.", "could_not_connect_to_server": "Impossible de se connecter au serveur. Veuillez vérifier lURL et votre connexion réseau.",
"an_unexpected_error_occurred": "Une erreur inattendue s'est produite", "an_unexpected_error_occured": "Une erreur inattendue s'est produite",
"change_server": "Changer de serveur", "change_server": "Changer de serveur",
"invalid_username_or_password": "Nom d'utilisateur ou mot de passe invalide", "invalid_username_or_password": "Nom d'utilisateur ou mot de passe invalide",
"user_does_not_have_permission_to_log_in": "L'utilisateur n'a pas la permission de se connecter", "user_does_not_have_permission_to_log_in": "L'utilisateur n'a pas la permission de se connecter",
"server_is_taking_too_long_to_respond_try_again_later": "Le serveur prend trop de temps à répondre, réessayez plus tard", "server_is_taking_too_long_to_respond_try_again_later": "Le serveur prend trop de temps à répondre, réessayez plus tard",
"server_received_too_many_requests_try_again_later": "Le serveur a reçu trop de demandes, réessayez plus tard.", "server_received_too_many_requests_try_again_later": "Le serveur a reçu trop de demandes, réessayez plus tard.",
"there_is_a_server_error": "Il y a une erreur de serveur", "there_is_a_server_error": "Il y a une erreur de serveur",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Une erreur inattendue sest produite. Avez-vous correctement saisi lURL du serveur ?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Une erreur inattendue sest produite. Avez-vous correctement saisi lURL du serveur ?",
"too_old_server_text": "Serveur Jellyfin non pris en charge découvert", "too_old_server_text": "Serveur Jellyfin non pris en charge découvert",
"too_old_server_description": "Veuillez mettre à jour Jellyfin vers la dernière version" "too_old_server_description": "Veuillez mettre à jour Jellyfin vers la dernière version"
}, },
@@ -42,19 +42,19 @@
"please_login_again": "Votre session enregistrée a expiré. Veuillez vous connecter à nouveau.", "please_login_again": "Votre session enregistrée a expiré. Veuillez vous connecter à nouveau.",
"remove_saved_login": "Supprimer l'identifiant enregistré", "remove_saved_login": "Supprimer l'identifiant enregistré",
"remove_saved_login_description": "Cela supprimera vos identifiants enregistrés pour ce serveur. Vous devrez saisir à nouveau votre nom dutilisateur et votre mot de passe la prochaine fois.", "remove_saved_login_description": "Cela supprimera vos identifiants enregistrés pour ce serveur. Vous devrez saisir à nouveau votre nom dutilisateur et votre mot de passe la prochaine fois.",
"accounts_count": "{{count}} comptes", "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": "Supprimer le serveur", "remove_server": "Remove Server",
"remove_server_description": "Ceci supprimera {{server}} et tous les comptes enregistrés de votre liste.", "remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
"select_your_server": "Sélectionnez votre serveur", "select_your_server": "Select Your Server",
"add_server_to_get_started": "Ajouter un serveur pour commencer", "add_server_to_get_started": "Add a server to get started",
"add_server": "Ajouter un serveur", "add_server": "Add Server",
"change_server": "Changer de serveur" "change_server": "Change Server"
}, },
"save_account": { "save_account": {
"title": "Enregistrer le compte", "title": "Sauvegarder le compte",
"save_for_later": "Enregistrer ce compte", "save_for_later": "Enregistrer ce compte",
"security_option": "Options de sécurité", "security_option": "Options de sécurité",
"no_protection": "Aucune protection", "no_protection": "Aucune protection",
@@ -95,9 +95,9 @@
"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": "Continuer", "continue": "Continue",
"next_up": "À suivre", "next_up": "À suivre",
"continue_and_next_up": "Continuer à 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}}",
"suggested_movies": "Films suggérés", "suggested_movies": "Films suggérés",
"suggested_episodes": "Épisodes suggérés", "suggested_episodes": "Épisodes suggérés",
@@ -120,10 +120,10 @@
"settings_title": "Paramètres", "settings_title": "Paramètres",
"log_out_button": "Déconnexion", "log_out_button": "Déconnexion",
"switch_user": { "switch_user": {
"title": "Changer d'utilisateur", "title": "Switch User",
"account": "Compte", "account": "Account",
"switch_user": "Changer d'utilisateur sur ce serveur", "switch_user": "Switch User on This Server",
"current": "actuel" "current": "current"
}, },
"categories": { "categories": {
"title": "Catégories" "title": "Catégories"
@@ -136,32 +136,32 @@
}, },
"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": "Arrière-plan d'accueil dynamique", "show_home_backdrop": "Dynamic Home Backdrop",
"show_hero_carousel": "Carrousel principal", "show_hero_carousel": "Hero Carousel",
"show_series_poster_on_episode": "Afficher l'affiche de la série sur les épisodes", "show_series_poster_on_episode": "Show Series Poster on Episodes",
"theme_music": "Musique de thème", "theme_music": "Theme Music",
"display_size": "Taille d'affichage", "display_size": "Display Size",
"display_size_small": "Petite", "display_size_small": "Small",
"display_size_default": "Par défaut", "display_size_default": "Default",
"display_size_large": "Grande", "display_size_large": "Large",
"display_size_extra_large": "Très grande" "display_size_extra_large": "Extra Large"
}, },
"network": { "network": {
"title": "Réseau", "title": "Réseau",
"local_network": "Réseau local", "local_network": "Réseau local",
"auto_switch_enabled": "Basculement automatique à la maison", "auto_switch_enabled": "Basculement automatique quand à la maison",
"auto_switch_description": "Basculer automatiquement vers l'URL locale lorsque vous êtes connecté au Wi-Fi de la maison", "auto_switch_description": "Basculer automatiquement vers l'URL locale lorsque vous êtes connecté au Wi-Fi de la maison",
"local_url": "URL locale", "local_url": "URL locale",
"local_url_hint": "Entrez l'adresse de votre serveur local (par exemple http://192.168.1.100:8096)", "local_url_hint": "Entrez l'adresse de votre serveur local (exemple, 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": "Réseaux Wi-Fi domestiques", "home_wifi_networks": "Réseaux Wi-Fi domestiques",
"add_current_network": "Ajouter \"{{ssid}}\"", "add_current_network": "Ajouter \"{{ssid}}\"",
"not_connected_to_wifi": "Non connecté au Wi-Fi", "not_connected_to_wifi": "Non connecté au WiFi",
"no_networks_configured": "Pas de réseau configuré", "no_networks_configured": "Pas de réseau configuré",
"add_network_hint": "Ajouter votre réseau Wi-Fi domestique pour activer le basculement automatique", "add_network_hint": "Ajouter votre réseau Wi-Fi domestique pour activer la commutation automatique",
"current_wifi": "Wi-Fi actuel", "current_wifi": "WiFi actuel",
"using_url": "Utilisant", "using_url": "Utilisant",
"local": "URL locale", "local": "URL locale",
"remote": "URL à distance", "remote": "URL à distance",
@@ -172,9 +172,9 @@
"not_configured": "Non configuré", "not_configured": "Non configuré",
"network_added": "Réseau ajouté", "network_added": "Réseau ajouté",
"network_already_added": "Réseau déjà ajouté", "network_already_added": "Réseau déjà ajouté",
"no_wifi_connected": "Non connecté au Wi-Fi", "no_wifi_connected": "Non connecté au WiFi",
"permission_denied": "Autorisation de localisation refusée", "permission_denied": "Autorisation de localisation refusée",
"permission_denied_explanation": "Une autorisation de localisation est requise pour détecter le réseau Wi-Fi afin de basculer automatiquement. Veuillez l'activer dans les paramètres." "permission_denied_explanation": "Une autorisation de localisation est requise pour détecter le réseau Wifi afin de changer automatiquement. Veuillez lactiver dans les paramètres."
}, },
"user_info": { "user_info": {
"user_info_title": "Informations utilisateur", "user_info_title": "Informations utilisateur",
@@ -184,35 +184,35 @@
"app_version": "Version de l'application" "app_version": "Version de l'application"
}, },
"quick_connect": { "quick_connect": {
"quick_connect_title": "Connexion rapide", "quick_connect_title": "Connexion Rapide",
"authorize_button": "Autoriser une connexion rapide", "authorize_button": "Autoriser une Connexion Rapide",
"enter_the_quick_connect_code": "Entrez le code de connexion rapide...", "enter_the_quick_connect_code": "Entrez le code de Connexion Rapide...",
"success": "Succès", "success": "Succès",
"quick_connect_authorized": "Connexion rapide autorisée", "quick_connect_autorized": "Connexion Rapide autorisé",
"error": "Erreur", "error": "Erreur",
"invalid_code": "Code invalide", "invalid_code": "Code invalide",
"authorize": "Autoriser" "authorize": "Autoriser"
}, },
"media_controls": { "media_controls": {
"media_controls_title": "Contrôles média", "media_controls_title": "Contrôles Média",
"forward_skip_length": "Durée de saut en avant", "forward_skip_length": "Durée de saut en avant",
"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": { "buffer": {
"title": "Paramètres du tampon", "title": "Buffer Settings",
"cache_mode": "Mode de cache", "cache_mode": "Cache Mode",
"cache_auto": "Auto", "cache_auto": "Auto",
"cache_yes": "Activé", "cache_yes": "Enabled",
"cache_no": "Désactivé", "cache_no": "Disabled",
"buffer_duration": "Durée du tampon", "buffer_duration": "Buffer Duration",
"max_cache_size": "Taille maximale du cache", "max_cache_size": "Max Cache Size",
"max_backward_cache": "Cache arrière maximal" "max_backward_cache": "Max Backward Cache"
}, },
"vo_driver": { "vo_driver": {
"title": "Sortie vidéo", "title": "Video Output",
"vo_mode": "Pilote VO", "vo_mode": "VO Driver",
"gpu_next": "gpu-next (Recommandé)", "gpu_next": "gpu-next (Recommended)",
"gpu": "gpu" "gpu": "gpu"
}, },
"gesture_controls": { "gesture_controls": {
@@ -225,8 +225,8 @@
"right_side_volume_description": "Glisser vers le haut/bas sur le côté droit pour ajuster le volume", "right_side_volume_description": "Glisser vers le haut/bas sur le côté droit pour ajuster le volume",
"hide_volume_slider": "Masquer le curseur de volume", "hide_volume_slider": "Masquer le curseur de volume",
"hide_volume_slider_description": "Masquer le curseur de volume dans le lecteur vidéo", "hide_volume_slider_description": "Masquer le curseur de volume dans le lecteur vidéo",
"hide_brightness_slider": "Masquer le curseur de luminosité", "hide_brightness_slider": "Cacher le curseur de luminosité",
"hide_brightness_slider_description": "Masquer le curseur de luminosité dans le lecteur vidéo" "hide_brightness_slider_description": "Masquer le curseur de volume dans le lecteur vidéo"
}, },
"audio": { "audio": {
"audio_title": "Audio", "audio_title": "Audio",
@@ -234,14 +234,14 @@
"audio_language": "Langue audio", "audio_language": "Langue audio",
"audio_hint": "Choisissez une langue audio par défaut.", "audio_hint": "Choisissez une langue audio par défaut.",
"none": "Aucune", "none": "Aucune",
"language": "Langue", "language": "Langage",
"transcode_mode": { "transcode_mode": {
"title": "Transcodage audio", "title": "Transcodage audio",
"description": "Contrôle la gestion de l'audio surround (7.1, TrueHD, DTS-HD)", "description": "Contrôle la gestion de l'audio surround (7.1, TrueHD, DTS-HD)",
"auto": "Auto", "auto": "Auto",
"stereo": "Forcer la stéréo", "stereo": "Forcer la stéréo",
"5_1": "Autoriser 5.1", "5_1": "Autoriser 5.1",
"passthrough": "Passthrough" "passthrough": "Intercommunication"
} }
}, },
"subtitles": { "subtitles": {
@@ -252,7 +252,7 @@
"set_subtitle_track": "Piste de sous-titres de l'élément précédent", "set_subtitle_track": "Piste de sous-titres de l'élément précédent",
"subtitle_size": "Taille des sous-titres", "subtitle_size": "Taille des sous-titres",
"none": "Aucune", "none": "Aucune",
"language": "Langue", "language": "Langage",
"loading": "Chargement", "loading": "Chargement",
"modes": { "modes": {
"Default": "Par défaut", "Default": "Par défaut",
@@ -262,20 +262,20 @@
"OnlyForced": "Forcés seulement" "OnlyForced": "Forcés seulement"
}, },
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Entrez votre clé API OpenSubtitles pour activer la recherche de sous-titres côté client comme solution de secours lorsque votre serveur Jellyfin n'a pas de fournisseur de sous-titres configuré.", "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": "Clé API", "opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Entrez la clé API...", "opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Obtenez votre clé API gratuite sur opensubtitles.com/fr/consumers", "opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Échelle des sous-titres", "mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Marge verticale", "mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Alignement horizontal", "mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Alignement vertical", "mpv_subtitle_align_y": "Vertical Align",
"align": { "align": {
"left": "Gauche", "left": "Left",
"center": "Centre", "center": "Center",
"right": "Droite", "right": "Right",
"top": "Haut", "top": "Top",
"bottom": "Bas" "bottom": "Bottom"
} }
}, },
"other": { "other": {
@@ -283,38 +283,38 @@
"video_orientation": "Orientation vidéo", "video_orientation": "Orientation vidéo",
"orientation": "Orientation", "orientation": "Orientation",
"orientations": { "orientations": {
"DEFAULT": "Suivre l'orientation de l'appareil", "DEFAULT": "Par défaut",
"ALL": "Toutes", "ALL": "Toutes",
"PORTRAIT": "Portrait auto", "PORTRAIT": "Portrait",
"PORTRAIT_UP": "Portrait haut", "PORTRAIT_UP": "Portrait Haut",
"PORTRAIT_DOWN": "Portrait bas", "PORTRAIT_DOWN": "Portrait Bas",
"LANDSCAPE": "Paysage auto", "LANDSCAPE": "Paysage",
"LANDSCAPE_LEFT": "Paysage gauche", "LANDSCAPE_LEFT": "Paysage Gauche",
"LANDSCAPE_RIGHT": "Paysage droite", "LANDSCAPE_RIGHT": "Paysage Droite",
"OTHER": "Autre", "OTHER": "Autre",
"UNKNOWN": "Inconnu" "UNKNOWN": "Inconnu"
}, },
"safe_area_in_controls": "Zone de sécurité dans les contrôles", "safe_area_in_controls": "Zone de sécurité dans les contrôles",
"show_custom_menu_links": "Afficher les liens personnalisés", "show_custom_menu_links": "Afficher les liens personnalisés",
"show_large_home_carousel": "Afficher le grand carrousel daccueil (bêta)", "show_large_home_carousel": "Afficher le grand carrousel daccueil (bêta)",
"hide_libraries": "Masquer les bibliothèques", "hide_libraries": "Cacher des bibliothèques",
"select_libraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.", "select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.",
"disable_haptic_feedback": "Désactiver le retour haptique", "disable_haptic_feedback": "Désactiver le retour haptique",
"default_quality": "Qualité par défaut", "default_quality": "Qualité par défaut",
"default_playback_speed": "Vitesse de lecture par défaut", "default_playback_speed": "Vitesse de lecture par défaut",
"auto_play_next_episode": "Lecture automatique de l'épisode suivant", "auto_play_next_episode": "Lecture automatique de l'épisode suivant",
"max_auto_play_episode_count": "Nombre max d'épisodes en lecture automatique", "max_auto_play_episode_count": "Nombre d'épisodes en lecture automatique max",
"disabled": "Désactivé" "disabled": "Désactivé"
}, },
"music": { "music": {
"title": "Musique", "title": "Musique",
"playback_title": "Lecture", "playback_title": "Lecture",
"playback_description": "Configurer le mode de lecture de la musique.", "playback_description": "Configurer le mode de lecture de la musique.",
"prefer_downloaded": "Préférer les musiques téléchargées", "prefer_downloaded": "Supprimer toutes les musiques téléchargées",
"caching_title": "Mise en cache", "caching_title": "Mise en cache",
"caching_description": "Mettre automatiquement en cache les pistes à venir pour une lecture plus fluide.", "caching_description": "Mettre automatiquement en cache les pistes à venir pour une lecture plus fluide.",
"lookahead_enabled": "Activer la mise en cache anticipée", "lookahead_enabled": "Activer la mise en cache guidée",
"lookahead_count": "Pistes à mettre en cache à l'avance", "lookahead_count": "Pistes à pré-mettre en cache",
"max_cache_size": "Taille max de cache" "max_cache_size": "Taille max de cache"
}, },
"plugins": { "plugins": {
@@ -333,7 +333,7 @@
"tv_quota_days": "Jours de quota de séries", "tv_quota_days": "Jours de quota de séries",
"reset_jellyseerr_config_button": "Réinitialiser la configuration Seerr", "reset_jellyseerr_config_button": "Réinitialiser la configuration Seerr",
"unlimited": "Illimité", "unlimited": "Illimité",
"plus_n_more": "+{{n}} de plus", "plus_n_more": "+{{n}} Plus",
"order_by": { "order_by": {
"DEFAULT": "Par défaut", "DEFAULT": "Par défaut",
"VOTE_COUNT_AND_AVERAGE": "Nombre de votes et moyenne", "VOTE_COUNT_AND_AVERAGE": "Nombre de votes et moyenne",
@@ -341,7 +341,7 @@
} }
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Activer la recherche Marlin", "enable_marlin_search": "Activer Marlin Search",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://domaine.org:port", "server_url_placeholder": "http(s)://domaine.org:port",
"marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.", "marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.",
@@ -362,10 +362,10 @@
"features_title": "Fonctionnalités", "features_title": "Fonctionnalités",
"enable_movie_recommendations": "Recommandations de films", "enable_movie_recommendations": "Recommandations de films",
"enable_series_recommendations": "Recommandations de séries", "enable_series_recommendations": "Recommandations de séries",
"enable_promoted_watchlists": "Listes de suivi promues", "enable_promoted_watchlists": "Listes de lecture promues",
"hide_watchlists_tab": "Masquer l'onglet des listes de suivi", "hide_watchlists_tab": "Masquer l'onglet des listes de lecture",
"home_sections_hint": "Afficher des recommandations personnalisées et des listes de suivi promues de Streamystats sur la page daccueil.", "home_sections_hint": "Afficher des recommandations personnalisées et des listes de lecture promues de Streamystats sur la page daccueil.",
"recommended_movies": "Films recommandés", "recommended_movies": "Films Recommandés",
"recommended_series": "Séries recommandées", "recommended_series": "Séries recommandées",
"toasts": { "toasts": {
"saved": "Enregistré", "saved": "Enregistré",
@@ -375,7 +375,7 @@
"refresh_from_server": "Rafraîchir les paramètres depuis le serveur" "refresh_from_server": "Rafraîchir les paramètres depuis le serveur"
}, },
"kefinTweaks": { "kefinTweaks": {
"watchlist_enabler": "Activer l'intégration de la liste de suivi" "watchlist_enabler": "Activer l'intégration de notre liste de lecture"
} }
}, },
"storage": { "storage": {
@@ -384,8 +384,6 @@
"device_usage": "Appareil {{availableSpace}}%", "device_usage": "Appareil {{availableSpace}}%",
"size_used": "{{used}} de {{total}} utilisés", "size_used": "{{used}} de {{total}} utilisés",
"delete_all_downloaded_files": "Supprimer tous les fichiers téléchargés", "delete_all_downloaded_files": "Supprimer tous les fichiers téléchargés",
"delete_all_downloaded_files_confirm": "Supprimer tous les fichiers téléchargés ?",
"delete_all_downloaded_files_confirm_desc": "Êtes-vous sûr de vouloir supprimer tous les fichiers téléchargés ? Cette action est irréversible.",
"music_cache_title": "Mise en cache de la musique", "music_cache_title": "Mise en cache de la musique",
"music_cache_description": "Mettez automatiquement en cache les chansons au fur et à mesure que vous écoutez pour une lecture plus fluide et une prise en charge hors ligne", "music_cache_description": "Mettez automatiquement en cache les chansons au fur et à mesure que vous écoutez pour une lecture plus fluide et une prise en charge hors ligne",
"clear_music_cache": "Vider le cache de la musique", "clear_music_cache": "Vider le cache de la musique",
@@ -394,10 +392,10 @@
"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": "Effacer tout le cache", "clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Effacer tout le cache ?", "clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Êtes-vous sûr de vouloir effacer toutes les données en cache ? Cela supprimera toutes les images, fichiers musicaux, sous-titres et caches de requêtes mis en cache. Vos paramètres et votre session de connexion seront conservés.", "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_error_desc": "Une erreur est survenue lors de l'effacement du cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Introduction", "title": "Introduction",
@@ -421,17 +419,17 @@
"error_deleting_files": "Erreur lors de la suppression des fichiers" "error_deleting_files": "Erreur lors de la suppression des fichiers"
}, },
"security": { "security": {
"title": "Sécurité", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Délai d'inactivité", "title": "Inactivity Timeout",
"disabled": "Désactivé", "disabled": "Disabled",
"1_minute": "1 minute", "1_minute": "1 minute",
"5_minutes": "5 minutes", "5_minutes": "5 minutes",
"15_minutes": "15 minutes", "15_minutes": "15 minutes",
"30_minutes": "30 minutes", "30_minutes": "30 minutes",
"1_hour": "1 heure", "1_hour": "1 hour",
"4_hours": "4 heures", "4_hours": "4 hours",
"24_hours": "24 heures" "24_hours": "24 hours"
} }
} }
}, },
@@ -448,7 +446,7 @@
"delete_all_movies_button": "Supprimer tous les films", "delete_all_movies_button": "Supprimer tous les films",
"delete_all_series_button": "Supprimer toutes les séries", "delete_all_series_button": "Supprimer toutes les séries",
"delete_all_button": "Supprimer tous les médias", "delete_all_button": "Supprimer tous les médias",
"delete_all_other_media_button": "Supprimer les autres médias", "delete_all_other_media_button": "Supprimer un autre média",
"active_download": "Téléchargement actif", "active_download": "Téléchargement actif",
"no_active_downloads": "Pas de téléchargements actifs", "no_active_downloads": "Pas de téléchargements actifs",
"active_downloads": "Téléchargements actifs", "active_downloads": "Téléchargements actifs",
@@ -456,7 +454,7 @@
"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": "Supprimer le téléchargement", "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}}",
@@ -467,7 +465,7 @@
"deleted_all_series_successfully": "Toutes les séries ont été supprimées avec succès!", "deleted_all_series_successfully": "Toutes les séries ont été supprimées avec succès!",
"failed_to_delete_all_series": "Échec de la suppression de toutes les séries", "failed_to_delete_all_series": "Échec de la suppression de toutes les séries",
"deleted_media_successfully": "Les autres médias ont été supprimés avec succès !", "deleted_media_successfully": "Les autres médias ont été supprimés avec succès !",
"failed_to_delete_media": "Échec de la suppression des autres médias", "failed_to_delete_media": "Échec de la suppression d'un autre média",
"download_cancelled": "Téléchargement annulé", "download_cancelled": "Téléchargement annulé",
"could_not_delete_download": "Impossible de supprimer le téléchargement", "could_not_delete_download": "Impossible de supprimer le téléchargement",
"download_completed": "Téléchargement terminé", "download_completed": "Téléchargement terminé",
@@ -485,16 +483,17 @@
} }
}, },
"common": { "common": {
"no_results": "Aucun résultat", "no_results": "No Results",
"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": "Marquer comme vu", "mark_as_played": "Mark as Played",
"mark_as_not_played": "Marquer comme non vu", "mark_as_not_played": "Mark as not Played",
"none": "Aucun", "none": "Aucun",
"track": "Piste", "track": "Suivre",
"cancel": "Annuler", "cancel": "Annuler",
"delete": "Supprimer", "delete": "Supprimer",
"ok": "Ok", "ok": "Ok",
@@ -502,15 +501,15 @@
"back": "Précédent", "back": "Précédent",
"continue": "Continuer", "continue": "Continuer",
"verifying": "Vérification...", "verifying": "Vérification...",
"login": "Connexion", "login": "Login",
"episodes": "Épisodes", "episodes": "Episodes",
"movies": "Films", "movies": "Movies",
"loading": "Chargement…", "loading": "Loading…",
"seeAll": "Tout afficher" "seeAll": "See all"
}, },
"search": { "search": {
"search": "Rechercher...", "search": "Rechercher...",
"x_items": "{{count}} médias", "x_items": "{{count}} Médias",
"library": "Bibliothèque", "library": "Bibliothèque",
"discover": "Découvrir", "discover": "Découvrir",
"no_results": "Aucun résultat", "no_results": "Aucun résultat",
@@ -528,7 +527,7 @@
"request_series": "Demander une série", "request_series": "Demander une série",
"recently_added": "Ajoutés récemment", "recently_added": "Ajoutés récemment",
"recent_requests": "Demandes récentes", "recent_requests": "Demandes récentes",
"plex_watchlist": "Liste de suivi Plex", "plex_watchlist": "Liste de lecture Plex",
"trending": "Tendance", "trending": "Tendance",
"popular_movies": "Films populaires", "popular_movies": "Films populaires",
"movie_genres": "Genres de films", "movie_genres": "Genres de films",
@@ -537,7 +536,7 @@
"popular_tv": "Séries populaires", "popular_tv": "Séries populaires",
"tv_genres": "Genres des séries", "tv_genres": "Genres des séries",
"upcoming_tv": "Séries à venir", "upcoming_tv": "Séries à venir",
"networks": "Chaînes", "networks": "Studios",
"tmdb_movie_keyword": "Mots-clés de film TMDB", "tmdb_movie_keyword": "Mots-clés de film TMDB",
"tmdb_movie_genre": "Genre de film TMDB", "tmdb_movie_genre": "Genre de film TMDB",
"tmdb_tv_keyword": "Mots-clés de séries TMDB", "tmdb_tv_keyword": "Mots-clés de séries TMDB",
@@ -554,7 +553,7 @@
"item_types": { "item_types": {
"movies": "Films", "movies": "Films",
"series": "Séries", "series": "Séries",
"boxsets": "Coffrets", "boxsets": "Coffrets ",
"playlists": "Playlists", "playlists": "Playlists",
"items": "Médias" "items": "Médias"
}, },
@@ -576,10 +575,10 @@
"filter_by": "Filtrer par", "filter_by": "Filtrer par",
"sort_order": "Ordre de tri", "sort_order": "Ordre de tri",
"tags": "Tags", "tags": "Tags",
"all": "Tout", "all": "All",
"reset": "Réinitialiser", "reset": "Reset",
"asc": "Croissant", "asc": "Ascending",
"desc": "Décroissant" "desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -593,14 +592,14 @@
"noData": "Marquez des éléments comme favoris pour les voir apparaître ici pour un accès rapide." "noData": "Marquez des éléments comme favoris pour les voir apparaître ici pour un accès rapide."
}, },
"custom_links": { "custom_links": {
"no_links": "Aucun lien" "no_links": "Aucuns liens"
}, },
"player": { "player": {
"live": "EN DIRECT", "live": "LIVE",
"mpv_player_title": "Lecteur MPV", "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_occurred_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.",
"client_error": "Erreur du client", "client_error": "Erreur du client",
"could_not_create_stream_for_chromecast": "Impossible de créer un flux sur la Chromecast", "could_not_create_stream_for_chromecast": "Impossible de créer un flux sur la Chromecast",
"message_from_server": "Message du serveur : {{message}}", "message_from_server": "Message du serveur : {{message}}",
@@ -612,71 +611,72 @@
"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": "Balayez vers le bas pour les paramètres", "swipe_down_settings": "Swipe down for settings",
"ends_at": "Se termine à {{time}}", "ends_at": "Ends at {{time}}",
"search_subtitles": "Rechercher des sous-titres", "search_subtitles": "Search Subtitles",
"subtitle_tracks": "Pistes", "subtitle_tracks": "Tracks",
"subtitle_search": "Rechercher et télécharger", "subtitle_search": "Search & Download",
"download": "Télécharger", "download": "Download",
"subtitle_download_hint": "Les sous-titres téléchargés seront enregistrés dans votre bibliothèque", "subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Utilisation du serveur Jellyfin", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Langue", "language": "Language",
"results": "Résultats", "results": "Results",
"search_failed": "Recherche échouée", "searching": "Searching...",
"no_subtitle_provider": "Aucun fournisseur de sous-titres configuré sur le serveur", "search_failed": "Search failed",
"no_subtitles_found": "Aucun sous-titre trouvé", "no_subtitle_provider": "No subtitle provider configured on server",
"add_opensubtitles_key_hint": "Ajoutez une clé API OpenSubtitles dans les paramètres pour une solution de secours côté client", "no_subtitles_found": "No subtitles found",
"settings": "Paramètres", "add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"skip_intro": "Passer l'intro", "settings": "Settings",
"skip_credits": "Passer le générique", "skip_intro": "Skip Intro",
"stopPlayback": "Arrêter la lecture", "skip_credits": "Skip Credits",
"stopPlayingTitle": "Arrêter de lire \"{{title}}\" ?", "stopPlayback": "Stop Playback",
"stopPlayingConfirm": "Êtes-vous sûr de vouloir arrêter la lecture ?", "stopPlayingTitle": "Stop playing \"{{title}}\"?",
"downloaded": "Téléchargé", "stopPlayingConfirm": "Are you sure you want to stop playback?",
"missing_parameters": "Paramètres de lecture manquants" "downloaded": "Downloaded",
"missing_parameters": "Missing playback parameters"
}, },
"chapters": { "chapters": {
"title": "Chapitres", "title": "Chapters",
"chapter_number": "Chapitre {{number}}", "chapter_number": "Chapter {{number}}",
"open": "Ouvrir les chapitres", "open": "Open chapters",
"close": "Fermer les chapitres" "close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "À suivre", "next_up": "À suivre",
"no_items_to_display": "Aucun média à afficher", "no_items_to_display": "Aucuns médias à afficher",
"cast_and_crew": "Distribution et équipe", "cast_and_crew": "Distribution et équipe",
"series": "Séries", "series": "Séries",
"seasons": "Saisons", "seasons": "Saisons",
"season": "Saison", "season": "Saison",
"from_this_series": "De cette série", "from_this_series": "From This Series",
"more_from_this_season": "Plus de cette saison", "more_from_this_season": "More from this Season",
"view_series": "Voir la série", "view_series": "View Series",
"view_season": "Voir la saison", "view_season": "View Season",
"select_season": "Sélectionner une saison", "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}}",
"similar_items": "Médias similaires", "similar_items": "Médias similaires",
"no_similar_items_found": "Aucun média similaire trouvé", "no_similar_items_found": "Aucuns médias similaires trouvés",
"video": "Vidéo", "video": "Vidéo",
"more_details": "Plus de détails", "more_details": "Plus de détails",
"media_options": "Options média", "media_options": "Options média",
"quality": "Qualité", "quality": "Qualité",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": {
"label": "Sous-titres", "label": "Subtitle",
"none": "Aucun", "none": "None",
"tracks": "Pistes" "tracks": "Tracks"
}, },
"show_more": "Afficher plus", "show_more": "Afficher plus",
"show_less": "Afficher moins", "show_less": "Afficher moins",
"left": "restant", "left": "left",
"director": "Réalisateur", "director": "Director",
"cast": "Distribution", "cast": "Cast",
"technical_details": "Détails techniques", "technical_details": "Technical Details",
"appeared_in": "Apparu dans", "appeared_in": "Apparu dans",
"movies": "Films", "movies": "Movies",
"shows": "Séries", "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": {
@@ -688,13 +688,13 @@
"download_unwatched_only": "Non visionné uniquement", "download_unwatched_only": "Non visionné uniquement",
"download_button": "Télécharger" "download_button": "Télécharger"
}, },
"mark_played": "Marquer comme vu", "mark_played": "Mark as Watched",
"mark_unplayed": "Marquer comme non vu", "mark_unplayed": "Mark as Unwatched",
"resume_playback": "Reprendre la lecture", "resume_playback": "Resume Playback",
"resume_playback_description": "Voulez-vous continuer où vous vous êtes arrêté ou commencer à partir du début ?", "resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Lire depuis le début", "play_from_start": "Play from Start",
"continue_from": "Continuer depuis {{time}}", "continue_from": "Continue from {{time}}",
"no_data_available": "Aucune donnée disponible" "no_data_available": "No data available"
}, },
"live_tv": { "live_tv": {
"next": "Suivant", "next": "Suivant",
@@ -706,16 +706,16 @@
"sports": "Sports", "sports": "Sports",
"for_kids": "Pour enfants", "for_kids": "Pour enfants",
"news": "Actualités", "news": "Actualités",
"page_of": "Page {{current}} sur {{total}}", "page_of": "Page {{current}} of {{total}}",
"no_programs": "Aucun programme disponible", "no_programs": "No programs available",
"no_channels": "Aucune chaîne disponible", "no_channels": "No channels available",
"tabs": { "tabs": {
"programs": "Programmes", "programs": "Programs",
"guide": "Guide", "guide": "Guide",
"channels": "Chaînes", "channels": "Channels",
"recordings": "Enregistrements", "recordings": "Recordings",
"schedule": "Programmation", "schedule": "Schedule",
"series": "Séries" "series": "Series"
} }
}, },
"jellyseerr": { "jellyseerr": {
@@ -725,7 +725,7 @@
"whats_wrong": "Quel est le problème?", "whats_wrong": "Quel est le problème?",
"issue_type": "Type de problème", "issue_type": "Type de problème",
"select_an_issue": "Sélectionnez un problème", "select_an_issue": "Sélectionnez un problème",
"types": "Types", "types": "Types de fichiers",
"describe_the_issue": "(optionnel) Décrivez le problème...", "describe_the_issue": "(optionnel) Décrivez le problème...",
"submit_button": "Soumettre", "submit_button": "Soumettre",
"report_issue_button": "Signaler un problème", "report_issue_button": "Signaler un problème",
@@ -734,7 +734,7 @@
"failed_to_login": "Échec de la connexion", "failed_to_login": "Échec de la connexion",
"cast": "Distribution", "cast": "Distribution",
"details": "Détails", "details": "Détails",
"status": "Statut", "status": "Statuts",
"original_title": "Titre original", "original_title": "Titre original",
"series_type": "Type de série", "series_type": "Type de série",
"release_dates": "Dates de sortie", "release_dates": "Dates de sortie",
@@ -752,32 +752,32 @@
"tags": "Tags", "tags": "Tags",
"quality_profile": "Profil de qualité", "quality_profile": "Profil de qualité",
"root_folder": "Dossier racine", "root_folder": "Dossier racine",
"season_all": "Saison (Toutes)", "season_all": "Saison (Tous)",
"season_number": "Saison {{season_number}}", "season_number": "Saison {{season_number}}",
"number_episodes": "{{episode_number}} épisodes", "number_episodes": "{{episode_number}} Épisodes",
"born": "Né(e) le", "born": "Né(e) le",
"appearances": "Apparitions", "appearances": "Apparences",
"approve": "Valider", "approve": "Valider",
"decline": "Refuser", "decline": "Refuser",
"requested_by": "Demandé par {{user}}", "requested_by": "Demandé par {{user}}",
"unknown_user": "Utilisateur inconnu", "unknown_user": "Utilisateur inconnu",
"select": "Sélectionner", "select": "Select",
"request_all": "Tout demander", "request_all": "Request All",
"request_seasons": "Demander des saisons", "request_seasons": "Request Seasons",
"select_seasons": "Sélectionner les saisons", "select_seasons": "Select Seasons",
"request_selected": "Demander la sélection", "request_selected": "Request Selected",
"n_selected": "{{count}} sélectionnés", "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.",
"failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Seerr", "failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Seerr",
"issue_submitted": "Problème soumis!", "issue_submitted": "Problème soumis!",
"requested_item": "{{item}} demandé !", "requested_item": "{{item}}} demandé!",
"you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander !", "you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander !",
"something_went_wrong_requesting_media": "Une erreur s'est produite lors de la demande du média !", "something_went_wrong_requesting_media": "Quelque chose s'est mal passé en demandant le média !",
"request_approved": "Demande approuvée !", "request_approved": "Demande approuvée !",
"request_declined": "Demande refusée !", "request_declined": "Demande déclinée !",
"failed_to_approve_request": "Échec de l'approbation de la demande", "failed_to_approve_request": "Échec d'approbation de la demande",
"failed_to_decline_request": "Échec du refus de la demande" "failed_to_decline_request": "Échec du refus de la demande"
} }
}, },
@@ -787,7 +787,7 @@
"library": "Bibliothèque", "library": "Bibliothèque",
"custom_links": "Liens personnalisés", "custom_links": "Liens personnalisés",
"favorites": "Favoris", "favorites": "Favoris",
"settings": "Réglages" "settings": "Settings"
}, },
"music": { "music": {
"title": "Musique", "title": "Musique",
@@ -807,13 +807,13 @@
"play_top_tracks": "Jouer les pistes les plus populaires", "play_top_tracks": "Jouer les pistes les plus populaires",
"no_suggestions": "Pas de suggestion disponible", "no_suggestions": "Pas de suggestion disponible",
"no_albums": "Pas d'albums trouvés", "no_albums": "Pas d'albums trouvés",
"no_artists": "Pas d'artistes trouvés", "no_artists": "Pas d'artistes trouvé",
"no_playlists": "Pas de playlists trouvées", "no_playlists": "Pas de playlists trouvées",
"album_not_found": "Album introuvable", "album_not_found": "Album introuvable",
"artist_not_found": "Artiste introuvable", "artist_not_found": "Artiste introuvable",
"playlist_not_found": "Playlist introuvable", "playlist_not_found": "Playlist introuvable",
"track_options": { "track_options": {
"play_next": "Lire ensuite", "play_next": "Lecture suivante",
"add_to_queue": "Ajouter à la file d'attente", "add_to_queue": "Ajouter à la file d'attente",
"add_to_playlist": "Ajouter à la playlist", "add_to_playlist": "Ajouter à la playlist",
"download": "Télécharger", "download": "Télécharger",
@@ -822,15 +822,15 @@
"cached": "En cache", "cached": "En cache",
"delete_download": "Supprimer un téléchargement", "delete_download": "Supprimer un téléchargement",
"delete_cache": "Supprimer du cache", "delete_cache": "Supprimer du cache",
"go_to_artist": "Aller à l'artiste", "go_to_artist": "Voir l'artiste",
"go_to_album": "Aller à l'album", "go_to_album": "Aller à lalbum",
"add_to_favorites": "Ajouter aux favoris", "add_to_favorites": "Ajouter aux favoris",
"remove_from_favorites": "Retirer des favoris", "remove_from_favorites": "Retirer des favoris",
"remove_from_playlist": "Retirer de la playlist" "remove_from_playlist": "Retirer de la playlist"
}, },
"playlists": { "playlists": {
"create_playlist": "Créer une playlist", "create_playlist": "Créer une Playlist",
"playlist_name": "Nom de la playlist", "playlist_name": "Nom de la Playlist",
"enter_name": "Entrer le nom de la playlist", "enter_name": "Entrer le nom de la playlist",
"create": "Créer", "create": "Créer",
"search_playlists": "Rechercher des playlists...", "search_playlists": "Rechercher des playlists...",
@@ -841,10 +841,10 @@
"created": "Playlist créée", "created": "Playlist créée",
"create_new": "Créer une nouvelle playlist", "create_new": "Créer une nouvelle playlist",
"failed_to_add": "Échec de l'ajout à la playlist", "failed_to_add": "Échec de l'ajout à la playlist",
"failed_to_remove": "Échec du retrait de la playlist", "failed_to_remove": "Échec de la suppression de la playlist",
"failed_to_create": "Échec de la création de la playlist", "failed_to_create": "Échec de la suppression de la playlist",
"delete_playlist": "Supprimer la playlist", "delete_playlist": "Supprimer la playlist",
"delete_confirm": "Êtes-vous sûr de vouloir supprimer « {{name}} » ? Cette action est irréversible.", "delete_confirm": "Êtes-vous sûr de vouloir supprimer « {{ name }} » ? Cette action est irréversible.",
"deleted": "Playlist supprimée", "deleted": "Playlist supprimée",
"failed_to_delete": "Échec de la suppression de la playlist" "failed_to_delete": "Échec de la suppression de la playlist"
}, },
@@ -855,46 +855,46 @@
} }
}, },
"watchlists": { "watchlists": {
"title": "Listes de suivi", "title": "Listes de lecture",
"my_watchlists": "Mes listes de suivi", "my_watchlists": "Mes listes de lecture",
"public_watchlists": "Listes de suivi publiques", "public_watchlists": "Watchlist publique",
"create_title": "Créer une liste de suivi", "create_title": "Créer une Watchlist",
"edit_title": "Modifier la liste de suivi", "edit_title": "Modifier la Watchlist",
"create_button": "Créer une liste de suivi", "create_button": "Créer une Watchlist",
"save_button": "Enregistrer les modifications", "save_button": "Enregistrer les modifications",
"delete_button": "Supprimer", "delete_button": "Supprimer",
"remove_button": "Retirer", "remove_button": "Retirer",
"cancel_button": "Annuler", "cancel_button": "Annuler",
"name_label": "Nom", "name_label": "Nom",
"name_placeholder": "Entrez le nom de la liste de suivi", "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 suivi publique", "is_public_label": "Liste de lecture Publique",
"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",
"empty_title": "Aucune liste de suivi", "empty_title": "Pas de Watchlists",
"empty_description": "Créez votre première liste de suivi pour commencer à organiser vos médias", "empty_description": "Créez votre première liste de suivi pour commencer à organiser vos médias",
"empty_watchlist": "Cette liste de suivi est vide", "empty_watchlist": "Cette liste de suivi est vide",
"empty_watchlist_hint": "Ajouter des éléments de votre bibliothèque à cette liste de suivi", "empty_watchlist_hint": "Ajouter des éléments de votre bibliothèque à cette liste de suivi",
"not_configured_title": "Streamystats non configuré", "not_configured_title": "Streamystats non configuré",
"not_configured_description": "Configurer Streamystats dans les paramètres pour utiliser les listes de suivi", "not_configured_description": "Configurer Streamystats dans les paramètres pour utiliser les listes de suivi",
"go_to_settings": "Accédez aux paramètres", "go_to_settings": "Accédez aux Paramètres",
"add_to_watchlist": "Ajouter à la liste de suivi", "add_to_watchlist": "Ajouter à la Watchlist",
"remove_from_watchlist": "Retirer de la liste de suivi", "remove_from_watchlist": "Retirer de la Watchlist",
"select_watchlist": "Sélectionner la liste de suivi", "select_watchlist": "Sélectionner la liste de suivi",
"create_new": "Créer une nouvelle liste de suivi", "create_new": "Créer une Watchlist",
"item": "élément", "item": "médias",
"items": "éléments", "items": "élément",
"public": "Publique", "public": "Publique",
"private": "Privée", "private": "Privée",
"you": "Vous", "you": "Vous-même",
"by_owner": "Par un autre utilisateur", "by_owner": "Par un autre utilisateur",
"not_found": "Liste de suivi introuvable", "not_found": "Playlist introuvable",
"delete_confirm_title": "Supprimer la liste de suivi", "delete_confirm_title": "Supprimer la Watchlist",
"delete_confirm_message": "Voulez-vous vraiment supprimer « {{name}} » ? Cette action est irréversible.", "delete_confirm_message": "Tous les médias (par défaut)",
"remove_item_title": "Retirer de la liste de suivi", "remove_item_title": "Retirer de la Watchlist",
"remove_item_message": "Retirer « {{name}} » de cette liste de suivi ?", "remove_item_message": "Retirer «{{name}}» de cette liste de suivi?",
"loading": "Chargement des listes de suivi...", "loading": "Chargement des listes de suivi...",
"no_compatible_watchlists": "Aucune liste de suivi compatible", "no_compatible_watchlists": "Aucune liste de suivi compatible",
"create_one_first": "Créer une liste de suivi qui accepte ce type de contenu" "create_one_first": "Créer une liste de suivi qui accepte ce type de contenu"
@@ -910,33 +910,33 @@
} }
}, },
"companion_login": { "companion_login": {
"title": "Associer à la TV", "title": "Pair with TV",
"align_qr": "Alignez le code QR dans le cadre", "align_qr": "Align the QR code within the frame",
"enter_code_manually": "Saisir le code manuellement", "enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Entrez les identifiants pour le téléviseur", "pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Code d'appairage", "pairing_code_label": "Pairing code",
"server": "Serveur", "server": "Server",
"authorize_button": "Autoriser", "authorize_button": "Authorize",
"authorizing": "Autorisation en cours...", "authorizing": "Authorizing...",
"scan_again": "Scanner à nouveau", "scan_again": "Scan Again",
"done": "Terminé", "done": "Done",
"success_title": "Autorisation envoyée", "success_title": "Authorization Sent",
"pairing_tv_connecting": "La TV se connecte à votre compte", "pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Échec de l'autorisation", "error_title": "Authorization Failed",
"error_invalid_qr": "Code QR invalide. Veuillez scanner le code d'appairage du téléviseur.", "error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Une erreur s'est produite. Veuillez réessayer.", "error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "L'accès à la caméra est nécessaire pour scanner les QR codes.", "error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Se connecter en tant que {{username}} ?", "login_as": "Log in as {{username}}?",
"on_server": "sur {{server}}", "on_server": "on {{server}}",
"use_different_user": "Utiliser un autre utilisateur", "use_different_user": "Use a different user",
"open_settings": "Ouvrir les paramètres" "open_settings": "Open Settings"
}, },
"pairing": { "pairing": {
"pair_with_phone": "Associer au téléphone", "pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Se connecter sur la TV", "pair_with_phone_title": "Login TV",
"waiting_for_phone": "En attente du téléphone...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scanner avec l'application Streamyfin sur votre téléphone", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Connexion en cours...", "logging_in": "Logging in...",
"logging_in_description": "Connexion à votre serveur" "logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -16,14 +16,14 @@
"got_it": "קיבלתי", "got_it": "קיבלתי",
"connection_failed": "ההתחברות נכשלה", "connection_failed": "ההתחברות נכשלה",
"could_not_connect_to_server": "לא היה ניתן להתחבר לשרת. אנא בדוק את הקישור ואת חיבור הרשת שלך.", "could_not_connect_to_server": "לא היה ניתן להתחבר לשרת. אנא בדוק את הקישור ואת חיבור הרשת שלך.",
"an_unexpected_error_occurred": "קרתה שגיאה לא צפויה", "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_occurred_did_you_enter_the_correct_url": "קרתה שגיאה לא צפויה. האם כתובת השרת שהוקלדה נכונה?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "קרתה שגיאה לא צפויה. האם כתובת השרת שהוקלדה נכונה?",
"too_old_server_text": "נמצא שרת Jellyfin שלא נתמך", "too_old_server_text": "נמצא שרת Jellyfin שלא נתמך",
"too_old_server_description": "אנא עדכן את גרסת ה-Jellyfin למעודכנת ביותר" "too_old_server_description": "אנא עדכן את גרסת ה-Jellyfin למעודכנת ביותר"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "אשר התחברות מהירה", "authorize_button": "אשר התחברות מהירה",
"enter_the_quick_connect_code": "הקלד את קוד ההתחברות המהירה...", "enter_the_quick_connect_code": "הקלד את קוד ההתחברות המהירה...",
"success": "הצלחה", "success": "הצלחה",
"quick_connect_authorized": "חיבור מהיר אושר", "quick_connect_autorized": "חיבור מהיר אושר",
"error": "שגיאה", "error": "שגיאה",
"invalid_code": "קוד שגוי", "invalid_code": "קוד שגוי",
"authorize": "אשר" "authorize": "אשר"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "הצג קישורים לתפריטים מותאמים אישית", "show_custom_menu_links": "הצג קישורים לתפריטים מותאמים אישית",
"show_large_home_carousel": "הצג קרוסלה גדולה במסך הבית (בטא)", "show_large_home_carousel": "הצג קרוסלה גדולה במסך הבית (בטא)",
"hide_libraries": "הסתר ספריות", "hide_libraries": "הסתר ספריות",
"select_libraries_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": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "מכשיר {{availableSpace}}%", "device_usage": "מכשיר {{availableSpace}}%",
"size_used": "השתמשת ב-{{used}} מתוך {{total}}", "size_used": "השתמשת ב-{{used}} מתוך {{total}}",
"delete_all_downloaded_files": "מחק את כל הקבצים שהורדו", "delete_all_downloaded_files": "מחק את כל הקבצים שהורדו",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "בחר",
"no_trailer_available": "אין טריילר זמין", "no_trailer_available": "אין טריילר זמין",
"video": "וידאו", "video": "וידאו",
"audio": "שמע", "audio": "שמע",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "mpv_player_title": "MPV Player",
"error": "שגיאה", "error": "שגיאה",
"failed_to_get_stream_url": "נכשל בהשגת קישור הזרם", "failed_to_get_stream_url": "נכשל בהשגת קישור הזרם",
"an_error_occurred_while_playing_the_video": "קרתה תקלה במהלך הניגון של הקובץ. בדוק את הלוגים בהגדרות.", "an_error_occured_while_playing_the_video": "קרתה תקלה במהלך הניגון של הקובץ. בדוק את הלוגים בהגדרות.",
"client_error": "שגיאת לקוח", "client_error": "שגיאת לקוח",
"could_not_create_stream_for_chromecast": "נכשל ביצירת זרם עבור Chromecast", "could_not_create_stream_for_chromecast": "נכשל ביצירת זרם עבור Chromecast",
"message_from_server": "הודעה מהשרת: {{message}}", "message_from_server": "הודעה מהשרת: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Értettem", "got_it": "Értettem",
"connection_failed": "Kapcsolódás Sikertelen", "connection_failed": "Kapcsolódás Sikertelen",
"could_not_connect_to_server": "Nem sikerült csatlakozni a szerverhez. Kérjük, ellenőrizd az URL-t és a hálózati kapcsolatot.", "could_not_connect_to_server": "Nem sikerült csatlakozni a szerverhez. Kérjük, ellenőrizd az URL-t és a hálózati kapcsolatot.",
"an_unexpected_error_occurred": "Váratlan Hiba Történt", "an_unexpected_error_occured": "Váratlan Hiba Történt",
"change_server": "Szerverváltás", "change_server": "Szerverváltás",
"invalid_username_or_password": "Érvénytelen Felhasználónév vagy Jelszó", "invalid_username_or_password": "Érvénytelen Felhasználónév vagy Jelszó",
"user_does_not_have_permission_to_log_in": "A felhasználónak nincs jogosultsága a bejelentkezéshez", "user_does_not_have_permission_to_log_in": "A felhasználónak nincs jogosultsága a bejelentkezéshez",
"server_is_taking_too_long_to_respond_try_again_later": "A szerver túl sokáig válaszol, próbáld újra később", "server_is_taking_too_long_to_respond_try_again_later": "A szerver túl sokáig válaszol, próbáld újra később",
"server_received_too_many_requests_try_again_later": "A szerver túl sok kérést kapott, próbáld újra később.", "server_received_too_many_requests_try_again_later": "A szerver túl sok kérést kapott, próbáld újra később.",
"there_is_a_server_error": "Szerverhiba történt", "there_is_a_server_error": "Szerverhiba történt",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Váratlan hiba történt. Helyesen adtad meg a szerver URL-jét?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Váratlan hiba történt. Helyesen adtad meg a szerver URL-jét?",
"too_old_server_text": "Nem Támogatott Jellyfin-szerver", "too_old_server_text": "Nem Támogatott Jellyfin-szerver",
"too_old_server_description": "Frissítsd a Jellyfint a legújabb verzióra" "too_old_server_description": "Frissítsd a Jellyfint a legújabb verzióra"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Gyorscsatlakozás Engedélyezése", "authorize_button": "Gyorscsatlakozás Engedélyezése",
"enter_the_quick_connect_code": "Add meg a gyors csatlakozási kódot...", "enter_the_quick_connect_code": "Add meg a gyors csatlakozási kódot...",
"success": "Siker", "success": "Siker",
"quick_connect_authorized": "Gyorscsatlakozás Engedélyezve", "quick_connect_autorized": "Gyorscsatlakozás Engedélyezve",
"error": "Hiba", "error": "Hiba",
"invalid_code": "Érvénytelen Kód", "invalid_code": "Érvénytelen Kód",
"authorize": "Engedélyezés" "authorize": "Engedélyezés"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Egyéni Menülinkek Megjelenítése", "show_custom_menu_links": "Egyéni Menülinkek Megjelenítése",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Könyvtárak Elrejtése", "hide_libraries": "Könyvtárak Elrejtése",
"select_libraries_you_want_to_hide": "Válaszd ki azokat a könyvtárakat, amelyeket el szeretnél rejteni a Könyvtár fülön és a kezdőlapon.", "select_liraries_you_want_to_hide": "Válaszd ki azokat a könyvtárakat, amelyeket el szeretnél rejteni a Könyvtár fülön és a kezdőlapon.",
"disable_haptic_feedback": "Haptikus Visszajelzés Letiltása", "disable_haptic_feedback": "Haptikus Visszajelzés Letiltása",
"default_quality": "Alapértelmezett Minőség", "default_quality": "Alapértelmezett Minőség",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Eszköz {{availableSpace}}%", "device_usage": "Eszköz {{availableSpace}}%",
"size_used": "{{used}} / {{total}} Használatban", "size_used": "{{used}} / {{total}} Használatban",
"delete_all_downloaded_files": "Minden Letöltött Fájl Törlése", "delete_all_downloaded_files": "Minden Letöltött Fájl Törlése",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Videó", "video": "Videó",
"audio": "Hang", "audio": "Hang",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Kliens Hiba", "client_error": "Kliens Hiba",
"could_not_create_stream_for_chromecast": "A Chromecast stream létrehozása sikertelen volt", "could_not_create_stream_for_chromecast": "A Chromecast stream létrehozása sikertelen volt",
"message_from_server": "Üzenet a szervertől: {{message}}", "message_from_server": "Üzenet a szervertől: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Capito", "got_it": "Capito",
"connection_failed": "Connessione fallita", "connection_failed": "Connessione fallita",
"could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.", "could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.",
"an_unexpected_error_occurred": "Si è verificato un errore inaspettato", "an_unexpected_error_occured": "Si è verificato un errore inaspettato",
"change_server": "Cambiare il server", "change_server": "Cambiare il server",
"invalid_username_or_password": "Nome utente o password non validi", "invalid_username_or_password": "Nome utente o password non validi",
"user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere", "user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere",
"server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi", "server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi",
"server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.", "server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.",
"there_is_a_server_error": "Si è verificato un errore del server", "there_is_a_server_error": "Si è verificato un errore del server",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?",
"too_old_server_text": "Scoperto Server Jellyfin non supportato", "too_old_server_text": "Scoperto Server Jellyfin non supportato",
"too_old_server_description": "Aggiorna Jellyfin all'ultima versione" "too_old_server_description": "Aggiorna Jellyfin all'ultima versione"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Autorizza Connessione Rapida", "authorize_button": "Autorizza Connessione Rapida",
"enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...", "enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...",
"success": "Successo", "success": "Successo",
"quick_connect_authorized": "Connessione Rapida autorizzata", "quick_connect_autorized": "Connessione Rapida autorizzata",
"error": "Errore", "error": "Errore",
"invalid_code": "Codice invalido", "invalid_code": "Codice invalido",
"authorize": "Autorizza" "authorize": "Autorizza"
@@ -226,7 +226,7 @@
"hide_volume_slider": "Hide Volume Slider", "hide_volume_slider": "Hide Volume Slider",
"hide_volume_slider_description": "Nascondi il cursore del volume nel lettore video", "hide_volume_slider_description": "Nascondi il cursore del volume nel lettore video",
"hide_brightness_slider": "Hide Brightness Slider", "hide_brightness_slider": "Hide Brightness Slider",
"hide_brightness_slider_description": "Nascondi il cursore della luminosità nel lettore video" "hide_brightness_slider_description": "Hide the brightness slider in the video player"
}, },
"audio": { "audio": {
"audio_title": "Audio", "audio_title": "Audio",
@@ -237,10 +237,10 @@
"language": "Lingua", "language": "Lingua",
"transcode_mode": { "transcode_mode": {
"title": "Audio Transcoding", "title": "Audio Transcoding",
"description": "Controlla come viene gestito l'audio surround (7.1, TrueHD, DTS-HD)", "description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
"auto": "Automatico", "auto": "Auto",
"stereo": "Force Stereo", "stereo": "Force Stereo",
"5_1": "Consenti 5.1", "5_1": "Allow 5.1",
"passthrough": "Passthrough" "passthrough": "Passthrough"
} }
}, },
@@ -262,20 +262,20 @@
"OnlyForced": "Solo forzati" "OnlyForced": "Solo forzati"
}, },
"opensubtitles_title": "OpenSubtitles", "opensubtitles_title": "OpenSubtitles",
"opensubtitles_hint": "Inserisci la tua chiave API OpenSubtitles per abilitare la ricerca dei sottotitoli quando il tuo server Jellyfin non ha un provider di sottotitoli configurato.", "opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
"opensubtitles_api_key": "API Key", "opensubtitles_api_key": "API Key",
"opensubtitles_api_key_placeholder": "Inserisci la chiave API...", "opensubtitles_api_key_placeholder": "Enter API key...",
"opensubtitles_get_key": "Ottieni la tua chiave API gratuita su opensubtitles.com/en/consumers", "opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
"mpv_subtitle_scale": "Subtitle Scale", "mpv_subtitle_scale": "Subtitle Scale",
"mpv_subtitle_margin_y": "Vertical Margin", "mpv_subtitle_margin_y": "Vertical Margin",
"mpv_subtitle_align_x": "Horizontal Align", "mpv_subtitle_align_x": "Horizontal Align",
"mpv_subtitle_align_y": "Vertical Align", "mpv_subtitle_align_y": "Vertical Align",
"align": { "align": {
"left": "Sinistra", "left": "Left",
"center": "Centro", "center": "Center",
"right": "Destra", "right": "Right",
"top": "Alto", "top": "Top",
"bottom": "Basso" "bottom": "Bottom"
} }
}, },
"other": { "other": {
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Mostra i link del menu personalizzato", "show_custom_menu_links": "Mostra i link del menu personalizzato",
"show_large_home_carousel": "Mostra Carosello Grande nella Home (beta)", "show_large_home_carousel": "Mostra Carosello Grande nella Home (beta)",
"hide_libraries": "Nascondi Librerie", "hide_libraries": "Nascondi Librerie",
"select_libraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.", "select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
"disable_haptic_feedback": "Disabilita il feedback aptico", "disable_haptic_feedback": "Disabilita il feedback aptico",
"default_quality": "Qualità predefinita", "default_quality": "Qualità predefinita",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -307,9 +307,9 @@
"disabled": "Disabilitato" "disabled": "Disabilitato"
}, },
"music": { "music": {
"title": "Musica", "title": "Music",
"playback_title": "Riproduzione", "playback_title": "Playback",
"playback_description": "Configura come viene riprodotta la musica.", "playback_description": "Configure how music is played.",
"prefer_downloaded": "Prefer Downloaded Songs", "prefer_downloaded": "Prefer Downloaded Songs",
"caching_title": "Caching", "caching_title": "Caching",
"caching_description": "Automatically cache upcoming tracks for smoother playback.", "caching_description": "Automatically cache upcoming tracks for smoother playback.",
@@ -333,7 +333,7 @@
"tv_quota_days": "Giorni di quota per le serie TV", "tv_quota_days": "Giorni di quota per le serie TV",
"reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr",
"unlimited": "Illimitato", "unlimited": "Illimitato",
"plus_n_more": "+{{n}} altro", "plus_n_more": "+{{n}} more",
"order_by": { "order_by": {
"DEFAULT": "Predefinito", "DEFAULT": "Predefinito",
"VOTE_COUNT_AND_AVERAGE": "Conteggio delle votazioni e media", "VOTE_COUNT_AND_AVERAGE": "Conteggio delle votazioni e media",
@@ -352,25 +352,25 @@
} }
}, },
"streamystats": { "streamystats": {
"disable_streamystats": "Disabilita Streamystats", "disable_streamystats": "Disable Streamystats",
"enable_search": "Use for Search", "enable_search": "Use for Search",
"url": "URL", "url": "URL",
"server_url_placeholder": "http(s)://streamystats.example.com", "server_url_placeholder": "http(s)://streamystats.example.com",
"streamystats_search_hint": "Inserisci l'URL per il tuo server Streamystats. L'URL dovrebbe includere http o https ed eventualmente la porta.", "streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
"read_more_about_streamystats": "Read More About Streamystats.", "read_more_about_streamystats": "Read More About Streamystats.",
"save": "Salva", "save": "Save",
"features_title": "Funzionalità", "features_title": "Features",
"enable_movie_recommendations": "Movie Recommendations", "enable_movie_recommendations": "Movie Recommendations",
"enable_series_recommendations": "Series Recommendations", "enable_series_recommendations": "Series Recommendations",
"enable_promoted_watchlists": "Promoted Watchlists", "enable_promoted_watchlists": "Promoted Watchlists",
"hide_watchlists_tab": "Hide Watchlists Tab", "hide_watchlists_tab": "Hide Watchlists Tab",
"home_sections_hint": "Mostra consigli personalizzati e watchlist promosse da Streamystats nella home page.", "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": "Recommended Series",
"toasts": { "toasts": {
"saved": "Salvato", "saved": "Saved",
"refreshed": "Impostazioni aggiornate dal server", "refreshed": "Settings refreshed from server",
"disabled": "Streamystats disabilitato" "disabled": "Streamystats disabled"
}, },
"refresh_from_server": "Refresh Settings from Server" "refresh_from_server": "Refresh Settings from Server"
}, },
@@ -384,20 +384,18 @@
"device_usage": "Dispositivo {{availableSpace}}%", "device_usage": "Dispositivo {{availableSpace}}%",
"size_used": "{{used}} di {{total}} usato", "size_used": "{{used}} di {{total}} usato",
"delete_all_downloaded_files": "Cancella Tutti i File Scaricati", "delete_all_downloaded_files": "Cancella Tutti i File Scaricati",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Precarica automaticamente i brani mentre ascolti per una riproduzione più fluida e il supporto offline", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
"music_cache_size": "{{size}} nella cache", "music_cache_size": "{{size}} cached",
"music_cache_cleared": "Cache musicale cancellata", "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}} scaricato", "downloaded_songs_size": "{{size}} downloaded",
"downloaded_songs_deleted": "Brani scaricati eliminati", "downloaded_songs_deleted": "Downloaded songs deleted",
"clear_all_cache": "Clear All Cache", "clear_all_cache": "Clear All Cache",
"clear_all_cache_confirm": "Clear All Cache?", "clear_all_cache_confirm": "Clear All Cache?",
"clear_all_cache_confirm_desc": "Sei sicuro di voler cancellare tutti i dati nella cache? Questo cancellerà tutte le immagini nella cache, i file musicali, i sottotitoli e le cache delle interrogazioni. Le impostazioni e la sessione di login verranno mantenute.", "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_error_desc": "Si è verificato un errore durante la cancellazione della cache." "clear_all_cache_error_desc": "An error occurred while clearing the cache."
}, },
"intro": { "intro": {
"title": "Intro", "title": "Intro",
@@ -406,8 +404,8 @@
}, },
"logs": { "logs": {
"logs_title": "Log", "logs_title": "Log",
"export_logs": "Esporta i logs", "export_logs": "Export logs",
"click_for_more_info": "Clicca per maggiori informazioni", "click_for_more_info": "Click for more info",
"level": "Livello", "level": "Livello",
"no_logs_available": "Nessun log disponibile", "no_logs_available": "Nessun log disponibile",
"delete_all_logs": "Cancella tutti i log" "delete_all_logs": "Cancella tutti i log"
@@ -421,17 +419,17 @@
"error_deleting_files": "Errore nella cancellazione dei file" "error_deleting_files": "Errore nella cancellazione dei file"
}, },
"security": { "security": {
"title": "Sicurezza", "title": "Security",
"inactivity_timeout": { "inactivity_timeout": {
"title": "Inactivity Timeout", "title": "Inactivity Timeout",
"disabled": "Disabilitato", "disabled": "Disabled",
"1_minute": "1 minuto", "1_minute": "1 minute",
"5_minutes": "5 minuti", "5_minutes": "5 minutes",
"15_minutes": "15 minuti", "15_minutes": "15 minutes",
"30_minutes": "30 minuti", "30_minutes": "30 minutes",
"1_hour": "1 ora", "1_hour": "1 hour",
"4_hours": "4 ore", "4_hours": "4 hours",
"24_hours": "24 ore" "24_hours": "24 hours"
} }
} }
}, },
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Seleziona",
"no_trailer_available": "Nessun trailer disponibile", "no_trailer_available": "Nessun trailer disponibile",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -495,18 +494,18 @@
"mark_as_not_played": "Mark as not Played", "mark_as_not_played": "Mark as not Played",
"none": "Nulla", "none": "Nulla",
"track": "Traccia", "track": "Traccia",
"cancel": "Annulla", "cancel": "Cancel",
"delete": "Cancella", "delete": "Delete",
"ok": "OK", "ok": "OK",
"remove": "Rimuovi", "remove": "Remove",
"back": "Indietro", "back": "Back",
"continue": "Continua", "continue": "Continue",
"verifying": "Verifica in corso...", "verifying": "Verifying...",
"login": "Accedi", "login": "Login",
"episodes": "Episodi", "episodes": "Episodes",
"movies": "Film", "movies": "Movies",
"loading": "Caricamento…", "loading": "Loading…",
"seeAll": "Visualizza tutti" "seeAll": "See all"
}, },
"search": { "search": {
"search": "Cerca...", "search": "Cerca...",
@@ -520,10 +519,10 @@
"episodes": "Episodi", "episodes": "Episodi",
"collections": "Collezioni", "collections": "Collezioni",
"actors": "Attori", "actors": "Attori",
"artists": "Artisti", "artists": "Artists",
"albums": "Album", "albums": "Albums",
"songs": "Tracce", "songs": "Songs",
"playlists": "Playlist", "playlists": "Playlists",
"request_movies": "Film Richiesti", "request_movies": "Film Richiesti",
"request_series": "Serie Richieste", "request_series": "Serie Richieste",
"recently_added": "Aggiunti di Recente", "recently_added": "Aggiunti di Recente",
@@ -555,7 +554,7 @@
"movies": "film", "movies": "film",
"series": "serie TV", "series": "serie TV",
"boxsets": "cofanetti", "boxsets": "cofanetti",
"playlists": "Playlist", "playlists": "Playlists",
"items": "elementi" "items": "elementi"
}, },
"options": { "options": {
@@ -567,7 +566,7 @@
"cover": "Copertina", "cover": "Copertina",
"show_titles": "Mostra titoli", "show_titles": "Mostra titoli",
"show_stats": "Mostra statistiche", "show_stats": "Mostra statistiche",
"options_title": "Impostazioni" "options_title": "Options"
}, },
"filters": { "filters": {
"genres": "Generi", "genres": "Generi",
@@ -576,10 +575,10 @@
"filter_by": "Filter By", "filter_by": "Filter By",
"sort_order": "Criterio di ordinamento", "sort_order": "Criterio di ordinamento",
"tags": "Tag", "tags": "Tag",
"all": "Tutto", "all": "All",
"reset": "Ripristina", "reset": "Reset",
"asc": "Crescente", "asc": "Ascending",
"desc": "Decrescente" "desc": "Descending"
} }
}, },
"favorites": { "favorites": {
@@ -596,50 +595,51 @@
"no_links": "Nessun link" "no_links": "Nessun link"
}, },
"player": { "player": {
"live": "IN DIRETTA", "live": "LIVE",
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Errore del client", "client_error": "Errore del client",
"could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast", "could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast",
"message_from_server": "Messaggio dal server", "message_from_server": "Messaggio dal server",
"next_episode": "Prossimo Episodio", "next_episode": "Prossimo Episodio",
"continue_watching": "Continua a guardare", "continue_watching": "Continua a guardare",
"go_back": "Indietro", "go_back": "Indietro",
"downloaded_file_title": "Questo file è stato scaricato", "downloaded_file_title": "You have this file downloaded",
"downloaded_file_message": "Vuoi riprodurre il file scaricato?", "downloaded_file_message": "Do you want to play the downloaded file?",
"downloaded_file_yes": "Si", "downloaded_file_yes": "Yes",
"downloaded_file_no": "No", "downloaded_file_no": "No",
"downloaded_file_cancel": "Annulla", "downloaded_file_cancel": "Cancel",
"swipe_down_settings": "Scorri in basso per le impostazioni", "swipe_down_settings": "Swipe down for settings",
"ends_at": "Termina alle {{time}}", "ends_at": "Ends at {{time}}",
"search_subtitles": "Search Subtitles", "search_subtitles": "Search Subtitles",
"subtitle_tracks": "Tracce", "subtitle_tracks": "Tracks",
"subtitle_search": "Search & Download", "subtitle_search": "Search & Download",
"download": "Scarica", "download": "Download",
"subtitle_download_hint": "I sottotitoli scaricati verranno salvati nella tua libreria", "subtitle_download_hint": "Downloaded subtitles will be saved to your library",
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Lingua", "language": "Language",
"results": "Risultati", "results": "Results",
"search_failed": "Ricerca fallita", "searching": "Searching...",
"no_subtitle_provider": "Nessun provider di sottotitoli configurato sul server", "search_failed": "Search failed",
"no_subtitles_found": "Nessun sottotitolo trovato", "no_subtitle_provider": "No subtitle provider configured on server",
"add_opensubtitles_key_hint": "Aggiungi la chiave API OpenSubtitles nelle impostazioni", "no_subtitles_found": "No subtitles found",
"settings": "Impostazioni", "add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
"settings": "Settings",
"skip_intro": "Skip Intro", "skip_intro": "Skip Intro",
"skip_credits": "Skip Credits", "skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback", "stopPlayback": "Stop Playback",
"stopPlayingTitle": "Interrompere la riproduzione \"{{title}}\"?", "stopPlayingTitle": "Stop playing \"{{title}}\"?",
"stopPlayingConfirm": "Sei sicuro di voler interrompere la riproduzione?", "stopPlayingConfirm": "Are you sure you want to stop playback?",
"downloaded": "Scaricato", "downloaded": "Downloaded",
"missing_parameters": "Parametri di riproduzione mancanti" "missing_parameters": "Missing playback parameters"
}, },
"chapters": { "chapters": {
"title": "Capitoli", "title": "Chapters",
"chapter_number": "Capitolo {{number}}", "chapter_number": "Chapter {{number}}",
"open": "Apri capitoli", "open": "Open chapters",
"close": "Chiudi i capitoli" "close": "Close chapters"
}, },
"item_card": { "item_card": {
"next_up": "Il prossimo", "next_up": "Il prossimo",
@@ -664,19 +664,19 @@
"quality": "Qualità", "quality": "Qualità",
"audio": "Audio", "audio": "Audio",
"subtitles": { "subtitles": {
"label": "Sottotitoli", "label": "Subtitle",
"none": "Vuoto", "none": "None",
"tracks": "Tracce" "tracks": "Tracks"
}, },
"show_more": "Mostra di più", "show_more": "Mostra di più",
"show_less": "Mostra di meno", "show_less": "Mostra di meno",
"left": "sinistra", "left": "left",
"director": "Regista", "director": "Director",
"cast": "Cast", "cast": "Cast",
"technical_details": "Technical Details", "technical_details": "Technical Details",
"appeared_in": "Apparso in", "appeared_in": "Apparso in",
"movies": "Film", "movies": "Movies",
"shows": "Serie", "shows": "Shows",
"could_not_load_item": "Impossibile caricare l'elemento", "could_not_load_item": "Impossibile caricare l'elemento",
"none": "Nessuno", "none": "Nessuno",
"download": { "download": {
@@ -691,10 +691,10 @@
"mark_played": "Mark as Watched", "mark_played": "Mark as Watched",
"mark_unplayed": "Mark as Unwatched", "mark_unplayed": "Mark as Unwatched",
"resume_playback": "Resume Playback", "resume_playback": "Resume Playback",
"resume_playback_description": "Vuoi continuare da dove hai lasciato o riniziare da capo?", "resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
"play_from_start": "Play from Start", "play_from_start": "Play from Start",
"continue_from": "Continua da {{time}}", "continue_from": "Continue from {{time}}",
"no_data_available": "Nessun dato disponibile" "no_data_available": "No data available"
}, },
"live_tv": { "live_tv": {
"next": "Prossimo", "next": "Prossimo",
@@ -706,16 +706,16 @@
"sports": "Sport", "sports": "Sport",
"for_kids": "Per Bambini", "for_kids": "Per Bambini",
"news": "Notiziari", "news": "Notiziari",
"page_of": "Pagina {{current}} di {{total}}", "page_of": "Page {{current}} of {{total}}",
"no_programs": "Nessun programma disponibile", "no_programs": "No programs available",
"no_channels": "Nessun canale disponibile", "no_channels": "No channels available",
"tabs": { "tabs": {
"programs": "Programmi", "programs": "Programs",
"guide": "Guida", "guide": "Guide",
"channels": "Canali", "channels": "Channels",
"recordings": "Registrazioni", "recordings": "Recordings",
"schedule": "Pianifica", "schedule": "Schedule",
"series": "Serie Tv" "series": "Series"
} }
}, },
"jellyseerr": { "jellyseerr": {
@@ -761,12 +761,12 @@
"decline": "Rifiuta", "decline": "Rifiuta",
"requested_by": "Richiesto da {{user}}", "requested_by": "Richiesto da {{user}}",
"unknown_user": "Utente Sconosciuto", "unknown_user": "Utente Sconosciuto",
"select": "Seleziona", "select": "Select",
"request_all": "Request All", "request_all": "Request All",
"request_seasons": "Request Seasons", "request_seasons": "Request Seasons",
"select_seasons": "Select Seasons", "select_seasons": "Select Seasons",
"request_selected": "Request Selected", "request_selected": "Request Selected",
"n_selected": "{{count}} selezionati", "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.",
@@ -787,39 +787,39 @@
"library": "Libreria", "library": "Libreria",
"custom_links": "Collegamenti personalizzati", "custom_links": "Collegamenti personalizzati",
"favorites": "Preferiti", "favorites": "Preferiti",
"settings": "Impostazioni" "settings": "Settings"
}, },
"music": { "music": {
"title": "Musica", "title": "Music",
"tabs": { "tabs": {
"suggestions": "Suggerimenti", "suggestions": "Suggestions",
"albums": "Album", "albums": "Albums",
"artists": "Artisti", "artists": "Artists",
"playlists": "Playlist", "playlists": "Playlists",
"tracks": "tracks" "tracks": "tracks"
}, },
"recently_added": "Recently Added", "recently_added": "Recently Added",
"recently_played": "Recently Played", "recently_played": "Recently Played",
"frequently_played": "Frequently Played", "frequently_played": "Frequently Played",
"top_tracks": "Top Tracks", "top_tracks": "Top Tracks",
"play": "Riproduci", "play": "Play",
"shuffle": "Riproduzione casuale", "shuffle": "Shuffle",
"play_top_tracks": "Play Top Tracks", "play_top_tracks": "Play Top Tracks",
"no_suggestions": "Nessun suggerimento disponibile", "no_suggestions": "No suggestions available",
"no_albums": "Nessun album trovato", "no_albums": "No albums found",
"no_artists": "Artista non trovato", "no_artists": "No artists found",
"no_playlists": "Nessuna playlist trovata", "no_playlists": "No playlists found",
"album_not_found": "Album non trovato", "album_not_found": "Album not found",
"artist_not_found": "Artista non trovato", "artist_not_found": "Artist not found",
"playlist_not_found": "Playlist non trovata", "playlist_not_found": "Playlist not found",
"track_options": { "track_options": {
"play_next": "Play Next", "play_next": "Play Next",
"add_to_queue": "Add to Queue", "add_to_queue": "Add to Queue",
"add_to_playlist": "Add to Playlist", "add_to_playlist": "Add to Playlist",
"download": "Scarica", "download": "Download",
"downloaded": "Scaricato", "downloaded": "Downloaded",
"downloading": "Scaricamento...", "downloading": "Downloading...",
"cached": "Memorizzato nella cache", "cached": "Cached",
"delete_download": "Delete Download", "delete_download": "Delete Download",
"delete_cache": "Remove from Cache", "delete_cache": "Remove from Cache",
"go_to_artist": "Go to Artist", "go_to_artist": "Go to Artist",
@@ -831,112 +831,112 @@
"playlists": { "playlists": {
"create_playlist": "Create Playlist", "create_playlist": "Create Playlist",
"playlist_name": "Playlist Name", "playlist_name": "Playlist Name",
"enter_name": "Inserisci il nome della playlist", "enter_name": "Enter playlist name",
"create": "Crea", "create": "Create",
"search_playlists": "Cerca playlist...", "search_playlists": "Search playlists...",
"added_to": "Aggiunto a {{name}}", "added_to": "Added to {{name}}",
"added": "Aggiunto alla playlist", "added": "Added to playlist",
"removed_from": "Rimosso da {{name}}", "removed_from": "Removed from {{name}}",
"removed": "Rimosso dalla playlist", "removed": "Removed from playlist",
"created": "Playlist creata", "created": "Playlist created",
"create_new": "Create New Playlist", "create_new": "Create New Playlist",
"failed_to_add": "Impossibile aggiungere alla playlist", "failed_to_add": "Failed to add to playlist",
"failed_to_remove": "Impossibile rimuovere dalla playlist", "failed_to_remove": "Failed to remove from playlist",
"failed_to_create": "Impossibile creare la playlist", "failed_to_create": "Failed to create playlist",
"delete_playlist": "Delete Playlist", "delete_playlist": "Delete Playlist",
"delete_confirm": "Sei sicuro di voler eliminare\"{{name}}\"? Questa azione non può essere annullata.", "delete_confirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"deleted": "Playlist eliminata", "deleted": "Playlist deleted",
"failed_to_delete": "Impossibile eliminare la playlist" "failed_to_delete": "Failed to delete playlist"
}, },
"sort": { "sort": {
"title": "Sort By", "title": "Sort By",
"alphabetical": "Alfabetico", "alphabetical": "Alphabetical",
"date_created": "Date Created" "date_created": "Date Created"
} }
}, },
"watchlists": { "watchlists": {
"title": "Da vedere", "title": "Watchlists",
"my_watchlists": "My Watchlists", "my_watchlists": "My Watchlists",
"public_watchlists": "Public Watchlists", "public_watchlists": "Public Watchlists",
"create_title": "Create Watchlist", "create_title": "Create Watchlist",
"edit_title": "Edit Watchlist", "edit_title": "Edit Watchlist",
"create_button": "Create Watchlist", "create_button": "Create Watchlist",
"save_button": "Save Changes", "save_button": "Save Changes",
"delete_button": "Cancella", "delete_button": "Delete",
"remove_button": "Rimuovi", "remove_button": "Remove",
"cancel_button": "Annulla", "cancel_button": "Cancel",
"name_label": "Nome", "name_label": "Name",
"name_placeholder": "Inserisci il nome della lista \"Da vedere\"", "name_placeholder": "Enter watchlist name",
"description_label": "Descrizione", "description_label": "Description",
"description_placeholder": "Inserisci descrizione (opzionale)", "description_placeholder": "Enter description (optional)",
"is_public_label": "Public Watchlist", "is_public_label": "Public Watchlist",
"is_public_description": "Permetti ad altri di vedere questa lista", "is_public_description": "Allow others to view this watchlist",
"allowed_type_label": "Content Type", "allowed_type_label": "Content Type",
"sort_order_label": "Default Sort Order", "sort_order_label": "Default Sort Order",
"empty_title": "No Watchlists", "empty_title": "No Watchlists",
"empty_description": "Crea la tua prima lista \"Da vedere\" per iniziare a organizzare i tuoi media", "empty_description": "Create your first watchlist to start organizing your media",
"empty_watchlist": "Questa lista è vuota", "empty_watchlist": "This watchlist is empty",
"empty_watchlist_hint": "Aggiungi elementi dalla tua libreria a questa lista", "empty_watchlist_hint": "Add items from your library to this watchlist",
"not_configured_title": "Streamystats Not Configured", "not_configured_title": "Streamystats Not Configured",
"not_configured_description": "Configura Streamystats nelle impostazioni per utilizzare le watchlist", "not_configured_description": "Configure Streamystats in settings to use watchlists",
"go_to_settings": "Vai alle impostazioni", "go_to_settings": "Go to Settings",
"add_to_watchlist": "Add to Watchlist", "add_to_watchlist": "Add to Watchlist",
"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": "elemento", "item": "item",
"items": "elementi", "items": "items",
"public": "Pubblico", "public": "Public",
"private": "Privato", "private": "Private",
"you": "Tu", "you": "You",
"by_owner": "Da un altro utente", "by_owner": "By another user",
"not_found": "\"Da vedere\" non trovata", "not_found": "Watchlist not found",
"delete_confirm_title": "Delete Watchlist", "delete_confirm_title": "Delete Watchlist",
"delete_confirm_message": "Sei sicuro di voler eliminare\"{{name}}\"? Questa azione non può essere annullata.", "delete_confirm_message": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"remove_item_title": "Remove from Watchlist", "remove_item_title": "Remove from Watchlist",
"remove_item_message": "Rimuovere \"{{name}}\" da questa lista?", "remove_item_message": "Remove \"{{name}}\" from this watchlist?",
"loading": "Caricamento liste...", "loading": "Loading watchlists...",
"no_compatible_watchlists": "Nessuna lista compatibile", "no_compatible_watchlists": "No compatible watchlists",
"create_one_first": "Crea una lista che accetti questo tipo di contenuto" "create_one_first": "Create a watchlist that accepts this content type"
}, },
"playback_speed": { "playback_speed": {
"title": "Playback Speed", "title": "Playback Speed",
"apply_to": "Apply To", "apply_to": "Apply To",
"speed": "Velocità", "speed": "Speed",
"scope": { "scope": {
"media": "Solo questo media", "media": "This media only",
"show": "Questo show", "show": "This show",
"all": "Tutti i media (predefinito)" "all": "All media (default)"
} }
}, },
"companion_login": { "companion_login": {
"title": "Associa con la TV", "title": "Pair with TV",
"align_qr": "Allinea il QR code all'interno del riquadro", "align_qr": "Align the QR code within the frame",
"enter_code_manually": "Inserisci il codice manualmente", "enter_code_manually": "Enter code manually",
"pairing_enter_credentials": "Inserire le credenziali per la TV", "pairing_enter_credentials": "Enter credentials for TV",
"pairing_code_label": "Codice di associazione", "pairing_code_label": "Pairing code",
"server": "Server", "server": "Server",
"authorize_button": "Autorizza", "authorize_button": "Authorize",
"authorizing": "Autorizzando...", "authorizing": "Authorizing...",
"scan_again": "Scan Again", "scan_again": "Scan Again",
"done": "Fatto", "done": "Done",
"success_title": "Authorization Sent", "success_title": "Authorization Sent",
"pairing_tv_connecting": "La TV si sta collegando al tuo account", "pairing_tv_connecting": "The TV is connecting to your account",
"error_title": "Authorization Failed", "error_title": "Authorization Failed",
"error_invalid_qr": "QR code non valido. Scansiona il codice di associazione della TV.", "error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
"error_generic": "Si è verificato un errore. Riprova.", "error_generic": "Something went wrong. Please try again.",
"error_permission_denied": "Per scansionare i codici QR è necessaria l'autorizzazione della fotocamera.", "error_permission_denied": "Camera permission is required to scan QR codes.",
"login_as": "Accedi come {{username}}?", "login_as": "Log in as {{username}}?",
"on_server": "su {{server}}", "on_server": "on {{server}}",
"use_different_user": "Usa un altro utente", "use_different_user": "Use a different user",
"open_settings": "Apri le impostazioni" "open_settings": "Open Settings"
}, },
"pairing": { "pairing": {
"pair_with_phone": "Pair with Phone", "pair_with_phone": "Pair with Phone",
"pair_with_phone_title": "Login TV", "pair_with_phone_title": "Login TV",
"waiting_for_phone": "In attesa del telefono...", "waiting_for_phone": "Waiting for phone...",
"scan_with_phone": "Scansiona con l'applicazione Streamyfin sul tuo telefono", "scan_with_phone": "Scan with the Streamyfin app on your phone",
"logging_in": "Accesso in corso...", "logging_in": "Logging in...",
"logging_in_description": "Sto connettendo al server" "logging_in_description": "Connecting to your server"
} }
} }

View File

@@ -16,14 +16,14 @@
"got_it": "了解", "got_it": "了解",
"connection_failed": "接続に失敗しました", "connection_failed": "接続に失敗しました",
"could_not_connect_to_server": "サーバーに接続できませんでした。URLとネットワーク接続を確認してください。", "could_not_connect_to_server": "サーバーに接続できませんでした。URLとネットワーク接続を確認してください。",
"an_unexpected_error_occurred": "予期しないエラーが発生しました", "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_occurred_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を最新バージョンにアップデートしてください"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "クイックコネクトを承認する", "authorize_button": "クイックコネクトを承認する",
"enter_the_quick_connect_code": "クイックコネクトコードを入力...", "enter_the_quick_connect_code": "クイックコネクトコードを入力...",
"success": "成功しました", "success": "成功しました",
"quick_connect_authorized": "クイックコネクトが承認されました", "quick_connect_autorized": "クイックコネクトが承認されました",
"error": "エラー", "error": "エラー",
"invalid_code": "無効なコードです", "invalid_code": "無効なコードです",
"authorize": "承認" "authorize": "承認"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "カスタムメニューのリンクを表示", "show_custom_menu_links": "カスタムメニューのリンクを表示",
"show_large_home_carousel": "大きなヒーローBeta", "show_large_home_carousel": "大きなヒーローBeta",
"hide_libraries": "ライブラリを非表示", "hide_libraries": "ライブラリを非表示",
"select_libraries_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": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "デバイス {{availableSpace}}%", "device_usage": "デバイス {{availableSpace}}%",
"size_used": "{{used}} / {{total}} 使用済み", "size_used": "{{used}} / {{total}} 使用済み",
"delete_all_downloaded_files": "すべてのダウンロードファイルを削除", "delete_all_downloaded_files": "すべてのダウンロードファイルを削除",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "選択",
"no_trailer_available": "トレーラーがありません", "no_trailer_available": "トレーラーがありません",
"video": "映像", "video": "映像",
"audio": "音声", "audio": "音声",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "mpv_player_title": "MPV Player",
"error": "エラー", "error": "エラー",
"failed_to_get_stream_url": "ストリームURLを取得できませんでした", "failed_to_get_stream_url": "ストリームURLを取得できませんでした",
"an_error_occurred_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。", "an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。",
"client_error": "クライアントエラー", "client_error": "クライアントエラー",
"could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした", "could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした",
"message_from_server": "サーバーからのメッセージ", "message_from_server": "サーバーからのメッセージ",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "성공", "got_it": "성공",
"connection_failed": "연결 실패", "connection_failed": "연결 실패",
"could_not_connect_to_server": "서버에 연결되지 않았습니다. URL과 네트워크 상태를 확인하세요.", "could_not_connect_to_server": "서버에 연결되지 않았습니다. URL과 네트워크 상태를 확인하세요.",
"an_unexpected_error_occurred": "예기치 않은 오류가 발생했습니다", "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_occurred_did_you_enter_the_correct_url": "예기치 않은 오류가 발생했습니다. 서버 URL을 올바르게 입력하셨습니까?", "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"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "퀵 커넥트 승인", "authorize_button": "퀵 커넥트 승인",
"enter_the_quick_connect_code": "퀵 커넥트 코드 입력...", "enter_the_quick_connect_code": "퀵 커넥트 코드 입력...",
"success": "성공", "success": "성공",
"quick_connect_authorized": "퀵 커넥트 승인됨", "quick_connect_autorized": "퀵 커넥트 승인됨",
"error": "오류", "error": "오류",
"invalid_code": "유효하지 않은 코드", "invalid_code": "유효하지 않은 코드",
"authorize": "승인" "authorize": "승인"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "사용자 지정 메뉴 링크 표시", "show_custom_menu_links": "사용자 지정 메뉴 링크 표시",
"show_large_home_carousel": "대형 홈 슬라이드 배너 표시 (베타)", "show_large_home_carousel": "대형 홈 슬라이드 배너 표시 (베타)",
"hide_libraries": "라이브러리 숨기기", "hide_libraries": "라이브러리 숨기기",
"select_libraries_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",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "디바이스 {{availableSpace}}%", "device_usage": "디바이스 {{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",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Client Error", "client_error": "Client Error",
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}", "message_from_server": "Message from Server: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Begrepen", "got_it": "Begrepen",
"connection_failed": "Verbinding mislukt", "connection_failed": "Verbinding mislukt",
"could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.", "could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.",
"an_unexpected_error_occurred": "Er is een onverwachte fout opgetreden", "an_unexpected_error_occured": "Er is een onverwachte fout opgetreden",
"change_server": "Server wijzigen", "change_server": "Server wijzigen",
"invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", "invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord",
"user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden", "user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden",
"server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw", "server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw",
"server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw", "server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw",
"there_is_a_server_error": "Er is een serverfout", "there_is_a_server_error": "Er is een serverfout",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Er is een onverwachte fout opgetreden. Heb je de server URL correct ingegeven?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Er is een onverwachte fout opgetreden. Heb je de server URL correct ingegeven?",
"too_old_server_text": "Niet-ondersteunde Jellyfin Server Ontdekt", "too_old_server_text": "Niet-ondersteunde Jellyfin Server Ontdekt",
"too_old_server_description": "Werk Jellyfin bij naar de laatste versie" "too_old_server_description": "Werk Jellyfin bij naar de laatste versie"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Snel Verbinden toestaan", "authorize_button": "Snel Verbinden toestaan",
"enter_the_quick_connect_code": "Vul de Snel Verbinden code in...", "enter_the_quick_connect_code": "Vul de Snel Verbinden code in...",
"success": "Succes", "success": "Succes",
"quick_connect_authorized": "Snel Verbinden toegestaan", "quick_connect_autorized": "Snel Verbinden toegestaan",
"error": "Fout", "error": "Fout",
"invalid_code": "Ongeldige code", "invalid_code": "Ongeldige code",
"authorize": "Toestaan" "authorize": "Toestaan"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Aangepaste menulinks tonen", "show_custom_menu_links": "Aangepaste menulinks tonen",
"show_large_home_carousel": "Toon grote carrousel op startpagina (bèta)", "show_large_home_carousel": "Toon grote carrousel op startpagina (bèta)",
"hide_libraries": "Verberg Bibliotheken", "hide_libraries": "Verberg Bibliotheken",
"select_libraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.", "select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
"disable_haptic_feedback": "Haptische feedback uitschakelen", "disable_haptic_feedback": "Haptische feedback uitschakelen",
"default_quality": "Standaard kwaliteit", "default_quality": "Standaard kwaliteit",
"default_playback_speed": "Standaard Afspeelsnelheid", "default_playback_speed": "Standaard Afspeelsnelheid",
@@ -384,8 +384,6 @@
"device_usage": "Toestel {{availableSpace}}%", "device_usage": "Toestel {{availableSpace}}%",
"size_used": "{{used}} van {{total}} gebruikt", "size_used": "{{used}} van {{total}} gebruikt",
"delete_all_downloaded_files": "Verwijder alle gedownloade bestanden", "delete_all_downloaded_files": "Verwijder alle gedownloade bestanden",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Selecteren",
"no_trailer_available": "Geen trailer beschikbaar", "no_trailer_available": "Geen trailer beschikbaar",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Fout van de client", "client_error": "Fout van de client",
"could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast", "could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast",
"message_from_server": "Bericht van de server", "message_from_server": "Bericht van de server",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Har det", "got_it": "Har det",
"connection_failed": "Tilkobling mislyktes", "connection_failed": "Tilkobling mislyktes",
"could_not_connect_to_server": "Kunne ikke koble til serveren. Kontroller nettadressen og nettverkstilkoblingen.", "could_not_connect_to_server": "Kunne ikke koble til serveren. Kontroller nettadressen og nettverkstilkoblingen.",
"an_unexpected_error_occurred": "En uventet feil oppstod", "an_unexpected_error_occured": "En uventet feil oppstod",
"change_server": "Endre server", "change_server": "Endre server",
"invalid_username_or_password": "Ugyldig brukernavn eller passord", "invalid_username_or_password": "Ugyldig brukernavn eller passord",
"user_does_not_have_permission_to_log_in": "Brukeren har ikke tillatelse til å logge inn", "user_does_not_have_permission_to_log_in": "Brukeren har ikke tillatelse til å logge inn",
"server_is_taking_too_long_to_respond_try_again_later": "Serveren tar for lang tid å svare, prøv igjen senere", "server_is_taking_too_long_to_respond_try_again_later": "Serveren tar for lang tid å svare, prøv igjen senere",
"server_received_too_many_requests_try_again_later": "Serveren mottok for mange forespørsler, prøv igjen senere.", "server_received_too_many_requests_try_again_later": "Serveren mottok for mange forespørsler, prøv igjen senere.",
"there_is_a_server_error": "Det er en serverfeil", "there_is_a_server_error": "Det er en serverfeil",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Det oppstod en uventet feil. Sendte du inn URLen til serveren riktig?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Det oppstod en uventet feil. Sendte du inn URLen til serveren riktig?",
"too_old_server_text": "Ustøttet Jellyfin Server oppdaget", "too_old_server_text": "Ustøttet Jellyfin Server oppdaget",
"too_old_server_description": "Vennligst oppdater Jellyfin til nyeste versjon" "too_old_server_description": "Vennligst oppdater Jellyfin til nyeste versjon"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Autoriser rask tilkobling", "authorize_button": "Autoriser rask tilkobling",
"enter_the_quick_connect_code": "Skriv inn hurtig-tilkobling kode...", "enter_the_quick_connect_code": "Skriv inn hurtig-tilkobling kode...",
"success": "Vellykket", "success": "Vellykket",
"quick_connect_authorized": "Hurtig tilkobling autorisert", "quick_connect_autorized": "Hurtig tilkobling autorisert",
"error": "Feil", "error": "Feil",
"invalid_code": "Ugyldig kode", "invalid_code": "Ugyldig kode",
"authorize": "Autoriser" "authorize": "Autoriser"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Vis tilpassede menylenker", "show_custom_menu_links": "Vis tilpassede menylenker",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Skjul biblioteker", "hide_libraries": "Skjul biblioteker",
"select_libraries_you_want_to_hide": "Velg bibliotekene du vil skjule deg for Biblioteket og avsnittene for hjemmesider.", "select_liraries_you_want_to_hide": "Velg bibliotekene du vil skjule deg for Biblioteket og avsnittene for hjemmesider.",
"disable_haptic_feedback": "Deaktiver Haptisk tilbakemelding", "disable_haptic_feedback": "Deaktiver Haptisk tilbakemelding",
"default_quality": "Standard kvalitet", "default_quality": "Standard kvalitet",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Enhet {{availableSpace}}%", "device_usage": "Enhet {{availableSpace}}%",
"size_used": "{{used}} av {{total}} er i bruk", "size_used": "{{used}} av {{total}} er i bruk",
"delete_all_downloaded_files": "Slett alle nedlastede filer", "delete_all_downloaded_files": "Slett alle nedlastede filer",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Velg",
"no_trailer_available": "Ingen trailer tilgjengelig", "no_trailer_available": "Ingen trailer tilgjengelig",
"video": "Video", "video": "Video",
"audio": "Lyd", "audio": "Lyd",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Feil med annonsør", "client_error": "Feil med annonsør",
"could_not_create_stream_for_chromecast": "Kan ikke opprette en strøm for Chromecast", "could_not_create_stream_for_chromecast": "Kan ikke opprette en strøm for Chromecast",
"message_from_server": "Melding fra tjener: {{message}}", "message_from_server": "Melding fra tjener: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Rozumiem", "got_it": "Rozumiem",
"connection_failed": "Połączenie nieudane", "connection_failed": "Połączenie nieudane",
"could_not_connect_to_server": "Nie można połączyć się z serwerem. Sprawdź adres URL oraz połączenie sieciowe.", "could_not_connect_to_server": "Nie można połączyć się z serwerem. Sprawdź adres URL oraz połączenie sieciowe.",
"an_unexpected_error_occurred": "Wystąpił nieoczekiwany błąd", "an_unexpected_error_occured": "Wystąpił nieoczekiwany błąd",
"change_server": "Zmień serwer", "change_server": "Zmień serwer",
"invalid_username_or_password": "Nieprawidłowa nazwa użytkownika lub hasło", "invalid_username_or_password": "Nieprawidłowa nazwa użytkownika lub hasło",
"user_does_not_have_permission_to_log_in": "Użytkownik nie ma uprawnień do logowania", "user_does_not_have_permission_to_log_in": "Użytkownik nie ma uprawnień do logowania",
"server_is_taking_too_long_to_respond_try_again_later": "Serwer zbyt długo nie odpowiada spróbuj ponownie później", "server_is_taking_too_long_to_respond_try_again_later": "Serwer zbyt długo nie odpowiada spróbuj ponownie później",
"server_received_too_many_requests_try_again_later": "Serwer otrzymał zbyt wiele żądań spróbuj ponownie później.", "server_received_too_many_requests_try_again_later": "Serwer otrzymał zbyt wiele żądań spróbuj ponownie później.",
"there_is_a_server_error": "Wystąpił błąd serwera", "there_is_a_server_error": "Wystąpił błąd serwera",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Wystąpił nieoczekiwany błąd. Czy wpisałeś poprawny adres URL?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Wystąpił nieoczekiwany błąd. Czy wpisałeś poprawny adres URL?",
"too_old_server_text": "Wykryto nieobsługiwany serwer Jellyfin", "too_old_server_text": "Wykryto nieobsługiwany serwer Jellyfin",
"too_old_server_description": "Proszę zaktualizować Jellyfin do najnowszej wersji" "too_old_server_description": "Proszę zaktualizować Jellyfin do najnowszej wersji"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Autoryzuj szybkie połączenie", "authorize_button": "Autoryzuj szybkie połączenie",
"enter_the_quick_connect_code": "Wpisz kod szybkiego połączenia...", "enter_the_quick_connect_code": "Wpisz kod szybkiego połączenia...",
"success": "Sukces", "success": "Sukces",
"quick_connect_authorized": "Szybkie połączenie autoryzowane", "quick_connect_autorized": "Szybkie połączenie autoryzowane",
"error": "Błąd", "error": "Błąd",
"invalid_code": "Nieprawidłowy kod", "invalid_code": "Nieprawidłowy kod",
"authorize": "Autoryzuj" "authorize": "Autoryzuj"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Pokaż niestandardowe odnośniki w menu", "show_custom_menu_links": "Pokaż niestandardowe odnośniki w menu",
"show_large_home_carousel": "Wyświetl Dużą Karuzelę na ekranie głównym (beta)", "show_large_home_carousel": "Wyświetl Dużą Karuzelę na ekranie głównym (beta)",
"hide_libraries": "Ukryj biblioteki", "hide_libraries": "Ukryj biblioteki",
"select_libraries_you_want_to_hide": "Wybierz biblioteki, które chcesz ukryć na karcie Biblioteka i w sekcjach strony głównej.", "select_liraries_you_want_to_hide": "Wybierz biblioteki, które chcesz ukryć na karcie Biblioteka i w sekcjach strony głównej.",
"disable_haptic_feedback": "Wyłącz wibracje", "disable_haptic_feedback": "Wyłącz wibracje",
"default_quality": "Domyślna jakość", "default_quality": "Domyślna jakość",
"default_playback_speed": "Domyślna prędkość odtwarzania", "default_playback_speed": "Domyślna prędkość odtwarzania",
@@ -384,8 +384,6 @@
"device_usage": "Urządzenie {{availableSpace}}%", "device_usage": "Urządzenie {{availableSpace}}%",
"size_used": "{{used}} z {{total}} wykorzystane", "size_used": "{{used}} z {{total}} wykorzystane",
"delete_all_downloaded_files": "Usuń wszystkie pobrane pliki", "delete_all_downloaded_files": "Usuń wszystkie pobrane pliki",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Bufor muzyki", "music_cache_title": "Bufor muzyki",
"music_cache_description": "Automatycznie buforuj piosenki w trakcie słuchania dla płynniejszego odtwarzania i wsparcia offline", "music_cache_description": "Automatycznie buforuj piosenki w trakcie słuchania dla płynniejszego odtwarzania i wsparcia offline",
"clear_music_cache": "Wyczyść bufor muzyki", "clear_music_cache": "Wyczyść bufor muzyki",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"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",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Błąd klienta", "client_error": "Błąd klienta",
"could_not_create_stream_for_chromecast": "Nie udało się utworzyć strumienia dla Chromecasta", "could_not_create_stream_for_chromecast": "Nie udało się utworzyć strumienia dla Chromecasta",
"message_from_server": "Wiadomość z serwera: {{message}}", "message_from_server": "Wiadomość z serwera: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Tenho isso", "got_it": "Tenho isso",
"connection_failed": "Falha na conexão", "connection_failed": "Falha na conexão",
"could_not_connect_to_server": "Não foi possível conectar ao servidor. Verifique a URL e sua conexão de rede.", "could_not_connect_to_server": "Não foi possível conectar ao servidor. Verifique a URL e sua conexão de rede.",
"an_unexpected_error_occurred": "An unexpected error occurred", "an_unexpected_error_occured": "Ocorreu um erro inesperado",
"change_server": "Alterar Servidor", "change_server": "Alterar Servidor",
"invalid_username_or_password": "Usuário ou senha inválidos", "invalid_username_or_password": "Usuário ou senha inválidos",
"user_does_not_have_permission_to_log_in": "Usuário não tem permissão para fazer login", "user_does_not_have_permission_to_log_in": "Usuário não tem permissão para fazer login",
"server_is_taking_too_long_to_respond_try_again_later": "O servidor está demorando muito para responder, tente novamente mais tarde", "server_is_taking_too_long_to_respond_try_again_later": "O servidor está demorando muito para responder, tente novamente mais tarde",
"server_received_too_many_requests_try_again_later": "O servidor recebeu muitos pedidos, tente novamente mais tarde.", "server_received_too_many_requests_try_again_later": "O servidor recebeu muitos pedidos, tente novamente mais tarde.",
"there_is_a_server_error": "Existe um erro no servidor", "there_is_a_server_error": "Existe um erro no servidor",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ocorreu um erro inesperado. Você inseriu a URL do servidor corretamente?",
"too_old_server_text": "Servidor Jellyfin Descoberto Não Suportado", "too_old_server_text": "Servidor Jellyfin Descoberto Não Suportado",
"too_old_server_description": "Por favor, atualize o Jellyfin para a versão mais recente" "too_old_server_description": "Por favor, atualize o Jellyfin para a versão mais recente"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Autorizar conexão rápida", "authorize_button": "Autorizar conexão rápida",
"enter_the_quick_connect_code": "Insira o código de conexão rápida...", "enter_the_quick_connect_code": "Insira o código de conexão rápida...",
"success": "Sucesso", "success": "Sucesso",
"quick_connect_authorized": "Quick Connect authorized", "quick_connect_autorized": "Acesso Rápido Autorizado",
"error": "Erro", "error": "Erro",
"invalid_code": "Código inválido", "invalid_code": "Código inválido",
"authorize": "Autorizar" "authorize": "Autorizar"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Mostrar Links de Menu Personalizado", "show_custom_menu_links": "Mostrar Links de Menu Personalizado",
"show_large_home_carousel": "Mostrar Carrossel Grande (beta)", "show_large_home_carousel": "Mostrar Carrossel Grande (beta)",
"hide_libraries": "Ocultar bibliotecas", "hide_libraries": "Ocultar bibliotecas",
"select_libraries_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": "Selecione as bibliotecas que você deseja ocultar da aba Biblioteca e seções da página inicial.",
"disable_haptic_feedback": "Desativar o retorno tátil", "disable_haptic_feedback": "Desativar o retorno tátil",
"default_quality": "Qualidade Padrão", "default_quality": "Qualidade Padrão",
"default_playback_speed": "Velocidade padrão de reprodução", "default_playback_speed": "Velocidade padrão de reprodução",
@@ -384,8 +384,6 @@
"device_usage": "Dispositivo {{availableSpace}}%", "device_usage": "Dispositivo {{availableSpace}}%",
"size_used": "{{used}} de {{total}} Utilizados", "size_used": "{{used}} de {{total}} Utilizados",
"delete_all_downloaded_files": "Excluir todos os arquivos baixados", "delete_all_downloaded_files": "Excluir todos os arquivos baixados",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Cache de Música", "music_cache_title": "Cache de Música",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Limpar Cache de Música", "clear_music_cache": "Limpar Cache de Música",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"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",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.", "an_error_occured_while_playing_the_video": "Ocorreu um erro ao reproduzir o vídeo. Verifique os logs nas configurações.",
"client_error": "Erro de Cliente", "client_error": "Erro de Cliente",
"could_not_create_stream_for_chromecast": "Não foi possível criar um fluxo para o Chromecast", "could_not_create_stream_for_chromecast": "Não foi possível criar um fluxo para o Chromecast",
"message_from_server": "Mensagem do Servidor: {{message}}", "message_from_server": "Mensagem do Servidor: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Am înţeles", "got_it": "Am înţeles",
"connection_failed": "Conectare eșuată", "connection_failed": "Conectare eșuată",
"could_not_connect_to_server": "Nu s-a putut conecta la server. Verificați adresa URL și conexiunea la internet.", "could_not_connect_to_server": "Nu s-a putut conecta la server. Verificați adresa URL și conexiunea la internet.",
"an_unexpected_error_occurred": "A apărut o eroare neașteptată", "an_unexpected_error_occured": "A apărut o eroare neașteptată",
"change_server": "Schimba", "change_server": "Schimba",
"invalid_username_or_password": "Nume de utilizator sau parolă greșită", "invalid_username_or_password": "Nume de utilizator sau parolă greșită",
"user_does_not_have_permission_to_log_in": "Utilizatorul nu are permisiunea de a se conecta", "user_does_not_have_permission_to_log_in": "Utilizatorul nu are permisiunea de a se conecta",
"server_is_taking_too_long_to_respond_try_again_later": "Serverul răspunde prea greu, încearcă din nou mai târziu.", "server_is_taking_too_long_to_respond_try_again_later": "Serverul răspunde prea greu, încearcă din nou mai târziu.",
"server_received_too_many_requests_try_again_later": "Serverul a primit prea multe solicitări, încercați din nou mai târziu.", "server_received_too_many_requests_try_again_later": "Serverul a primit prea multe solicitări, încercați din nou mai târziu.",
"there_is_a_server_error": "Eroare de server", "there_is_a_server_error": "Eroare de server",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "A apărut o eroare neașteptată. Ați introdus corect adresa URL a serverului?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "A apărut o eroare neașteptată. Ați introdus corect adresa URL a serverului?",
"too_old_server_text": "Serverul Jellyfin Neacceptat Descoperit", "too_old_server_text": "Serverul Jellyfin Neacceptat Descoperit",
"too_old_server_description": "Vă rugăm să actualizați Jellyfin la cea mai recentă versiune" "too_old_server_description": "Vă rugăm să actualizați Jellyfin la cea mai recentă versiune"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Autorizare conectare rapidă", "authorize_button": "Autorizare conectare rapidă",
"enter_the_quick_connect_code": "Introduceți codul pt. conectare rapidă...", "enter_the_quick_connect_code": "Introduceți codul pt. conectare rapidă...",
"success": "Succes", "success": "Succes",
"quick_connect_authorized": "Conectare rapidă autorizată", "quick_connect_autorized": "Conectare rapidă autorizată",
"error": "Eroare", "error": "Eroare",
"invalid_code": "Cod invalid.", "invalid_code": "Cod invalid.",
"authorize": "Autorizează" "authorize": "Autorizează"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Afișează link-uri personalizate în meniu", "show_custom_menu_links": "Afișează link-uri personalizate în meniu",
"show_large_home_carousel": "Arată Caruselul Media Mare (beta)", "show_large_home_carousel": "Arată Caruselul Media Mare (beta)",
"hide_libraries": "Ascunde bibliotecile", "hide_libraries": "Ascunde bibliotecile",
"select_libraries_you_want_to_hide": "Selectează bibliotecile pe care dorești să le ascunzi din fila Bibliotecă și din secțiunile paginii principale.", "select_liraries_you_want_to_hide": "Selectează bibliotecile pe care dorești să le ascunzi din fila Bibliotecă și din secțiunile paginii principale.",
"disable_haptic_feedback": "Dezactivează vibrațiile tactile", "disable_haptic_feedback": "Dezactivează vibrațiile tactile",
"default_quality": "Calitate implicită", "default_quality": "Calitate implicită",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Dispozitiv {{availableSpace}}%", "device_usage": "Dispozitiv {{availableSpace}}%",
"size_used": "{{used}} din {{total}} folosit", "size_used": "{{used}} din {{total}} folosit",
"delete_all_downloaded_files": "Ștergeți toate fișierele descărcate", "delete_all_downloaded_files": "Ștergeți toate fișierele descărcate",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Selectare",
"no_trailer_available": "Nicio remorcă disponibilă", "no_trailer_available": "Nicio remorcă disponibilă",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Eroare client", "client_error": "Eroare client",
"could_not_create_stream_for_chromecast": "Nu s-a putut crea un flux pentru Chromecast", "could_not_create_stream_for_chromecast": "Nu s-a putut crea un flux pentru Chromecast",
"message_from_server": "Mesaj de la server: {{message}}", "message_from_server": "Mesaj de la server: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Принято", "got_it": "Принято",
"connection_failed": "Соединение не удалось", "connection_failed": "Соединение не удалось",
"could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста, проверьте URL и ваше интернет-соединение.", "could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста, проверьте URL и ваше интернет-соединение.",
"an_unexpected_error_occurred": "Возникла непредвиденная ошибка", "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_occurred_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 до последней версии"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Авторизовать через быстрое подключение", "authorize_button": "Авторизовать через быстрое подключение",
"enter_the_quick_connect_code": "Введите код для быстрого подключения...", "enter_the_quick_connect_code": "Введите код для быстрого подключения...",
"success": "Успех", "success": "Успех",
"quick_connect_authorized": "Быстрое подключение авторизовано", "quick_connect_autorized": "Быстрое подключение авторизовано",
"error": "Ошибка", "error": "Ошибка",
"invalid_code": "Неверный код", "invalid_code": "Неверный код",
"authorize": "Авторизовать" "authorize": "Авторизовать"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Показать ссылки пользовательского меню", "show_custom_menu_links": "Показать ссылки пользовательского меню",
"show_large_home_carousel": "Показывать большую карусель (beta)", "show_large_home_carousel": "Показывать большую карусель (beta)",
"hide_libraries": "Скрыть библиотеки", "hide_libraries": "Скрыть библиотеки",
"select_libraries_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": "Скорость воспроизведения по умолчанию",
@@ -384,8 +384,6 @@
"device_usage": "Устройство {{availableSpace}}%", "device_usage": "Устройство {{availableSpace}}%",
"size_used": "{{used}} из {{total}} использовано", "size_used": "{{used}} из {{total}} использовано",
"delete_all_downloaded_files": "Удалить все загруженные файлы", "delete_all_downloaded_files": "Удалить все загруженные файлы",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Кеш музыки", "music_cache_title": "Кеш музыки",
"music_cache_description": "Автоматически кешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета", "music_cache_description": "Автоматически кешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета",
"clear_music_cache": "Очистить кеш музыки", "clear_music_cache": "Очистить кеш музыки",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Выбрать",
"no_trailer_available": "Трейлер недоступен", "no_trailer_available": "Трейлер недоступен",
"video": "Видео", "video": "Видео",
"audio": "Звук", "audio": "Звук",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "mpv_player_title": "MPV Player",
"error": "Ошибка", "error": "Ошибка",
"failed_to_get_stream_url": "Не удалось получить URL потока", "failed_to_get_stream_url": "Не удалось получить URL потока",
"an_error_occurred_while_playing_the_video": "Возникла Неожиданная ошибка во время воспроизведения. Проверьте логи в настройках.", "an_error_occured_while_playing_the_video": "Возникла Неожиданная ошибка во время воспроизведения. Проверьте логи в настройках.",
"client_error": "Ошибка клиента", "client_error": "Ошибка клиента",
"could_not_create_stream_for_chromecast": "Не удалось создать поток для Chromecast", "could_not_create_stream_for_chromecast": "Не удалось создать поток для Chromecast",
"message_from_server": "Сообщение от сервера: {{message}}", "message_from_server": "Сообщение от сервера: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Koden har mottagits", "got_it": "Koden har mottagits",
"connection_failed": "Anslutningen Misslyckades", "connection_failed": "Anslutningen Misslyckades",
"could_not_connect_to_server": "Det gick inte att ansluta till servern. Kontrollera webbadressen och din nätverksanslutning.", "could_not_connect_to_server": "Det gick inte att ansluta till servern. Kontrollera webbadressen och din nätverksanslutning.",
"an_unexpected_error_occurred": "Ett Oväntat Fel Uppstod", "an_unexpected_error_occured": "Ett Oväntat Fel Uppstod",
"change_server": "Byt Server", "change_server": "Byt Server",
"invalid_username_or_password": "Ogiltigt användarnamn eller lösenord", "invalid_username_or_password": "Ogiltigt användarnamn eller lösenord",
"user_does_not_have_permission_to_log_in": "Användaren har inte behörighet att logga in", "user_does_not_have_permission_to_log_in": "Användaren har inte behörighet att logga in",
"server_is_taking_too_long_to_respond_try_again_later": "Servern svarar för långsamt, försök igen senare", "server_is_taking_too_long_to_respond_try_again_later": "Servern svarar för långsamt, försök igen senare",
"server_received_too_many_requests_try_again_later": "Servern har fått för många förfrågningar, försök igen senare.", "server_received_too_many_requests_try_again_later": "Servern har fått för många förfrågningar, försök igen senare.",
"there_is_a_server_error": "Ett serverfel uppstod", "there_is_a_server_error": "Ett serverfel uppstod",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Ett oväntat fel uppstod. Har du angett serverns URL korrekt?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Ett oväntat fel uppstod. Har du angett serverns URL korrekt?",
"too_old_server_text": "Jellyfin Servern Stöds Inte", "too_old_server_text": "Jellyfin Servern Stöds Inte",
"too_old_server_description": "Var God Uppdatera Jellyfin Till Den Senaste Versionen" "too_old_server_description": "Var God Uppdatera Jellyfin Till Den Senaste Versionen"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Godkänn snabbanslutning", "authorize_button": "Godkänn snabbanslutning",
"enter_the_quick_connect_code": "Ange snabbanslutningskod...", "enter_the_quick_connect_code": "Ange snabbanslutningskod...",
"success": "Snabbanslutning lyckades", "success": "Snabbanslutning lyckades",
"quick_connect_authorized": "Snabbanslutning Godkänd", "quick_connect_autorized": "Snabbanslutning Godkänd",
"error": "Fel", "error": "Fel",
"invalid_code": "Ogiltig kod", "invalid_code": "Ogiltig kod",
"authorize": "Autentisera" "authorize": "Autentisera"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Visa anpassade menylänkar", "show_custom_menu_links": "Visa anpassade menylänkar",
"show_large_home_carousel": "Visa toppbanner (beta)", "show_large_home_carousel": "Visa toppbanner (beta)",
"hide_libraries": "Dölj bibliotek", "hide_libraries": "Dölj bibliotek",
"select_libraries_you_want_to_hide": "Välj de bibliotek du vill dölja på fliken Bibliotek och på startsidan.", "select_liraries_you_want_to_hide": "Välj de bibliotek du vill dölja på fliken Bibliotek och på startsidan.",
"disable_haptic_feedback": "Stäng av vibrationer", "disable_haptic_feedback": "Stäng av vibrationer",
"default_quality": "Förvald Kvalitet", "default_quality": "Förvald Kvalitet",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Telefon {{availableSpace}}%", "device_usage": "Telefon {{availableSpace}}%",
"size_used": "{{used}} av {{total}} används", "size_used": "{{used}} av {{total}} används",
"delete_all_downloaded_files": "Ta bort alla nerladdade filer", "delete_all_downloaded_files": "Ta bort alla nerladdade filer",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Musikcache", "music_cache_title": "Musikcache",
"music_cache_description": "Cacha automatiskt låtar när du lyssnar för smidigare uppspelning och offline-stöd", "music_cache_description": "Cacha automatiskt låtar när du lyssnar för smidigare uppspelning och offline-stöd",
"clear_music_cache": "Rensa musikcache", "clear_music_cache": "Rensa musikcache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "Inga resultat", "no_results": "Inga resultat",
"select": "Välj",
"no_trailer_available": "Ingen trailer tillgänglig", "no_trailer_available": "Ingen trailer tillgänglig",
"video": "Video", "video": "Video",
"audio": "Ljud", "audio": "Ljud",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Klientfel", "client_error": "Klientfel",
"could_not_create_stream_for_chromecast": "Kunde inte skapa stream för Chromecast", "could_not_create_stream_for_chromecast": "Kunde inte skapa stream för Chromecast",
"message_from_server": "Meddelande från servern: {{message}}", "message_from_server": "Meddelande från servern: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Använder Jellyfin-server", "using_jellyfin_server": "Använder Jellyfin-server",
"language": "Språk", "language": "Språk",
"results": "Resultat", "results": "Resultat",
"searching": "Söker...",
"search_failed": "Sökningen misslyckades", "search_failed": "Sökningen misslyckades",
"no_subtitle_provider": "Ingen undertextleverantör konfigurerad på servern", "no_subtitle_provider": "Ingen undertextleverantör konfigurerad på servern",
"no_subtitles_found": "Inga undertexter hittades", "no_subtitles_found": "Inga undertexter hittades",

View File

@@ -16,14 +16,14 @@
"got_it": "Got It", "got_it": "Got It",
"connection_failed": "Connection Failed", "connection_failed": "Connection Failed",
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.", "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
"an_unexpected_error_occurred": "An unexpected error occurred", "an_unexpected_error_occured": "An Unexpected Error Occurred",
"change_server": "Change Server", "change_server": "Change Server",
"invalid_username_or_password": "Invalid Username or Password", "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_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later",
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
"there_is_a_server_error": "There is a server error", "there_is_a_server_error": "There is a server error",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?", "an_unexpected_error_occured_did_you_enter_the_correct_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"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Authorize Quick Connect", "authorize_button": "Authorize Quick Connect",
"enter_the_quick_connect_code": "Enter the quick connect code...", "enter_the_quick_connect_code": "Enter the quick connect code...",
"success": "Success", "success": "Success",
"quick_connect_authorized": "Quick Connect authorized", "quick_connect_autorized": "Quick Connect Authorized",
"error": "Error", "error": "Error",
"invalid_code": "Invalid Code", "invalid_code": "Invalid Code",
"authorize": "Authorize" "authorize": "Authorize"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Show Custom Menu Links", "show_custom_menu_links": "Show Custom Menu Links",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Hide Libraries", "hide_libraries": "Hide Libraries",
"select_libraries_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",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Device {{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",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Client Error", "client_error": "Client Error",
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}", "message_from_server": "Message from Server: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "jIyaj", "got_it": "jIyaj",
"connection_failed": "ngoQlaHbe'", "connection_failed": "ngoQlaHbe'",
"could_not_connect_to_server": "SeHlaw veS Ho'Do'laHbe'. URL 'ej ret ghun mej.", "could_not_connect_to_server": "SeHlaw veS Ho'Do'laHbe'. URL 'ej ret ghun mej.",
"an_unexpected_error_occurred": "num ghIq Doch", "an_unexpected_error_occured": "num ghIq Doch",
"change_server": "Ho'Do' veS yIghoS", "change_server": "Ho'Do' veS yIghoS",
"invalid_username_or_password": "tlhIngan pagh ngoq De' law'be'", "invalid_username_or_password": "tlhIngan pagh ngoq De' law'be'",
"user_does_not_have_permission_to_log_in": "tlhIngan lut 'el je'laHbe'", "user_does_not_have_permission_to_log_in": "tlhIngan lut 'el je'laHbe'",
"server_is_taking_too_long_to_respond_try_again_later": "Ho'Do' veS jachrup. pItlh yIHaD", "server_is_taking_too_long_to_respond_try_again_later": "Ho'Do' veS jachrup. pItlh yIHaD",
"server_received_too_many_requests_try_again_later": "Ho'Do' veS lutlh ngeb petlh law'. pItlh yIHaD.", "server_received_too_many_requests_try_again_later": "Ho'Do' veS lutlh ngeb petlh law'. pItlh yIHaD.",
"there_is_a_server_error": "Ho'Do' veS ghIq maS", "there_is_a_server_error": "Ho'Do' veS ghIq maS",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "num ghIq Doch. URL mej Danej'a'?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "num ghIq Doch. URL mej Danej'a'?",
"too_old_server_text": "Unsupported Jellyfin Server Discovered", "too_old_server_text": "Unsupported Jellyfin Server Discovered",
"too_old_server_description": "**Jellyfin chu'qu' Dotlh yIchoHmoH**" "too_old_server_description": "**Jellyfin chu'qu' Dotlh yIchoHmoH**"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "parmaq ngoQ yIje'", "authorize_button": "parmaq ngoQ yIje'",
"enter_the_quick_connect_code": "parmaq ngoQ De' yI'el...", "enter_the_quick_connect_code": "parmaq ngoQ De' yI'el...",
"success": "Qapla'", "success": "Qapla'",
"quick_connect_authorized": "parmaq ngoQ je'laH", "quick_connect_autorized": "parmaq ngoQ je'laH",
"error": "ghIq", "error": "ghIq",
"invalid_code": "De' law'be'", "invalid_code": "De' law'be'",
"authorize": "yIje'" "authorize": "yIje'"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "menuDaq ret teqlu' yInej", "show_custom_menu_links": "menuDaq ret teqlu' yInej",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "De'wI' bom yIQIj", "hide_libraries": "De'wI' bom yIQIj",
"select_libraries_you_want_to_hide": "De'wI' bom Danej QIj yIwIv.", "select_liraries_you_want_to_hide": "De'wI' bom Danej QIj yIwIv.",
"disable_haptic_feedback": "Qub quvHa' yIQIj", "disable_haptic_feedback": "Qub quvHa' yIQIj",
"default_quality": "wa' luj", "default_quality": "wa' luj",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "naDev {{availableSpace}}%", "device_usage": "naDev {{availableSpace}}%",
"size_used": "{{used}} / {{total}} ram", "size_used": "{{used}} / {{total}} ram",
"delete_all_downloaded_files": "Hoch Qaw' Doch yIQaw'", "delete_all_downloaded_files": "Hoch Qaw' Doch yIQaw'",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "mu'tlhegh", "video": "mu'tlhegh",
"audio": "QoQ", "audio": "QoQ",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "lut 'el ghIq", "client_error": "lut 'el ghIq",
"could_not_create_stream_for_chromecast": "Chromecast tlhol ret qonlaHbe'", "could_not_create_stream_for_chromecast": "Chromecast tlhol ret qonlaHbe'",
"message_from_server": "Ho'Do' veS jach: {{message}}", "message_from_server": "Ho'Do' veS jach: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Anlaşıldı", "got_it": "Anlaşıldı",
"connection_failed": "Bağlantı başarısız", "connection_failed": "Bağlantı başarısız",
"could_not_connect_to_server": "Sunucuya bağlanılamadı. Lütfen URL'yi ve ağ bağlantınızı kontrol edin.", "could_not_connect_to_server": "Sunucuya bağlanılamadı. Lütfen URL'yi ve ağ bağlantınızı kontrol edin.",
"an_unexpected_error_occurred": "Beklenmedik bir hata oluştu", "an_unexpected_error_occured": "Beklenmedik bir hata oluştu",
"change_server": "Sunucu değiştir", "change_server": "Sunucu değiştir",
"invalid_username_or_password": "Geçersiz kullanıcı adı veya şifre", "invalid_username_or_password": "Geçersiz kullanıcı adı veya şifre",
"user_does_not_have_permission_to_log_in": "Kullanıcının giriş yapma izni yok", "user_does_not_have_permission_to_log_in": "Kullanıcının giriş yapma izni yok",
"server_is_taking_too_long_to_respond_try_again_later": "Sunucunun yanıt vermesi çok uzun sürüyor, lütfen daha sonra tekrar deneyin", "server_is_taking_too_long_to_respond_try_again_later": "Sunucunun yanıt vermesi çok uzun sürüyor, lütfen daha sonra tekrar deneyin",
"server_received_too_many_requests_try_again_later": "Sunucu çok fazla istek aldı, lütfen daha sonra tekrar deneyin.", "server_received_too_many_requests_try_again_later": "Sunucu çok fazla istek aldı, lütfen daha sonra tekrar deneyin.",
"there_is_a_server_error": "Sunucu hatası var", "there_is_a_server_error": "Sunucu hatası var",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Beklenmedik bir hata oluştu. Sunucu URL'sini doğru girdiğinizden emin misiniz?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Beklenmedik bir hata oluştu. Sunucu URL'sini doğru girdiğinizden emin misiniz?",
"too_old_server_text": "Desteklenmeyen Jellyfin Sunucu sürümü bulundu.", "too_old_server_text": "Desteklenmeyen Jellyfin Sunucu sürümü bulundu.",
"too_old_server_description": "Lütfen Jellyfin'i en son sürüme güncelleyin." "too_old_server_description": "Lütfen Jellyfin'i en son sürüme güncelleyin."
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Hızlı Bağlantıyı Yetkilendir", "authorize_button": "Hızlı Bağlantıyı Yetkilendir",
"enter_the_quick_connect_code": "Hızlı bağlantı kodunu girin...", "enter_the_quick_connect_code": "Hızlı bağlantı kodunu girin...",
"success": "Başarılı", "success": "Başarılı",
"quick_connect_authorized": "Hızlı Bağlantı Yetkilendirildi", "quick_connect_autorized": "Hızlı Bağlantı Yetkilendirildi",
"error": "Hata", "error": "Hata",
"invalid_code": "Geçersiz kod", "invalid_code": "Geçersiz kod",
"authorize": "Yetkilendir" "authorize": "Yetkilendir"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster", "show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Kütüphaneleri Gizle", "hide_libraries": "Kütüphaneleri Gizle",
"select_libraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.", "select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.",
"disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak", "disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak",
"default_quality": "Varsayılan kalite", "default_quality": "Varsayılan kalite",
"default_playback_speed": "Varsayılan Oynatma Hızı", "default_playback_speed": "Varsayılan Oynatma Hızı",
@@ -384,8 +384,6 @@
"device_usage": "Cihaz {{availableSpace}}%", "device_usage": "Cihaz {{availableSpace}}%",
"size_used": "{{used}} / {{total}} kullanıldı", "size_used": "{{used}} / {{total}} kullanıldı",
"delete_all_downloaded_files": "Tüm indirilen dosyaları sil", "delete_all_downloaded_files": "Tüm indirilen dosyaları sil",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Müzik Ön Belleği", "music_cache_title": "Müzik Ön Belleği",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Müzik Ön Belleğini Temizle", "clear_music_cache": "Müzik Ön Belleğini Temizle",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Seç",
"no_trailer_available": "Fragman mevcut değil", "no_trailer_available": "Fragman mevcut değil",
"video": "Video", "video": "Video",
"audio": "Ses", "audio": "Ses",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "İstemci hatası", "client_error": "İstemci hatası",
"could_not_create_stream_for_chromecast": "Chromecast için yayın oluşturulamadı", "could_not_create_stream_for_chromecast": "Chromecast için yayın oluşturulamadı",
"message_from_server": "Sunucudan mesaj: {{message}}", "message_from_server": "Sunucudan mesaj: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Готово", "got_it": "Готово",
"connection_failed": "Помилка зʼєднання", "connection_failed": "Помилка зʼєднання",
"could_not_connect_to_server": "Неможливо підʼєднатися до серверу. Будь ласка перевірте URL і ваше зʼєднання з мережею", "could_not_connect_to_server": "Неможливо підʼєднатися до серверу. Будь ласка перевірте URL і ваше зʼєднання з мережею",
"an_unexpected_error_occurred": "Сталася несподівана помилка", "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_occurred_did_you_enter_the_correct_url": "Відбулася несподівана помилка. Чи введений URL сервера правильний?", "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"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Авторизуйте Швидке Зʼєднання", "authorize_button": "Авторизуйте Швидке Зʼєднання",
"enter_the_quick_connect_code": "Введіть код для швидкого зʼєднання...", "enter_the_quick_connect_code": "Введіть код для швидкого зʼєднання...",
"success": "Успіх", "success": "Успіх",
"quick_connect_authorized": "Швидке Зʼєднання авторизовано", "quick_connect_autorized": "Швидке Зʼєднання авторизовано",
"error": "Помилка", "error": "Помилка",
"invalid_code": "Не правильний код", "invalid_code": "Не правильний код",
"authorize": "Авторизувати" "authorize": "Авторизувати"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Показати користувацькі посилання меню", "show_custom_menu_links": "Показати користувацькі посилання меню",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Сховати медіатеки", "hide_libraries": "Сховати медіатеки",
"select_libraries_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": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Гаджет {{availableSpace}}%", "device_usage": "Гаджет {{availableSpace}}%",
"size_used": "{{used}} з {{total}} використано", "size_used": "{{used}} з {{total}} використано",
"delete_all_downloaded_files": "Видалити усі завантаженні файли", "delete_all_downloaded_files": "Видалити усі завантаженні файли",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "mpv_player_title": "MPV Player",
"error": "Помилка", "error": "Помилка",
"failed_to_get_stream_url": "Не вдалося отримати URL-адресу потоку", "failed_to_get_stream_url": "Не вдалося отримати URL-адресу потоку",
"an_error_occurred_while_playing_the_video": "Під час відтворення відео сталася помилка. Перевірте журнал в налаштуваннях.", "an_error_occured_while_playing_the_video": "Під час відтворення відео сталася помилка. Перевірте журнал в налаштуваннях.",
"client_error": "Помилка клієнту", "client_error": "Помилка клієнту",
"could_not_create_stream_for_chromecast": "Не вдалося створити потік для Chromecast", "could_not_create_stream_for_chromecast": "Не вдалося створити потік для Chromecast",
"message_from_server": "Повідомлення від серверу: {{message}}", "message_from_server": "Повідомлення від серверу: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Đã hiểu", "got_it": "Đã hiểu",
"connection_failed": "Kết nối thất bại", "connection_failed": "Kết nối thất bại",
"could_not_connect_to_server": "Không thể kết nối tới máy chủ. Vui lòng kiểm tra URL và kết nối mạng.", "could_not_connect_to_server": "Không thể kết nối tới máy chủ. Vui lòng kiểm tra URL và kết nối mạng.",
"an_unexpected_error_occurred": "Đã xảy ra lỗi không mong muốn", "an_unexpected_error_occured": "Đã xảy ra lỗi không mong muốn",
"change_server": "Đổi máy chủ", "change_server": "Đổi máy chủ",
"invalid_username_or_password": "Tên đăng nhập hoặc mật khẩu không đúng", "invalid_username_or_password": "Tên đăng nhập hoặc mật khẩu không đúng",
"user_does_not_have_permission_to_log_in": "Người dùng không có quyền đăng nhập", "user_does_not_have_permission_to_log_in": "Người dùng không có quyền đăng nhập",
"server_is_taking_too_long_to_respond_try_again_later": "Máy chủ phản hồi quá lâu, vui lòng thử lại sau", "server_is_taking_too_long_to_respond_try_again_later": "Máy chủ phản hồi quá lâu, vui lòng thử lại sau",
"server_received_too_many_requests_try_again_later": "Máy chủ nhận quá nhiều yêu cầu, vui lòng thử lại sau.", "server_received_too_many_requests_try_again_later": "Máy chủ nhận quá nhiều yêu cầu, vui lòng thử lại sau.",
"there_is_a_server_error": "Đã xảy ra lỗi ở máy chủ", "there_is_a_server_error": "Đã xảy ra lỗi ở máy chủ",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "Đã xảy ra lỗi không mong muốn. Bạn có chắc đã nhập đúng URL máy chủ?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "Đã xảy ra lỗi không mong muốn. Bạn có chắc đã nhập đúng URL máy chủ?",
"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"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Cho phép Kết nối nhanh", "authorize_button": "Cho phép Kết nối nhanh",
"enter_the_quick_connect_code": "Nhập mã kết nối nhanh...", "enter_the_quick_connect_code": "Nhập mã kết nối nhanh...",
"success": "Thành công", "success": "Thành công",
"quick_connect_authorized": "Kết nối nhanh đã được cho phép", "quick_connect_autorized": "Kết nối nhanh đã được cho phép",
"error": "Lỗi", "error": "Lỗi",
"invalid_code": "Mã không hợp lệ", "invalid_code": "Mã không hợp lệ",
"authorize": "Cho phép" "authorize": "Cho phép"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Hiện liên kết tùy chỉnh", "show_custom_menu_links": "Hiện liên kết tùy chỉnh",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Ẩn thư viện", "hide_libraries": "Ẩn thư viện",
"select_libraries_you_want_to_hide": "Chọn các thư viện muốn ẩn khỏi mục Thư viện và Trang chủ.", "select_liraries_you_want_to_hide": "Chọn các thư viện muốn ẩn khỏi mục Thư viện và Trang chủ.",
"disable_haptic_feedback": "Tắt phản hồi rung", "disable_haptic_feedback": "Tắt phản hồi rung",
"default_quality": "Chất lượng mặc định", "default_quality": "Chất lượng mặc định",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Thiết bị sử dụng {{availableSpace}}%", "device_usage": "Thiết bị sử dụng {{availableSpace}}%",
"size_used": "{{used}} / {{total}} đã dùng", "size_used": "{{used}} / {{total}} đã dùng",
"delete_all_downloaded_files": "Xóa toàn bộ tập tin đã tải", "delete_all_downloaded_files": "Xóa toàn bộ tập tin đã tải",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Âm thanh", "audio": "Âm thanh",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "mpv_player_title": "MPV Player",
"error": "Lỗi", "error": "Lỗi",
"failed_to_get_stream_url": "Không thể lấy URL phát trực tiếp", "failed_to_get_stream_url": "Không thể lấy URL phát trực tiếp",
"an_error_occurred_while_playing_the_video": "Có lỗi khi phát video. Xem nhật ký trong cài đặt.", "an_error_occured_while_playing_the_video": "Có lỗi khi phát video. Xem nhật ký trong cài đặt.",
"client_error": "Lỗi phía máy khách", "client_error": "Lỗi phía máy khách",
"could_not_create_stream_for_chromecast": "Không thể tạo luồng cho Chromecast", "could_not_create_stream_for_chromecast": "Không thể tạo luồng cho Chromecast",
"message_from_server": "Thông báo từ máy chủ: {{message}}", "message_from_server": "Thông báo từ máy chủ: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -16,14 +16,14 @@
"got_it": "Got It", "got_it": "Got It",
"connection_failed": "Connection Failed", "connection_failed": "Connection Failed",
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.", "could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
"an_unexpected_error_occurred": "An unexpected error occurred", "an_unexpected_error_occured": "An Unexpected Error Occurred",
"change_server": "Change Server", "change_server": "Change Server",
"invalid_username_or_password": "Invalid Username or Password", "invalid_username_or_password": "Invalid Username or Password",
"user_does_not_have_permission_to_log_in": "User does not have permission to log in", "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_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later",
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.", "server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
"there_is_a_server_error": "There is a server error", "there_is_a_server_error": "There is a server error",
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
"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"
}, },
@@ -188,7 +188,7 @@
"authorize_button": "Authorize Quick Connect", "authorize_button": "Authorize Quick Connect",
"enter_the_quick_connect_code": "Enter the quick connect code...", "enter_the_quick_connect_code": "Enter the quick connect code...",
"success": "Success", "success": "Success",
"quick_connect_authorized": "Quick Connect authorized", "quick_connect_autorized": "Quick Connect Authorized",
"error": "Error", "error": "Error",
"invalid_code": "Invalid Code", "invalid_code": "Invalid Code",
"authorize": "Authorize" "authorize": "Authorize"
@@ -298,7 +298,7 @@
"show_custom_menu_links": "Show Custom Menu Links", "show_custom_menu_links": "Show Custom Menu Links",
"show_large_home_carousel": "Show Large Home Carousel (beta)", "show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Hide Libraries", "hide_libraries": "Hide Libraries",
"select_libraries_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",
"default_playback_speed": "Default Playback Speed", "default_playback_speed": "Default Playback Speed",
@@ -384,8 +384,6 @@
"device_usage": "Device {{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",
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
"music_cache_title": "Music Cache", "music_cache_title": "Music Cache",
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support", "music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
"clear_music_cache": "Clear Music Cache", "clear_music_cache": "Clear Music Cache",
@@ -486,6 +484,7 @@
}, },
"common": { "common": {
"no_results": "No Results", "no_results": "No Results",
"select": "Select",
"no_trailer_available": "No trailer available", "no_trailer_available": "No trailer available",
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
@@ -600,7 +599,7 @@
"mpv_player_title": "MPV Player", "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_occurred_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.",
"client_error": "Client Error", "client_error": "Client Error",
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast", "could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}", "message_from_server": "Message from Server: {{message}}",
@@ -622,6 +621,7 @@
"using_jellyfin_server": "Using Jellyfin Server", "using_jellyfin_server": "Using Jellyfin Server",
"language": "Language", "language": "Language",
"results": "Results", "results": "Results",
"searching": "Searching...",
"search_failed": "Search failed", "search_failed": "Search failed",
"no_subtitle_provider": "No subtitle provider configured on server", "no_subtitle_provider": "No subtitle provider configured on server",
"no_subtitles_found": "No subtitles found", "no_subtitles_found": "No subtitles found",

View File

@@ -11,6 +11,14 @@ export type TVSubtitleModalState = {
onServerSubtitleDownloaded?: () => void; onServerSubtitleDownloaded?: () => void;
onLocalSubtitleDownloaded?: (path: string) => void; onLocalSubtitleDownloaded?: (path: string) => void;
refreshSubtitleTracks?: () => Promise<Track[]>; refreshSubtitleTracks?: () => Promise<Track[]>;
/**
* Run the selection callback AFTER the modal route is dismissed. Needed when
* `setTrack` navigates (the player's replacePlayer for a burn-in switch),
* which would be swallowed by the still-active modal route. Leave false for
* callers whose selection only updates state (the item detail page), so the
* update runs before dismissal and doesn't yank focus after it returns.
*/
deferApplyUntilDismissed?: boolean;
} | null; } | null;
export const tvSubtitleModalAtom = atom<TVSubtitleModalState>(null); export const tvSubtitleModalAtom = atom<TVSubtitleModalState>(null);

View File

@@ -0,0 +1,485 @@
import { describe, expect, test } from "bun:test";
import type {
MediaStream,
SubtitleDeliveryMethod,
} from "@jellyfin/sdk/lib/generated-client";
import {
applyMpvSubtitleSelection,
compareTracksForMenu,
getExternalSubtitleUrl,
isExternalSubtitle,
type PlayerSubtitleTrack,
resolveSubtitleTrack,
} from "@/utils/jellyfin/subtitleUtils";
// String-enum values as typed literals — avoids a runtime SDK import (see subtitleUtils.ts).
const External = "External" as SubtitleDeliveryMethod;
const Embed = "Embed" as SubtitleDeliveryMethod;
const Encode = "Encode" as SubtitleDeliveryMethod;
const Hls = "Hls" as SubtitleDeliveryMethod;
// --- fixtures --------------------------------------------------------------
const sub = (o: Partial<MediaStream> & { Index: number }): MediaStream =>
({ Type: "Subtitle", ...o }) as MediaStream;
const ext = (Index: number, o: Partial<MediaStream> = {}): MediaStream =>
sub({
Index,
DeliveryMethod: External,
IsExternal: true,
DeliveryUrl: `/sub/${Index}.srt`,
...o,
});
const emb = (Index: number, o: Partial<MediaStream> = {}): MediaStream =>
sub({ Index, DeliveryMethod: Embed, ...o });
const track = (o: PlayerSubtitleTrack): PlayerSubtitleTrack => o;
// Mirror direct-player.tsx online URL builder.
const urlBuilder =
(base: string) =>
(s: MediaStream): string | undefined =>
s.DeliveryUrl ? `${base}${s.DeliveryUrl}` : undefined;
const resolve = (
streams: MediaStream[],
index: number | undefined,
player: PlayerSubtitleTrack[],
getExpectedExternalUrl = urlBuilder("http://srv"),
) =>
resolveSubtitleTrack({
subtitleStreams: streams,
jellyfinSubtitleIndex: index,
playerTracks: player,
getExpectedExternalUrl,
});
// --- tests -----------------------------------------------------------------
describe("isExternalSubtitle", () => {
test("true for External delivery or the IsExternal flag, not a bare DeliveryUrl", () => {
expect(isExternalSubtitle(ext(0))).toBe(true);
expect(isExternalSubtitle(sub({ Index: 1, IsExternal: true }))).toBe(true);
expect(isExternalSubtitle(emb(2))).toBe(false);
// A DeliveryUrl alone (e.g. an Hls-delivered sub) is NOT a sub-added sidecar.
expect(isExternalSubtitle(sub({ Index: 3, DeliveryUrl: "/x.srt" }))).toBe(
false,
);
});
test("a sidecar re-delivered by the server (Hls/Encode) is NOT external", () => {
// IsExternal only wins while no device-specific delivery method is assigned;
// once the server picks Hls (inside the stream) or Encode (burned), the
// track is not a sub-added sidecar and must not use the external path.
expect(
isExternalSubtitle(
sub({ Index: 0, IsExternal: true, DeliveryMethod: Hls }),
),
).toBe(false);
expect(
isExternalSubtitle(
sub({ Index: 0, IsExternal: true, DeliveryMethod: Encode }),
),
).toBe(false);
});
});
describe("resolveSubtitleTrack — disable / notFound", () => {
test("index -1 or undefined disables", () => {
expect(resolve([], -1, [])).toEqual({ kind: "disable" });
expect(resolve([], undefined, [])).toEqual({ kind: "disable" });
});
test("index not present returns notFound", () => {
expect(resolve([emb(0)], 99, [track({ id: 1 })])).toEqual({
kind: "notFound",
});
});
});
describe("resolveSubtitleTrack — hidden embedded (#954)", () => {
// Server hides embedded subs: MediaStreams lists only the 3 externals,
// but mpv still demuxes the 3 embedded from the file → externals get ids 4,5,6.
const streams = [
ext(0, { Language: "por" }),
ext(1, { Language: "eng" }),
ext(2, { Language: "eng", Title: "SDH" }),
];
const player = [
track({ id: 1, external: false, language: "eng", title: "CC" }),
track({ id: 2, external: false, language: "spa" }),
track({ id: 3, external: false, language: "fre" }),
track({ id: 4, external: true, externalFilename: "http://srv/sub/0.srt" }),
track({ id: 5, external: true, externalFilename: "http://srv/sub/1.srt" }),
track({ id: 6, external: true, externalFilename: "http://srv/sub/2.srt" }),
];
test("each external maps to the right player id by filename (not 1,2,3)", () => {
expect(resolve(streams, 0, player)).toEqual({ kind: "select", trackId: 4 });
expect(resolve(streams, 1, player)).toEqual({ kind: "select", trackId: 5 });
expect(resolve(streams, 2, player)).toEqual({ kind: "select", trackId: 6 });
});
test("falls back to external ordinal when filenames are unavailable", () => {
const noNames = player.map((t) =>
t.external ? { ...t, externalFilename: undefined } : t,
);
expect(resolve(streams, 1, noNames)).toEqual({
kind: "select",
trackId: 5,
});
});
});
describe("resolveSubtitleTrack — external/embed reversal (non-hidden)", () => {
// Jellyfin lists externals first; mpv lists embedded first then externals.
const streams = [
ext(0, { Language: "eng" }),
emb(1, { Language: "spa" }),
emb(2, { Language: "fre" }),
];
const player = [
track({ id: 1, external: false, language: "spa" }),
track({ id: 2, external: false, language: "fre" }),
track({ id: 3, external: true, externalFilename: "http://srv/sub/0.srt" }),
];
test("external resolves by filename, embedded by language", () => {
expect(resolve(streams, 0, player)).toEqual({ kind: "select", trackId: 3 });
expect(resolve(streams, 1, player)).toEqual({ kind: "select", trackId: 1 });
expect(resolve(streams, 2, player)).toEqual({ kind: "select", trackId: 2 });
});
});
describe("resolveSubtitleTrack — external without DeliveryUrl (#1763 CodeRabbit)", () => {
// Middle external has no DeliveryUrl → never loaded into the player.
const streams = [
ext(0, { Language: "eng", DeliveryUrl: "/sub/a.srt" }),
sub({ Index: 1, DeliveryMethod: External, IsExternal: true }),
ext(2, { Language: "fre", DeliveryUrl: "/sub/c.srt" }),
];
const player = [
track({ id: 4, external: true, externalFilename: "http://srv/sub/a.srt" }),
track({ id: 5, external: true, externalFilename: "http://srv/sub/c.srt" }),
];
test("loaded externals still map correctly despite the gap", () => {
expect(resolve(streams, 0, player)).toEqual({ kind: "select", trackId: 4 });
expect(resolve(streams, 2, player)).toEqual({ kind: "select", trackId: 5 });
});
test("selecting the unloaded external returns notFound", () => {
expect(resolve(streams, 1, player)).toEqual({ kind: "notFound" });
});
});
describe("resolveSubtitleTrack — embedded matching", () => {
test("unique language match wins even when player order differs (not positional)", () => {
const streams = [emb(0, { Language: "eng" }), emb(1, { Language: "jpn" })];
// Player lists them in the OPPOSITE order — a positional map would mis-pick.
const player = [
track({ id: 1, external: false, language: "jpn" }),
track({ id: 2, external: false, language: "eng" }),
];
expect(resolve(streams, 0, player)).toEqual({ kind: "select", trackId: 2 }); // eng
expect(resolve(streams, 1, player)).toEqual({ kind: "select", trackId: 1 }); // jpn
});
test("same-language tracks with no distinguishing title fall back to ordinal among matches", () => {
const streams = [emb(0, { Language: "eng" }), emb(1, { Language: "eng" })];
// Both eng, no title → identity can't disambiguate → ordinal among matches.
const player = [
track({ id: 5, external: false, language: "eng" }),
track({ id: 6, external: false, language: "eng" }),
];
expect(resolve(streams, 0, player)).toEqual({ kind: "select", trackId: 5 });
expect(resolve(streams, 1, player)).toEqual({ kind: "select", trackId: 6 });
});
test("falls back to embedded ordinal when no language/title info", () => {
const streams = [emb(0), emb(1)];
const player = [
track({ id: 1, external: false }),
track({ id: 2, external: false }),
];
expect(resolve(streams, 1, player)).toEqual({ kind: "select", trackId: 2 });
});
});
describe("compareTracksForMenu — jellyfin-web order", () => {
test("externals sort after embedded despite lower Index", () => {
const sorted = [
ext(0, { Language: "eng" }),
emb(7, { Language: "fra" }),
].sort(compareTracksForMenu);
expect(sorted.map((s) => s.Index)).toEqual([7, 0]);
});
test("forced then default float to the top within a group", () => {
const sorted = [
emb(2, { Language: "eng" }),
emb(1, { Language: "eng", IsDefault: true }),
emb(0, { Language: "eng", IsForced: true }),
].sort(compareTracksForMenu);
expect(sorted.map((s) => s.Index)).toEqual([0, 1, 2]);
});
test("full Okiku order: embedded first, externals last by Index", () => {
const streams = [
ext(0, { Language: "eng" }),
ext(1, { Language: "eng" }),
ext(2, { Language: "fra" }),
ext(3, { Language: "fra" }),
emb(7, { Language: "fra", Title: "French" }),
];
expect([...streams].sort(compareTracksForMenu).map((s) => s.Index)).toEqual(
[7, 0, 1, 2, 3],
);
});
});
describe("resolveSubtitleTrack — same-identity group ordinal (duplicate languages)", () => {
// [jpn, eng, eng] with no titles: the eng group starts at embedded position 1,
// so the group ordinal (not the global one) must drive the pick.
const streams = [
emb(0, { Language: "jpn" }),
emb(1, { Language: "eng" }),
emb(2, { Language: "eng" }),
];
const playerTracks = [
track({ id: 1, language: "jpn" }),
track({ id: 2, language: "eng" }),
track({ id: 3, language: "eng" }),
];
test("first eng selects the first matching player track", () => {
expect(resolve(streams, 1, playerTracks)).toEqual({
kind: "select",
trackId: 2,
});
});
test("second eng selects the second matching player track", () => {
expect(resolve(streams, 2, playerTracks)).toEqual({
kind: "select",
trackId: 3,
});
});
});
describe("resolveSubtitleTrack — server-burned (Encode) streams", () => {
const burned = emb(2, {
DeliveryMethod: Encode,
IsTextSubtitleStream: false,
Codec: "pgssub",
});
test("selecting a burned-in sub returns burnedIn (a stream refresh, not a track)", () => {
expect(resolve([burned, emb(3)], 2, [track({ id: 1 })])).toEqual({
kind: "burnedIn",
});
});
test("burned-in streams do not shift the embedded ordinal", () => {
// Player only demuxes the two untagged text subs; the burned PGS is pixels.
const streams = [burned, emb(3), emb(4)];
const playerTracks = [track({ id: 1 }), track({ id: 2 })];
expect(resolve(streams, 3, playerTracks)).toEqual({
kind: "select",
trackId: 1,
});
expect(resolve([burned, emb(3)], 3, [track({ id: 1 })])).toEqual({
kind: "select",
trackId: 1,
});
});
});
describe("resolveSubtitleTrack — Hls-delivered sidecar goes through the embedded path", () => {
test("resolves by identity against the player's in-stream track", () => {
const hlsSidecar = sub({
Index: 0,
IsExternal: true,
DeliveryMethod: Hls,
DeliveryUrl: "/videos/x/subs/0.vtt",
Language: "fre",
});
const r = resolve([hlsSidecar, emb(1, { Language: "eng" })], 0, [
track({ id: 1, language: "fre" }),
track({ id: 2, language: "eng" }),
]);
expect(r).toEqual({ kind: "select", trackId: 1 });
});
});
describe("applyMpvSubtitleSelection — short-circuits", () => {
test("disable (-1) never reads the player track list", async () => {
let enumerated = false;
let disabled = false;
const r = await applyMpvSubtitleSelection(
{
getSubtitleTracks: async () => {
enumerated = true;
return [];
},
setSubtitleTrack: () => {},
disableSubtitles: () => {
disabled = true;
},
},
{ subtitleStreams: [], jellyfinSubtitleIndex: -1 },
);
expect(r).toEqual({ kind: "disable" });
expect(disabled).toBe(true);
expect(enumerated).toBe(false);
});
test("burned-in target returns without touching the player", async () => {
let enumerated = false;
const r = await applyMpvSubtitleSelection(
{
getSubtitleTracks: async () => {
enumerated = true;
return [];
},
setSubtitleTrack: () => {},
disableSubtitles: () => {},
},
{
subtitleStreams: [
emb(2, { DeliveryMethod: Encode, IsTextSubtitleStream: false }),
],
jellyfinSubtitleIndex: 2,
},
);
expect(r).toEqual({ kind: "burnedIn" });
expect(enumerated).toBe(false);
});
});
describe("getExternalSubtitleUrl — server contract (MediaInfoHelper)", () => {
test("server-relative DeliveryUrl gets the basePath prefix", () => {
expect(
getExternalSubtitleUrl(ext(0, { DeliveryUrl: "/sub/0.srt" }), {
offline: false,
basePath: "http://srv",
}),
).toBe("http://srv/sub/0.srt");
});
test("IsExternalUrl means DeliveryUrl is already absolute — no prefix", () => {
expect(
getExternalSubtitleUrl(
ext(0, {
DeliveryUrl: "https://cdn.example/sub.srt",
IsExternalUrl: true,
}),
{ offline: false, basePath: "http://srv" },
),
).toBe("https://cdn.example/sub.srt");
});
test("offline returns the stored local path as-is", () => {
expect(
getExternalSubtitleUrl(ext(0, { DeliveryUrl: "file:///subs/0.srt" }), {
offline: true,
basePath: "http://srv",
}),
).toBe("file:///subs/0.srt");
});
test("no DeliveryUrl or no basePath online → undefined", () => {
expect(
getExternalSubtitleUrl(sub({ Index: 0, IsExternal: true }), {
offline: false,
basePath: "http://srv",
}),
).toBeUndefined();
expect(
getExternalSubtitleUrl(ext(0), { offline: false, basePath: undefined }),
).toBeUndefined();
});
});
describe("resolveSubtitleTrack — language tag variants (ISO 639-1 / 639-2 B-T / IETF)", () => {
test("Jellyfin 639-2/B matches mpv 639-2/T and 639-1 tags", () => {
// Server says "ger" (639-2/B); muxers can surface "deu" (/T) or "de" (639-1).
const streams = [emb(0, { Language: "ger" }), emb(1, { Language: "fre" })];
const playerT = [
track({ id: 1, language: "deu" }),
track({ id: 2, language: "fra" }),
];
expect(resolve(streams, 0, playerT)).toEqual({
kind: "select",
trackId: 1,
});
expect(resolve(streams, 1, playerT)).toEqual({
kind: "select",
trackId: 2,
});
const player1 = [
track({ id: 1, language: "de" }),
track({ id: 2, language: "fr" }),
];
expect(resolve(streams, 0, player1)).toEqual({
kind: "select",
trackId: 1,
});
});
test("IETF tags reduce to their primary subtag", () => {
const streams = [emb(0, { Language: "eng" }), emb(1, { Language: "spa" })];
const player = [
track({ id: 1, language: "en-US" }),
track({ id: 2, language: "es-419" }),
];
expect(resolve(streams, 0, player)).toEqual({ kind: "select", trackId: 1 });
expect(resolve(streams, 1, player)).toEqual({ kind: "select", trackId: 2 });
});
test("different languages still never match ('ger' vs 'gre')", () => {
const streams = [emb(0, { Language: "ger" })];
const player = [
track({ id: 1, language: "gre" }),
track({ id: 2, language: "ger" }),
];
expect(resolve(streams, 0, player)).toEqual({ kind: "select", trackId: 2 });
});
});
describe("compareTracksForMenu — stable order across play methods (8 Mile live find)", () => {
test("transcode re-delivery (SRT→External, PGS→Encode) must not reshuffle the menu", () => {
// In-file order: srt(1) srt(2) pgs(3) pgs(4), plus a real sidecar ext(0).
const directPlay = [
ext(0, { Language: "eng" }),
emb(1, { Language: "fre" }),
emb(2, { Language: "eng" }),
emb(3, { Language: "fre", IsTextSubtitleStream: false }),
emb(4, { Language: "eng", IsTextSubtitleStream: false }),
];
// Same file while transcoding: server re-delivers embedded text as External
// (extracted) and burns image subs (Encode). IsExternal stays a FILE property.
const transcoding = [
ext(0, { Language: "eng" }),
emb(1, { Language: "fre", DeliveryMethod: External, DeliveryUrl: "/x1" }),
emb(2, { Language: "eng", DeliveryMethod: External, DeliveryUrl: "/x2" }),
emb(3, {
Language: "fre",
IsTextSubtitleStream: false,
DeliveryMethod: Encode,
}),
emb(4, {
Language: "eng",
IsTextSubtitleStream: false,
DeliveryMethod: Encode,
}),
];
const order = (streams: MediaStream[]) =>
[...streams].sort(compareTracksForMenu).map((s) => s.Index);
expect(order(directPlay)).toEqual([1, 2, 3, 4, 0]);
expect(order(transcoding)).toEqual(order(directPlay));
});
});

View File

@@ -1,91 +1,584 @@
/** /**
* Subtitle utility functions for mapping between Jellyfin and MPV track indices. * Subtitle utilities: resolve a Jellyfin subtitle stream to the right track in
* the *player's real track list* by identity — never by positional counting.
* *
* Jellyfin uses server-side indices (e.g., 3, 4, 5 for subtitles in MediaStreams). * Why: Jellyfin renumbers MediaStreams (externals first); the player enumerates
* MPV uses its own track IDs starting from 1, only counting tracks loaded into MPV. * embedded-from-container first and externals (`sub-add`) last; and a library that
* hides embedded subs drops them from MediaStreams while the player still demuxes
* them from the file. Positional Index→id mapping therefore mis-selects (e.g.
* picking Spanish shows English). See {@link resolveSubtitleTrack}.
* *
* Image-based subtitles (PGS, VOBSUB) during transcoding are burned into the video * Image-based subtitles (PGS, VOBSUB) during transcoding are burned into the video
* and NOT available in MPV's track list. * and absent from the player's track list.
*/ */
import { import type {
type MediaSourceInfo, MediaSourceInfo,
type MediaStream, MediaStream,
SubtitleDeliveryMethod, SubtitleDeliveryMethod,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
// "External" is the value of SubtitleDeliveryMethod.External. Compared as a typed
// literal so this util needs no *runtime* import of the SDK barrel — which pulls in
// the axios-dependent `/api` modules and breaks unit tests under `bun test`.
const EXTERNAL_DELIVERY = "External" as SubtitleDeliveryMethod;
const ENCODE_DELIVERY = "Encode" as SubtitleDeliveryMethod;
/** Check if subtitle is image-based (PGS, VOBSUB, etc.) */ /** Check if subtitle is image-based (PGS, VOBSUB, etc.) */
export const isImageBasedSubtitle = (sub: MediaStream): boolean => export const isImageBasedSubtitle = (sub: MediaStream): boolean =>
sub.IsTextSubtitleStream === false; sub.IsTextSubtitleStream === false;
/** /**
* Determine if a subtitle will be available in MPV's track list. * Burned into the video by the server (`DeliveryMethod === Encode`, e.g. image
* * subs while transcoding, or sidecar formats no profile can deliver). Never a
* A subtitle is in MPV if: * selectable player track — switching to/away requires a stream refresh.
* - Delivery is Embed/Hls/External AND not an image-based sub during transcode
*/ */
export const isSubtitleInMpv = ( export const isBurnedInSubtitle = (sub: MediaStream): boolean =>
sub: MediaStream, sub.DeliveryMethod === ENCODE_DELIVERY;
isTranscoding: boolean,
): boolean => {
// During transcoding, image-based subs are burned in, not in MPV
if (isTranscoding && isImageBasedSubtitle(sub)) {
return false;
}
// Embed/Hls/External methods mean the sub is loaded into MPV /**
return ( * A Jellyfin subtitle stream is "external" when the server delivers it as a
sub.DeliveryMethod === SubtitleDeliveryMethod.Embed || * sub-added sidecar — i.e. `DeliveryMethod === External` (or the `IsExternal`
sub.DeliveryMethod === SubtitleDeliveryMethod.Hls || * flag before a device-specific delivery method is assigned).
sub.DeliveryMethod === SubtitleDeliveryMethod.External *
); * Deliberately NOT keyed on `DeliveryUrl`: an Hls-delivered sub also carries a
* `DeliveryUrl` but lives inside the player's track list (not `sub-add`-ed), so
* it must resolve through the embedded path. Keeping this in lockstep with the
* load sites (which only `sub-add` `DeliveryMethod === External`) and with the
* menu comparator below avoids a sub being sorted as embedded yet resolved as
* external (→ `notFound`).
*/
export const isExternalSubtitle = (sub: MediaStream): boolean =>
sub.DeliveryMethod === EXTERNAL_DELIVERY ||
(sub.DeliveryMethod == null && sub.IsExternal === true);
/**
* The exact URL/path an external sub is (or would be) loaded into the player
* with. Single source of truth for BOTH the load site (`videoSource`
* externalSubtitles) and identity matching (`getExpectedExternalUrl`) — the
* filename match only works while the two stay byte-identical.
*
* Server contract (MediaInfoHelper.SetDeviceSpecificSubtitleInfo): DeliveryUrl
* is server-relative (`/Videos/...`, may carry `?ApiKey=`) UNLESS
* `IsExternalUrl` is set — then it is already an absolute URL and must not be
* prefixed. Offline: DeliveryUrl holds the local file path.
*/
export const getExternalSubtitleUrl = (
sub: MediaStream,
opts: { offline: boolean; basePath?: string | null },
): string | undefined => {
if (!sub.DeliveryUrl) return undefined;
if (opts.offline || sub.IsExternalUrl) return sub.DeliveryUrl;
return opts.basePath ? `${opts.basePath}${sub.DeliveryUrl}` : undefined;
}; };
/** /**
* Calculate the MPV track ID for a given Jellyfin subtitle index. * Order subtitle MediaStreams for the selection menu exactly like jellyfin-web's
* `itemHelper.sortTracks`: in-container tracks first then external, and within
* each group forced first, then default, then `Index` ascending. Callers prepend
* their own "None/Off" entry separately.
* *
* MPV track IDs are 1-based and only count subtitles that are actually in MPV. * The Jellyfin server inserts external (sidecar) streams at the FRONT of
* We iterate through all subtitles, counting only those in MPV, until we find * `MediaStreams` (low indices), so raw Index order shows externals first — this
* the one matching the Jellyfin index. * comparator flips that to match web (externals last). Uses the raw `IsExternal`
* * flag exactly like web's `sortTracks` (itemHelper.js): it is a property of the
* @param mediaSource - The media source containing subtitle streams * FILE, so the menu order stays identical between direct play and transcode —
* @param jellyfinSubtitleIndex - The Jellyfin server-side subtitle index (-1 = disabled) * delivery-based grouping would reshuffle entries when the server re-delivers
* @param isTranscoding - Whether the stream is being transcoded * extracted text subs as External and burns image subs (Encode). Ordering is
* @returns MPV track ID (1-based), or -1 if disabled, or undefined if not in MPV * purely cosmetic; selection resolves by `Index` identity regardless.
*/ */
export const getMpvSubtitleId = ( export const compareTracksForMenu = (a: MediaStream, b: MediaStream): number =>
mediaSource: MediaSourceInfo | null | undefined, Number(a.IsExternal ?? false) - Number(b.IsExternal ?? false) ||
jellyfinSubtitleIndex: number | undefined, Number(b.IsForced ?? false) - Number(a.IsForced ?? false) ||
isTranscoding: boolean, Number(b.IsDefault ?? false) - Number(a.IsDefault ?? false) ||
): number | undefined => { // Missing Index sorts to the end (not 0, which would float it to the top and
// -1 or undefined means disabled // collide with a real Index 0).
(a.Index ?? Number.MAX_SAFE_INTEGER) - (b.Index ?? Number.MAX_SAFE_INTEGER);
/**
* Identity of a subtitle track as reported by the *player's real track list*
* (mpv `track-list`, or a Cast media-track list). Player-agnostic on purpose so
* the same resolver can drive the mpv player today and the Chromecast backend later.
*/
export type PlayerSubtitleTrack = {
/** Player-side id used to actually select the track (mpv `sid`, cast trackId). */
id: number;
/** True if loaded from a separate file (mpv `external`). */
external?: boolean;
/** For external tracks: the exact URL/path it was loaded from (mpv `external-filename`). */
externalFilename?: string;
language?: string;
title?: string;
codec?: string;
};
export type SubtitleSelection =
| { kind: "select"; trackId: number }
| { kind: "disable" }
/** Target is server-burned (Encode) — only a stream refresh can show/hide it. */
| { kind: "burnedIn" }
| { kind: "notFound" };
/** Decode percent-encoding and strip a leading `file://` scheme for tolerant comparison. */
const normalizeUrl = (url: string): string => {
let u = url;
try {
u = decodeURIComponent(u);
} catch {
// not decodable — compare raw
}
return u.replace(/^file:\/\//, "");
};
const externalFilenameMatches = (
trackFilename: string | undefined,
expectedUrl: string | undefined,
): boolean => {
if (!trackFilename || !expectedUrl) return false;
const a = normalizeUrl(trackFilename);
const b = normalizeUrl(expectedUrl);
return a === b || a.endsWith(b) || b.endsWith(a);
};
const eq = (a?: string | null, b?: string | null): boolean =>
!!a && !!b && a.toLowerCase() === b.toLowerCase();
/**
* ISO 639-1 (2-letter) and ISO 639-2/T tags mapped to their ISO 639-2/B form,
* so tags from different muxers/servers compare equal ("de"/"deu"/"ger" → "ger").
* Jellyfin normalizes probe results to 639-2/B, but mkv `LanguageIETF` tags
* ("en-US") and some muxers' 639-1 or /T tags leak through to mpv's `lang`.
*/
const LANG_CANONICAL: Record<string, string> = {
// ISO 639-1 → 639-2/B
aa: "aar",
ab: "abk",
ae: "ave",
af: "afr",
ak: "aka",
am: "amh",
an: "arg",
ar: "ara",
as: "asm",
av: "ava",
ay: "aym",
az: "aze",
ba: "bak",
be: "bel",
bg: "bul",
bh: "bih",
bi: "bis",
bm: "bam",
bn: "ben",
bo: "tib",
br: "bre",
bs: "bos",
ca: "cat",
ce: "che",
ch: "cha",
co: "cos",
cr: "cre",
cs: "cze",
cu: "chu",
cv: "chv",
cy: "wel",
da: "dan",
de: "ger",
dv: "div",
dz: "dzo",
ee: "ewe",
el: "gre",
en: "eng",
eo: "epo",
es: "spa",
et: "est",
eu: "baq",
fa: "per",
ff: "ful",
fi: "fin",
fj: "fij",
fo: "fao",
fr: "fre",
fy: "fry",
ga: "gle",
gd: "gla",
gl: "glg",
gn: "grn",
gu: "guj",
gv: "glv",
ha: "hau",
he: "heb",
hi: "hin",
ho: "hmo",
hr: "hrv",
ht: "hat",
hu: "hun",
hy: "arm",
hz: "her",
ia: "ina",
id: "ind",
ie: "ile",
ig: "ibo",
ii: "iii",
ik: "ipk",
io: "ido",
is: "ice",
it: "ita",
iu: "iku",
ja: "jpn",
jv: "jav",
ka: "geo",
kg: "kon",
ki: "kik",
kj: "kua",
kk: "kaz",
kl: "kal",
km: "khm",
kn: "kan",
ko: "kor",
kr: "kau",
ks: "kas",
ku: "kur",
kv: "kom",
kw: "cor",
ky: "kir",
la: "lat",
lb: "ltz",
lg: "lug",
li: "lim",
ln: "lin",
lo: "lao",
lt: "lit",
lu: "lub",
lv: "lav",
mg: "mlg",
mh: "mah",
mi: "mao",
mk: "mac",
ml: "mal",
mn: "mon",
mr: "mar",
ms: "may",
mt: "mlt",
my: "bur",
na: "nau",
nb: "nob",
nd: "nde",
ne: "nep",
ng: "ndo",
nl: "dut",
nn: "nno",
no: "nor",
nr: "nbl",
nv: "nav",
ny: "nya",
oc: "oci",
oj: "oji",
om: "orm",
or: "ori",
os: "oss",
pa: "pan",
pi: "pli",
pl: "pol",
ps: "pus",
pt: "por",
qu: "que",
rm: "roh",
rn: "run",
ro: "rum",
ru: "rus",
rw: "kin",
sa: "san",
sc: "srd",
sd: "snd",
se: "sme",
sg: "sag",
si: "sin",
sk: "slo",
sl: "slv",
sm: "smo",
sn: "sna",
so: "som",
sq: "alb",
sr: "srp",
ss: "ssw",
st: "sot",
su: "sun",
sv: "swe",
sw: "swa",
ta: "tam",
te: "tel",
tg: "tgk",
th: "tha",
ti: "tir",
tk: "tuk",
tl: "tgl",
tn: "tsn",
to: "ton",
tr: "tur",
ts: "tso",
tt: "tat",
tw: "twi",
ty: "tah",
ug: "uig",
uk: "ukr",
ur: "urd",
uz: "uzb",
ve: "ven",
vi: "vie",
vo: "vol",
wa: "wln",
wo: "wol",
xh: "xho",
yi: "yid",
yo: "yor",
za: "zha",
zh: "chi",
zu: "zul",
// ISO 639-2/T → /B (the 20 languages with two distinct 3-letter codes)
bod: "tib",
ces: "cze",
cym: "wel",
deu: "ger",
ell: "gre",
eus: "baq",
fas: "per",
fra: "fre",
hye: "arm",
isl: "ice",
kat: "geo",
mkd: "mac",
mri: "mao",
msa: "may",
mya: "bur",
nld: "dut",
ron: "rum",
slk: "slo",
sqi: "alb",
zho: "chi",
};
/** Canonicalize a language tag: lowercase, primary subtag ("en-US" → "en"), 639-2/B. */
const canonicalLang = (raw: string): string => {
const primary = raw.trim().toLowerCase().split("-")[0];
return LANG_CANONICAL[primary] ?? primary;
};
/** Language-tag comparison across ISO 639-1 / 639-2 B/T / IETF variants. */
const langEq = (a?: string | null, b?: string | null): boolean =>
!!a && !!b && canonicalLang(a) === canonicalLang(b);
/** Match an embedded player track to a Jellyfin stream by language/title (codec-agnostic). */
const embeddedIdentityMatches = (
track: PlayerSubtitleTrack,
stream: MediaStream,
): boolean => {
if (langEq(track.language, stream.Language)) {
// When both carry a title it must agree; otherwise language alone is enough.
if (track.title && stream.Title) return eq(track.title, stream.Title);
return true;
}
// No language on one side — fall back to a title match.
if (!track.language || !stream.Language) return eq(track.title, stream.Title);
return false;
};
/**
* Resolve the player track id for a given Jellyfin subtitle index by matching
* against the player's REAL track list (identity), never by positional counting.
*
* Why identity, not position: Jellyfin renumbers `MediaStreams` (externals first)
* while the player enumerates embedded-from-container first and externals
* (`sub-add`) last; and when a library hides embedded subs they vanish from
* `MediaStreams` but still physically exist in the file the player demuxes.
* Positional Index→id mapping therefore mis-selects (e.g. picking Spanish shows
* English — issues #954/#1690/#618/#1467/#976/#1451).
*
* Strategy:
* - disabled (-1/undefined) → `disable`
* - external Jellyfin sub → match the player track by `externalFilename`
* (exact identity, immune to hidden-embedded shifts); fall back to the
* ordinal among *loadable* externals (Swiftfin: externals are the list tail).
* - embedded Jellyfin sub → match by language/title among non-external tracks;
* fall back to the embedded ordinal (container order aligns on both sides).
*
* Player-agnostic: pass any player's track list + a URL builder, so the mpv
* player and (later) the Chromecast backend share one source of truth.
*/
export const resolveSubtitleTrack = (params: {
subtitleStreams: MediaStream[] | undefined;
jellyfinSubtitleIndex: number | undefined;
playerTracks: PlayerSubtitleTrack[];
/** Build the exact URL/path an external Jellyfin sub was loaded into the player with. */
getExpectedExternalUrl?: (sub: MediaStream) => string | undefined;
}): SubtitleSelection => {
const { jellyfinSubtitleIndex, playerTracks, getExpectedExternalUrl } =
params;
const subtitleStreams = params.subtitleStreams ?? [];
if (jellyfinSubtitleIndex === undefined || jellyfinSubtitleIndex === -1) { if (jellyfinSubtitleIndex === undefined || jellyfinSubtitleIndex === -1) {
return -1; return { kind: "disable" };
} }
const allSubs = const target = subtitleStreams.find((s) => s.Index === jellyfinSubtitleIndex);
mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || []; if (!target) return { kind: "notFound" };
// Find the subtitle with the matching Jellyfin index // Server-burned subs are pixels, not tracks — signal the caller to refresh
const targetSub = allSubs.find((s) => s.Index === jellyfinSubtitleIndex); // the stream instead of hunting for a track that cannot exist.
if (isBurnedInSubtitle(target)) return { kind: "burnedIn" };
// If the target subtitle isn't in MPV (e.g., image-based during transcode), return undefined if (isExternalSubtitle(target)) {
if (!targetSub || !isSubtitleInMpv(targetSub, isTranscoding)) { const playerExternals = playerTracks.filter((t) => t.external === true);
return undefined;
// 1) Exact identity by external filename — robust against hidden-embedded offset.
const expectedUrl = getExpectedExternalUrl?.(target);
const byName = playerExternals.find((t) =>
externalFilenameMatches(t.externalFilename, expectedUrl),
);
if (byName) return { kind: "select", trackId: byName.id };
// 2) Fallback: externals are appended in MediaStreams order → ordinal among
// *loadable* externals (those actually added to the player) stays in lockstep
// with the player's external list, skipping ones with no DeliveryUrl (#1763).
const externalStreams = subtitleStreams.filter(isExternalSubtitle);
const loadableExternals = getExpectedExternalUrl
? externalStreams.filter((s) => getExpectedExternalUrl(s))
: externalStreams;
const ordinal = loadableExternals.findIndex(
(s) => s.Index === jellyfinSubtitleIndex,
);
if (ordinal >= 0 && ordinal < playerExternals.length) {
return { kind: "select", trackId: playerExternals[ordinal].id };
}
return { kind: "notFound" };
} }
// Count MPV track position (1-based) // Embedded / in-container subtitle. Burned-in (Encode) streams are excluded:
let mpvIndex = 0; // they are baked into the video and never appear in the player's track list,
for (const sub of allSubs) { // so counting them would shift every ordinal below.
if (isSubtitleInMpv(sub, isTranscoding)) { const embeddedStreams = subtitleStreams.filter(
mpvIndex++; (s) => !isExternalSubtitle(s) && !isBurnedInSubtitle(s),
if (sub.Index === jellyfinSubtitleIndex) { );
return mpvIndex; const playerEmbedded = playerTracks.filter((t) => t.external !== true);
}
// 1) Identity by language/title (unique match wins).
const identityMatches = playerEmbedded.filter((t) =>
embeddedIdentityMatches(t, target),
);
if (identityMatches.length === 1) {
return { kind: "select", trackId: identityMatches[0].id };
}
// 2) Multiple same-identity tracks: ordinal within the same-identity GROUP —
// the k-th matching stream corresponds to the k-th matching player track
// (container order is preserved on both sides, filter preserves order).
// The group ordinal, not the global one: with [jpn, eng, eng] the first
// eng is global position 1 but group position 0.
if (identityMatches.length > 1) {
const groupStreams = embeddedStreams.filter((s) =>
identityMatches.some((t) => embeddedIdentityMatches(t, s)),
);
const groupOrdinal = groupStreams.findIndex(
(s) => s.Index === jellyfinSubtitleIndex,
);
if (groupOrdinal >= 0) {
const idx = Math.min(groupOrdinal, identityMatches.length - 1);
return { kind: "select", trackId: identityMatches[idx].id };
} }
} }
return undefined; // 3) Fallback: embedded order is container order on both sides → ordinal.
const ordinal = embeddedStreams.findIndex(
(s) => s.Index === jellyfinSubtitleIndex,
);
if (ordinal >= 0 && ordinal < playerEmbedded.length) {
return { kind: "select", trackId: playerEmbedded[ordinal].id };
}
return { kind: "notFound" };
};
/**
* A subtitle track as reported by a concrete player's track-list API
* (mpv `getSubtitleTracks`, or a Cast track list). `lang` mirrors mpv's field name.
*/
export type PlayerSubtitleTrackRaw = {
id: number;
lang?: string;
title?: string;
codec?: string;
external?: boolean;
externalFilename?: string;
};
/**
* Minimal player surface needed to select a subtitle. Satisfied structurally by
* the mpv player ref and (later) implementable by the Chromecast backend.
*/
export interface SubtitleSelectablePlayer {
getSubtitleTracks: () => Promise<PlayerSubtitleTrackRaw[] | null | undefined>;
setSubtitleTrack: (trackId: number) => unknown;
disableSubtitles: () => unknown;
}
/**
* Read the player's real track list, resolve the Jellyfin subtitle index by
* identity ({@link resolveSubtitleTrack}) and apply the result. Single entry point
* for both the mobile controls and the player screen, so selection stays
* consistent everywhere. Returns the resolution for callers that want to react.
*/
export const applyMpvSubtitleSelection = async (
player: SubtitleSelectablePlayer | null | undefined,
params: {
subtitleStreams: MediaStream[] | undefined;
jellyfinSubtitleIndex: number;
/** Build the exact URL/path an external sub was loaded into the player with. */
getExpectedExternalUrl?: (sub: MediaStream) => string | undefined;
},
): Promise<SubtitleSelection> => {
if (!player) return { kind: "notFound" };
// Called fire-and-forget (`void applyMpvSubtitleSelection(...)`), so any native
// rejection from getSubtitleTracks/setSubtitleTrack/disableSubtitles must be
// swallowed here instead of escaping as an unhandled promise rejection.
try {
// Short-circuit the outcomes that don't need the player's track list, so
// the common subtitles-off case skips a full native enumeration.
if (params.jellyfinSubtitleIndex === -1) {
await player.disableSubtitles();
return { kind: "disable" };
}
const burnTarget = params.subtitleStreams?.find(
(s) => s.Index === params.jellyfinSubtitleIndex,
);
if (burnTarget && isBurnedInSubtitle(burnTarget)) {
return { kind: "burnedIn" };
}
const tracks = (await player.getSubtitleTracks()) ?? [];
const selection = resolveSubtitleTrack({
subtitleStreams: params.subtitleStreams,
jellyfinSubtitleIndex: params.jellyfinSubtitleIndex,
playerTracks: tracks.map((t) => ({
id: t.id,
external: t.external,
externalFilename: t.externalFilename,
language: t.lang,
title: t.title,
codec: t.codec,
})),
getExpectedExternalUrl: params.getExpectedExternalUrl,
});
if (selection.kind === "select") {
await player.setSubtitleTrack(selection.trackId);
} else if (selection.kind === "disable") {
await player.disableSubtitles();
}
// notFound → leave current selection (e.g. image subs burned in while transcoding)
return selection;
} catch {
return { kind: "notFound" };
}
}; };
/** /**