forked from Ninjalama/streamyfin_mirror
Compare commits
1 Commits
v0.28.0
...
fix/playba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
187f504d86 |
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -9,6 +9,7 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"prettier.printWidth": 120,
|
||||
"[swift]": {
|
||||
"editor.defaultFormatter": "sswg.swift-lang"
|
||||
}
|
||||
|
||||
@@ -12,40 +12,26 @@ import {
|
||||
ProgressUpdatePayload,
|
||||
VlcPlayerViewRef,
|
||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||
// import { useDownload } from "@/providers/DownloadProvider";
|
||||
const downloadProvider = !Platform.isTV
|
||||
? require("@/providers/DownloadProvider")
|
||||
: null;
|
||||
const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null;
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import { writeToLog } from "@/utils/log";
|
||||
import native from "@/utils/profiles/native";
|
||||
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
||||
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
||||
import {
|
||||
getPlaystateApi,
|
||||
getUserLibraryApi,
|
||||
} from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getPlaystateApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { useGlobalSearchParams, useNavigation } from "expo-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import React, {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { Alert, View, AppState, AppStateStatus, Platform } from "react-native";
|
||||
import React, { useCallback, useMemo, useRef, useState, useEffect } from "react";
|
||||
import { Alert, View, Platform } from "react-native";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { BaseItemDto, MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||
|
||||
export default function page() {
|
||||
console.log("Direct Player");
|
||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||
const user = useAtomValue(userAtom);
|
||||
const api = useAtomValue(apiAtom);
|
||||
@@ -93,111 +79,101 @@ export default function page() {
|
||||
offline: string;
|
||||
}>();
|
||||
const [settings] = useSettings();
|
||||
const insets = useSafeAreaInsets();
|
||||
const offline = offlineStr === "true";
|
||||
|
||||
const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined;
|
||||
const subtitleIndex = subtitleIndexStr ? parseInt(subtitleIndexStr, 10) : -1;
|
||||
const bitrateValue = bitrateValueStr
|
||||
? parseInt(bitrateValueStr, 10)
|
||||
: BITRATES[0].value;
|
||||
const bitrateValue = bitrateValueStr ? parseInt(bitrateValueStr, 10) : BITRATES[0].value;
|
||||
|
||||
const {
|
||||
data: item,
|
||||
isLoading: isLoadingItem,
|
||||
isError: isErrorItem,
|
||||
} = useQuery({
|
||||
queryKey: ["item", itemId],
|
||||
queryFn: async () => {
|
||||
if (offline && !Platform.isTV) {
|
||||
const item = await getDownloadedItem.getDownloadedItem(itemId);
|
||||
if (item) return item.item;
|
||||
}
|
||||
|
||||
const res = await getUserLibraryApi(api!).getItem({
|
||||
itemId,
|
||||
userId: user?.Id,
|
||||
});
|
||||
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!itemId,
|
||||
staleTime: 0,
|
||||
const [item, setItem] = useState<BaseItemDto | null>(null);
|
||||
const [itemStatus, setItemStatus] = useState({
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
const [stream, setStream] = useState<{
|
||||
mediaSource: MediaSourceInfo;
|
||||
url: string;
|
||||
sessionId: string | undefined;
|
||||
} | null>(null);
|
||||
const [isLoadingStream, setIsLoadingStream] = useState(true);
|
||||
const [isErrorStream, setIsErrorStream] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStream = async () => {
|
||||
setIsLoadingStream(true);
|
||||
setIsErrorStream(false);
|
||||
|
||||
const fetchItemData = async () => {
|
||||
setItemStatus({ isLoading: true, isError: false });
|
||||
try {
|
||||
let fetchedItem: BaseItemDto | null = null;
|
||||
if (offline && !Platform.isTV) {
|
||||
const data = await getDownloadedItem.getDownloadedItem(itemId);
|
||||
if (!data?.mediaSource) {
|
||||
setStream(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = await getDownloadedFileUrl(data.item.Id!);
|
||||
|
||||
if (item) {
|
||||
setStream({
|
||||
mediaSource: data.mediaSource as MediaSourceInfo,
|
||||
url,
|
||||
sessionId: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (data) fetchedItem = data.item as BaseItemDto;
|
||||
} else {
|
||||
const res = await getUserLibraryApi(api!).getItem({
|
||||
itemId,
|
||||
userId: user?.Id,
|
||||
});
|
||||
fetchedItem = res.data;
|
||||
}
|
||||
|
||||
const res = await getStreamUrl({
|
||||
api,
|
||||
item,
|
||||
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
||||
userId: user?.Id,
|
||||
audioStreamIndex: audioIndex,
|
||||
maxStreamingBitrate: bitrateValue,
|
||||
mediaSourceId: mediaSourceId,
|
||||
subtitleStreamIndex: subtitleIndex,
|
||||
deviceProfile: native,
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
setStream(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const { mediaSource, sessionId, url } = res;
|
||||
|
||||
if (!sessionId || !mediaSource || !url) {
|
||||
Alert.alert(t("player.error"), t("player.failed_to_get_stream_url"));
|
||||
setStream(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setStream({
|
||||
mediaSource,
|
||||
sessionId,
|
||||
url,
|
||||
});
|
||||
setItem(fetchedItem);
|
||||
} catch (error) {
|
||||
console.error("Error fetching stream:", error);
|
||||
setIsErrorStream(true);
|
||||
setStream(null);
|
||||
console.error("Failed to fetch item:", error);
|
||||
setItemStatus({ isLoading: false, isError: true });
|
||||
} finally {
|
||||
setIsLoadingStream(false);
|
||||
setItemStatus({ isLoading: false, isError: false });
|
||||
}
|
||||
};
|
||||
|
||||
fetchStream();
|
||||
}, [itemId, mediaSourceId]);
|
||||
if (itemId) {
|
||||
fetchItemData();
|
||||
}
|
||||
}, [itemId, offline, api, user?.Id]);
|
||||
|
||||
interface Stream {
|
||||
mediaSource: MediaSourceInfo;
|
||||
sessionId: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const [stream, setStream] = useState<Stream | null>(null);
|
||||
const [streamStatus, setStreamStatus] = useState({
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStreamData = async () => {
|
||||
try {
|
||||
let result: Stream | null = null;
|
||||
if (offline && !Platform.isTV) {
|
||||
const data = await getDownloadedItem.getDownloadedItem(itemId);
|
||||
if (!data?.mediaSource) return;
|
||||
const url = await getDownloadedFileUrl(data.item.Id!);
|
||||
if (item) {
|
||||
result = { mediaSource: data.mediaSource, sessionId: "", url };
|
||||
}
|
||||
} else {
|
||||
const res = await getStreamUrl({
|
||||
api,
|
||||
item,
|
||||
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
||||
userId: user?.Id,
|
||||
audioStreamIndex: audioIndex,
|
||||
maxStreamingBitrate: bitrateValue,
|
||||
mediaSourceId: mediaSourceId,
|
||||
subtitleStreamIndex: subtitleIndex,
|
||||
deviceProfile: native,
|
||||
});
|
||||
if (!res) return;
|
||||
const { mediaSource, sessionId, url } = res;
|
||||
if (!sessionId || !mediaSource || !url) {
|
||||
Alert.alert(t("player.error"), t("player.failed_to_get_stream_url"));
|
||||
return;
|
||||
}
|
||||
result = { mediaSource, sessionId, url };
|
||||
}
|
||||
setStream(result);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch stream:", error);
|
||||
setStreamStatus({ isLoading: false, isError: true });
|
||||
} finally {
|
||||
setStreamStatus({ isLoading: false, isError: false });
|
||||
}
|
||||
};
|
||||
fetchStreamData();
|
||||
}, [itemId, mediaSourceId, bitrateValue, api, item, user?.Id]);
|
||||
|
||||
const togglePlay = useCallback(async () => {
|
||||
if (!api) return;
|
||||
@@ -208,37 +184,11 @@ export default function page() {
|
||||
} else {
|
||||
videoRef.current?.play();
|
||||
}
|
||||
|
||||
if (!offline && stream) {
|
||||
await getPlaystateApi(api).onPlaybackProgress({
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: msToTicks(progress.get()),
|
||||
isPaused: !isPlaying,
|
||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
isPlaying,
|
||||
api,
|
||||
item,
|
||||
stream,
|
||||
videoRef,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
mediaSourceId,
|
||||
offline,
|
||||
progress,
|
||||
]);
|
||||
}, [isPlaying, api, item, stream, videoRef, audioIndex, subtitleIndex, mediaSourceId, offline, progress]);
|
||||
|
||||
const reportPlaybackStopped = useCallback(async () => {
|
||||
if (offline) return;
|
||||
|
||||
const currentTimeInTicks = msToTicks(progress.get());
|
||||
|
||||
await getPlaystateApi(api!).onPlaybackStopped({
|
||||
itemId: item?.Id!,
|
||||
mediaSourceId: mediaSourceId,
|
||||
@@ -255,12 +205,18 @@ export default function page() {
|
||||
videoRef.current?.stop();
|
||||
}, [videoRef, reportPlaybackStopped]);
|
||||
|
||||
useEffect(() => {
|
||||
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
||||
return () => {
|
||||
beforeRemoveListener();
|
||||
};
|
||||
}, [navigation, stop]);
|
||||
|
||||
const onProgress = useCallback(
|
||||
async (data: ProgressUpdatePayload) => {
|
||||
if (isSeeking.get() || isPlaybackStopped) return;
|
||||
|
||||
const { currentTime } = data.nativeEvent;
|
||||
|
||||
if (isBuffering) {
|
||||
setIsBuffering(false);
|
||||
}
|
||||
@@ -284,9 +240,57 @@ export default function page() {
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
},
|
||||
[item?.Id, isSeeking, api, isPlaybackStopped, audioIndex, subtitleIndex]
|
||||
[item?.Id, audioIndex, subtitleIndex, mediaSourceId, isPlaying, stream, isSeeking, isPlaybackStopped, isBuffering]
|
||||
);
|
||||
|
||||
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
||||
const { pipStarted } = e.nativeEvent;
|
||||
setIsPipStarted(pipStarted);
|
||||
}, []);
|
||||
|
||||
const changePlaybackState = useCallback(
|
||||
async (isPlaying: boolean) => {
|
||||
if (!api || offline || !stream) return;
|
||||
await getPlaystateApi(api).onPlaybackProgress({
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: msToTicks(progress.get()),
|
||||
isPaused: !isPlaying,
|
||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
},
|
||||
[api, offline, stream, item?.Id, audioIndex, subtitleIndex, mediaSourceId, progress]
|
||||
);
|
||||
|
||||
const startPosition = useMemo(() => {
|
||||
if (offline) return 0;
|
||||
return item?.UserData?.PlaybackPositionTicks ? ticksToSeconds(item.UserData.PlaybackPositionTicks) : 0;
|
||||
}, [item]);
|
||||
|
||||
const reportPlaybackStart = useCallback(async () => {
|
||||
if (offline || !stream) return;
|
||||
await getPlaystateApi(api!).onPlaybackStart({
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
playMethod: stream.url?.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream?.sessionId ? stream?.sessionId : undefined,
|
||||
});
|
||||
hasReportedRef.current = true;
|
||||
}, [api, item, stream]);
|
||||
|
||||
const hasReportedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (stream && !hasReportedRef.current) {
|
||||
reportPlaybackStart();
|
||||
hasReportedRef.current = true; // Mark as reported
|
||||
}
|
||||
}, [stream]);
|
||||
|
||||
useWebSocket({
|
||||
isPlaying: isPlaying,
|
||||
togglePlay: togglePlay,
|
||||
@@ -294,75 +298,41 @@ export default function page() {
|
||||
offline,
|
||||
});
|
||||
|
||||
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
||||
const { pipStarted } = e.nativeEvent;
|
||||
setIsPipStarted(pipStarted);
|
||||
}, []);
|
||||
const onPlaybackStateChanged = useCallback(
|
||||
async (e: PlaybackStatePayload) => {
|
||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||
|
||||
const onPlaybackStateChanged = useCallback(async (e: PlaybackStatePayload) => {
|
||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||
if (state === "Playing") {
|
||||
setIsPlaying(true);
|
||||
await changePlaybackState(true);
|
||||
if (!Platform.isTV) await activateKeepAwakeAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === "Playing") {
|
||||
setIsPlaying(true);
|
||||
if (!Platform.isTV) await activateKeepAwakeAsync()
|
||||
return;
|
||||
}
|
||||
if (state === "Paused") {
|
||||
setIsPlaying(false);
|
||||
await changePlaybackState(false);
|
||||
if (!Platform.isTV) await deactivateKeepAwake();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === "Paused") {
|
||||
setIsPlaying(false);
|
||||
if (!Platform.isTV) await deactivateKeepAwake();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPlaying) {
|
||||
setIsPlaying(true);
|
||||
setIsBuffering(false);
|
||||
} else if (isBuffering) {
|
||||
setIsBuffering(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const startPosition = useMemo(() => {
|
||||
if (offline) return 0;
|
||||
|
||||
return item?.UserData?.PlaybackPositionTicks
|
||||
? ticksToSeconds(item.UserData.PlaybackPositionTicks)
|
||||
: 0;
|
||||
}, [item]);
|
||||
|
||||
// Preselection of audio and subtitle tracks.
|
||||
if (!settings) return null;
|
||||
let initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
|
||||
|
||||
const allAudio =
|
||||
stream?.mediaSource.MediaStreams?.filter(
|
||||
(audio) => audio.Type === "Audio"
|
||||
) || [];
|
||||
const allSubs =
|
||||
stream?.mediaSource.MediaStreams?.filter(
|
||||
(sub) => sub.Type === "Subtitle"
|
||||
) || [];
|
||||
const textSubs = allSubs.filter((sub) => sub.IsTextSubtitleStream);
|
||||
|
||||
const chosenSubtitleTrack = allSubs.find(
|
||||
(sub) => sub.Index === subtitleIndex
|
||||
if (isPlaying) {
|
||||
setIsPlaying(true);
|
||||
setIsBuffering(false);
|
||||
} else if (isBuffering) {
|
||||
setIsBuffering(true);
|
||||
}
|
||||
},
|
||||
[changePlaybackState]
|
||||
);
|
||||
const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex);
|
||||
|
||||
const notTranscoding = !stream?.mediaSource.TranscodingUrl;
|
||||
if (
|
||||
chosenSubtitleTrack &&
|
||||
(notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
|
||||
) {
|
||||
const finalIndex = notTranscoding
|
||||
? allSubs.indexOf(chosenSubtitleTrack)
|
||||
: textSubs.indexOf(chosenSubtitleTrack);
|
||||
initOptions.push(`--sub-track=${finalIndex}`);
|
||||
}
|
||||
const allAudio = stream?.mediaSource.MediaStreams?.filter((audio) => audio.Type === "Audio") || [];
|
||||
|
||||
if (notTranscoding && chosenAudioTrack) {
|
||||
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
||||
}
|
||||
// Move all the external subtitles last, because vlc places them last.
|
||||
const allSubs =
|
||||
stream?.mediaSource.MediaStreams?.filter((sub) => sub.Type === "Subtitle").sort(
|
||||
(a, b) => Number(a.IsExternal) - Number(b.IsExternal)
|
||||
) || [];
|
||||
|
||||
const externalSubtitles = allSubs
|
||||
.filter((sub: any) => sub.DeliveryMethod === "External")
|
||||
@@ -371,6 +341,22 @@ export default function page() {
|
||||
DeliveryUrl: api?.basePath + sub.DeliveryUrl,
|
||||
}));
|
||||
|
||||
const textSubs = allSubs.filter((sub) => sub.IsTextSubtitleStream);
|
||||
|
||||
const chosenSubtitleTrack = allSubs.find((sub) => sub.Index === subtitleIndex);
|
||||
const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex);
|
||||
|
||||
const notTranscoding = !stream?.mediaSource.TranscodingUrl;
|
||||
let initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
|
||||
if (chosenSubtitleTrack && (notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)) {
|
||||
const finalIndex = notTranscoding ? allSubs.indexOf(chosenSubtitleTrack) : textSubs.indexOf(chosenSubtitleTrack);
|
||||
initOptions.push(`--sub-track=${finalIndex}`);
|
||||
}
|
||||
|
||||
if (notTranscoding && chosenAudioTrack) {
|
||||
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
||||
}
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
// Add useEffect to handle mounting
|
||||
@@ -379,22 +365,15 @@ export default function page() {
|
||||
return () => setIsMounted(false);
|
||||
}, []);
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
useEffect(() => {
|
||||
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
||||
return () => {
|
||||
beforeRemoveListener();
|
||||
};
|
||||
}, [navigation]);
|
||||
|
||||
if (!item || isLoadingItem || !stream)
|
||||
if (itemStatus.isLoading || streamStatus.isLoading) {
|
||||
return (
|
||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
||||
<Loader />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isErrorItem || isErrorStream)
|
||||
if (!item || !stream || itemStatus.isError || streamStatus.isError)
|
||||
return (
|
||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
||||
<Text className="text-white">{t("player.error")}</Text>
|
||||
@@ -435,10 +414,7 @@ export default function page() {
|
||||
}}
|
||||
onVideoError={(e) => {
|
||||
console.error("Video Error:", e.nativeEvent);
|
||||
Alert.alert(
|
||||
t("player.error"),
|
||||
t("player.an_error_occured_while_playing_the_video")
|
||||
);
|
||||
Alert.alert(t("player.error"), t("player.an_error_occured_while_playing_the_video"));
|
||||
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
||||
}}
|
||||
/>
|
||||
@@ -470,7 +446,6 @@ export default function page() {
|
||||
setSubtitleTrack={videoRef.current.setSubtitleTrack}
|
||||
setSubtitleURL={videoRef.current.setSubtitleURL}
|
||||
setAudioTrack={videoRef.current.setAudioTrack}
|
||||
stop={stop}
|
||||
isVlc
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -71,7 +71,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
const lightHapticFeedback = useHaptic("light");
|
||||
|
||||
const goToPlayer = useCallback(
|
||||
(q: string, bitrateValue: number | undefined) => {
|
||||
(q: string) => {
|
||||
router.push(`/player/direct-player?${q}`);
|
||||
},
|
||||
[router]
|
||||
@@ -94,7 +94,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
const queryString = queryParams.toString();
|
||||
|
||||
if (!client) {
|
||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
||||
goToPlayer(queryString);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
||||
goToPlayer(queryString);
|
||||
break;
|
||||
case cancelButtonIndex:
|
||||
break;
|
||||
|
||||
@@ -34,10 +34,10 @@ const ANIMATION_DURATION = 500;
|
||||
const MIN_PLAYBACK_WIDTH = 15;
|
||||
|
||||
export const PlayButton: React.FC<Props> = ({
|
||||
item,
|
||||
selectedOptions,
|
||||
...props
|
||||
}: Props) => {
|
||||
item,
|
||||
selectedOptions,
|
||||
...props
|
||||
}: Props) => {
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -57,7 +57,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
const lightHapticFeedback = useHaptic("light");
|
||||
|
||||
const goToPlayer = useCallback(
|
||||
(q: string, bitrateValue: number | undefined) => {
|
||||
(q: string) => {
|
||||
router.push(`/player/direct-player?${q}`);
|
||||
},
|
||||
[router]
|
||||
@@ -78,7 +78,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
});
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
||||
goToPlayer(queryString);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -88,9 +88,9 @@ export const PlayButton: React.FC<Props> = ({
|
||||
if (userData && userData.PlaybackPositionTicks) {
|
||||
return userData.PlaybackPositionTicks > 0
|
||||
? Math.max(
|
||||
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
||||
MIN_PLAYBACK_WIDTH
|
||||
)
|
||||
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
||||
MIN_PLAYBACK_WIDTH
|
||||
)
|
||||
: 0;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -87,40 +87,38 @@ interface Props {
|
||||
setSubtitleURL?: (url: string, customName: string) => void;
|
||||
setSubtitleTrack?: (index: number) => void;
|
||||
setAudioTrack?: (index: number) => void;
|
||||
stop: (() => Promise<void>) | (() => void);
|
||||
isVlc?: boolean;
|
||||
}
|
||||
|
||||
const CONTROLS_TIMEOUT = 4000;
|
||||
|
||||
export const Controls: React.FC<Props> = ({
|
||||
item,
|
||||
seek,
|
||||
startPictureInPicture,
|
||||
play,
|
||||
pause,
|
||||
togglePlay,
|
||||
isPlaying,
|
||||
isSeeking,
|
||||
progress,
|
||||
isBuffering,
|
||||
cacheProgress,
|
||||
showControls,
|
||||
setShowControls,
|
||||
ignoreSafeAreas,
|
||||
setIgnoreSafeAreas,
|
||||
mediaSource,
|
||||
isVideoLoaded,
|
||||
getAudioTracks,
|
||||
getSubtitleTracks,
|
||||
setSubtitleURL,
|
||||
setSubtitleTrack,
|
||||
setAudioTrack,
|
||||
stop,
|
||||
offline = false,
|
||||
enableTrickplay = true,
|
||||
isVlc = false,
|
||||
}) => {
|
||||
item,
|
||||
seek,
|
||||
startPictureInPicture,
|
||||
play,
|
||||
pause,
|
||||
togglePlay,
|
||||
isPlaying,
|
||||
isSeeking,
|
||||
progress,
|
||||
isBuffering,
|
||||
cacheProgress,
|
||||
showControls,
|
||||
setShowControls,
|
||||
ignoreSafeAreas,
|
||||
setIgnoreSafeAreas,
|
||||
mediaSource,
|
||||
isVideoLoaded,
|
||||
getAudioTracks,
|
||||
getSubtitleTracks,
|
||||
setSubtitleURL,
|
||||
setSubtitleTrack,
|
||||
setAudioTrack,
|
||||
offline = false,
|
||||
enableTrickplay = true,
|
||||
isVlc = false,
|
||||
}) => {
|
||||
const [settings] = useSettings();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -189,75 +187,60 @@ export const Controls: React.FC<Props> = ({
|
||||
isVlc
|
||||
);
|
||||
|
||||
const goToItemCommon = useCallback(
|
||||
(item: BaseItemDto) => {
|
||||
if (!item || !settings) return;
|
||||
|
||||
lightHapticFeedback();
|
||||
|
||||
const previousIndexes = {
|
||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
||||
};
|
||||
|
||||
const {
|
||||
mediaSource: newMediaSource,
|
||||
audioIndex: defaultAudioIndex,
|
||||
subtitleIndex: defaultSubtitleIndex,
|
||||
} = getDefaultPlaySettings(
|
||||
item,
|
||||
settings,
|
||||
previousIndexes,
|
||||
mediaSource ?? undefined
|
||||
);
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id ?? "",
|
||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
||||
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
||||
mediaSourceId: newMediaSource?.Id ?? "",
|
||||
bitrateValue: bitrateValue.toString(),
|
||||
}).toString();
|
||||
|
||||
// @ts-expect-error
|
||||
router.replace(`player/direct-player?${queryParams}`);
|
||||
},
|
||||
[settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router]
|
||||
);
|
||||
|
||||
const goToPreviousItem = useCallback(() => {
|
||||
if (!previousItem || !settings) return;
|
||||
|
||||
lightHapticFeedback();
|
||||
|
||||
const previousIndexes: previousIndexes = {
|
||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
||||
};
|
||||
|
||||
const {
|
||||
mediaSource: newMediaSource,
|
||||
audioIndex: defaultAudioIndex,
|
||||
subtitleIndex: defaultSubtitleIndex,
|
||||
} = getDefaultPlaySettings(
|
||||
previousItem,
|
||||
settings,
|
||||
previousIndexes,
|
||||
mediaSource ?? undefined
|
||||
);
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: previousItem.Id ?? "", // Ensure itemId is a string
|
||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
||||
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
||||
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
||||
bitrateValue: bitrateValue.toString(),
|
||||
}).toString();
|
||||
|
||||
stop();
|
||||
|
||||
// @ts-expect-error
|
||||
router.replace(`player/direct-player?${queryParams}`);
|
||||
}, [previousItem, settings, subtitleIndex, audioIndex]);
|
||||
if (!previousItem) return;
|
||||
goToItemCommon(previousItem);
|
||||
}, [previousItem, goToItemCommon]);
|
||||
|
||||
const goToNextItem = useCallback(() => {
|
||||
if (!nextItem || !settings) return;
|
||||
if (!nextItem) return;
|
||||
goToItemCommon(nextItem);
|
||||
}, [nextItem, goToItemCommon]);
|
||||
|
||||
lightHapticFeedback();
|
||||
|
||||
const previousIndexes: previousIndexes = {
|
||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
||||
};
|
||||
|
||||
const {
|
||||
mediaSource: newMediaSource,
|
||||
audioIndex: defaultAudioIndex,
|
||||
subtitleIndex: defaultSubtitleIndex,
|
||||
} = getDefaultPlaySettings(
|
||||
nextItem,
|
||||
settings,
|
||||
previousIndexes,
|
||||
mediaSource ?? undefined
|
||||
);
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: nextItem.Id ?? "", // Ensure itemId is a string
|
||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
||||
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
||||
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
||||
bitrateValue: bitrateValue.toString(),
|
||||
}).toString();
|
||||
|
||||
stop();
|
||||
|
||||
// @ts-expect-error
|
||||
router.replace(`player/direct-player?${queryParams}`);
|
||||
}, [nextItem, settings, subtitleIndex, audioIndex]);
|
||||
const goToItem = useCallback(
|
||||
async (itemId: string) => {
|
||||
const gotoItem = await getItemById(api, itemId);
|
||||
if (!gotoItem) return;
|
||||
goToItemCommon(gotoItem);
|
||||
},
|
||||
[goToItemCommon, api]
|
||||
);
|
||||
|
||||
const updateTimes = useCallback(
|
||||
(currentProgress: number, maxValue: number) => {
|
||||
@@ -381,49 +364,6 @@ export const Controls: React.FC<Props> = ({
|
||||
}
|
||||
}, [settings, isPlaying, isVlc]);
|
||||
|
||||
const goToItem = useCallback(
|
||||
async (itemId: string) => {
|
||||
try {
|
||||
const gotoItem = await getItemById(api, itemId);
|
||||
if (!settings || !gotoItem) return;
|
||||
|
||||
lightHapticFeedback();
|
||||
|
||||
const previousIndexes: previousIndexes = {
|
||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
||||
};
|
||||
|
||||
const {
|
||||
mediaSource: newMediaSource,
|
||||
audioIndex: defaultAudioIndex,
|
||||
subtitleIndex: defaultSubtitleIndex,
|
||||
} = getDefaultPlaySettings(
|
||||
gotoItem,
|
||||
settings,
|
||||
previousIndexes,
|
||||
mediaSource ?? undefined
|
||||
);
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: gotoItem.Id ?? "", // Ensure itemId is a string
|
||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
||||
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
||||
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
||||
bitrateValue: bitrateValue.toString(),
|
||||
}).toString();
|
||||
|
||||
stop();
|
||||
|
||||
// @ts-expect-error
|
||||
router.replace(`player/direct-player?${queryParams}`);
|
||||
} catch (error) {
|
||||
console.error("Error in gotoEpisode:", error);
|
||||
}
|
||||
},
|
||||
[settings, subtitleIndex, audioIndex]
|
||||
);
|
||||
|
||||
const toggleIgnoreSafeAreas = useCallback(() => {
|
||||
setIgnoreSafeAreas((prev) => !prev);
|
||||
lightHapticFeedback();
|
||||
@@ -497,7 +437,6 @@ export const Controls: React.FC<Props> = ({
|
||||
}, [trickPlayUrl, trickplayInfo, time]);
|
||||
|
||||
const onClose = async () => {
|
||||
stop();
|
||||
lightHapticFeedback();
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||
@@ -549,7 +488,7 @@ export const Controls: React.FC<Props> = ({
|
||||
setSubtitleTrack={setSubtitleTrack}
|
||||
setSubtitleURL={setSubtitleURL}
|
||||
>
|
||||
<DropdownView showControls={showControls} />
|
||||
<DropdownView />
|
||||
</VideoProvider>
|
||||
</View>
|
||||
)}
|
||||
@@ -790,8 +729,8 @@ export const Controls: React.FC<Props> = ({
|
||||
!nextItem
|
||||
? false
|
||||
: isVlc
|
||||
? remainingTime < 10000
|
||||
: remainingTime < 10
|
||||
? remainingTime < 10000
|
||||
: remainingTime < 10
|
||||
}
|
||||
onFinish={goToNextItem}
|
||||
onPress={goToNextItem}
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import { TrackInfo } from "@/modules/vlc-player";
|
||||
import {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import React, { createContext, useContext, useState, ReactNode, useEffect, useMemo } from "react";
|
||||
import { useControlContext } from "./ControlContext";
|
||||
import { Track } from "../types";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
@@ -27,14 +16,8 @@ const VideoContext = createContext<VideoContextProps | undefined>(undefined);
|
||||
|
||||
interface VideoProviderProps {
|
||||
children: ReactNode;
|
||||
getAudioTracks:
|
||||
| (() => Promise<TrackInfo[] | null>)
|
||||
| (() => TrackInfo[])
|
||||
| undefined;
|
||||
getSubtitleTracks:
|
||||
| (() => Promise<TrackInfo[] | null>)
|
||||
| (() => TrackInfo[])
|
||||
| undefined;
|
||||
getAudioTracks: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]) | undefined;
|
||||
getSubtitleTracks: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]) | undefined;
|
||||
setAudioTrack: ((index: number) => void) | undefined;
|
||||
setSubtitleTrack: ((index: number) => void) | undefined;
|
||||
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
|
||||
@@ -55,23 +38,19 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
||||
const mediaSource = ControlContext?.mediaSource;
|
||||
|
||||
const allSubs =
|
||||
mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || [];
|
||||
const allSubs = mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || [];
|
||||
|
||||
const { itemId, audioIndex, bitrateValue, subtitleIndex } =
|
||||
useLocalSearchParams<{
|
||||
itemId: string;
|
||||
audioIndex: string;
|
||||
subtitleIndex: string;
|
||||
mediaSourceId: string;
|
||||
bitrateValue: string;
|
||||
}>();
|
||||
const { itemId, audioIndex, bitrateValue, subtitleIndex } = useLocalSearchParams<{
|
||||
itemId: string;
|
||||
audioIndex: string;
|
||||
subtitleIndex: string;
|
||||
mediaSourceId: string;
|
||||
bitrateValue: string;
|
||||
}>();
|
||||
|
||||
const onTextBasedSubtitle = useMemo(
|
||||
() =>
|
||||
allSubs.find(
|
||||
(s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream
|
||||
) || subtitleIndex === "-1",
|
||||
allSubs.find((s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream) || subtitleIndex === "-1",
|
||||
[allSubs, subtitleIndex]
|
||||
);
|
||||
|
||||
@@ -95,21 +74,14 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
router.replace(`player/direct-player?${queryParams}`);
|
||||
};
|
||||
|
||||
const setTrackParams = (
|
||||
type: "audio" | "subtitle",
|
||||
index: number,
|
||||
serverIndex: number
|
||||
) => {
|
||||
const setTrackParams = (type: "audio" | "subtitle", index: number, serverIndex: number) => {
|
||||
const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack;
|
||||
const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex";
|
||||
|
||||
// If we're transcoding and we're going from a image based subtitle
|
||||
// to a text based subtitle, we need to change the player params.
|
||||
|
||||
const shouldChangePlayerParams =
|
||||
type === "subtitle" &&
|
||||
mediaSource?.TranscodingUrl &&
|
||||
!onTextBasedSubtitle;
|
||||
const shouldChangePlayerParams = type === "subtitle" && mediaSource?.TranscodingUrl && !onTextBasedSubtitle;
|
||||
|
||||
console.log("Set player params", index, serverIndex);
|
||||
if (shouldChangePlayerParams) {
|
||||
@@ -129,23 +101,22 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
if (getSubtitleTracks) {
|
||||
const subtitleData = await getSubtitleTracks();
|
||||
|
||||
// Step 1: Move external subs to the end, because VLC puts external subs at the end
|
||||
const sortedSubs = allSubs.sort((a, b) => Number(a.IsExternal) - Number(b.IsExternal));
|
||||
|
||||
// Step 2: Apply VLC indexing logic
|
||||
let textSubIndex = 0;
|
||||
const subtitles: Track[] = allSubs?.map((sub) => {
|
||||
const processedSubs: Track[] = sortedSubs?.map((sub) => {
|
||||
// Always increment for non-transcoding subtitles
|
||||
// Only increment for text-based subtitles when transcoding
|
||||
const shouldIncrement =
|
||||
!mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream;
|
||||
|
||||
const displayTitle = sub.DisplayTitle || "Undefined Subtitle";
|
||||
const shouldIncrement = !mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream;
|
||||
const vlcIndex = subtitleData?.at(textSubIndex)?.index ?? -1;
|
||||
|
||||
const finalIndex = shouldIncrement ? vlcIndex : sub.Index ?? -1;
|
||||
|
||||
if (shouldIncrement) textSubIndex++;
|
||||
return {
|
||||
name: displayTitle,
|
||||
name: sub.DisplayTitle || "Undefined Subtitle",
|
||||
index: sub.Index ?? -1,
|
||||
originalIndex: finalIndex,
|
||||
setTrack: () =>
|
||||
shouldIncrement
|
||||
? setTrackParams("subtitle", finalIndex, sub.Index ?? -1)
|
||||
@@ -155,6 +126,9 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
};
|
||||
});
|
||||
|
||||
// Step 3: Restore the original order
|
||||
const subtitles: Track[] = processedSubs.sort((a, b) => a.index - b.index);
|
||||
|
||||
// Add a "Disable Subtitles" option
|
||||
subtitles.unshift({
|
||||
name: "Disable",
|
||||
@@ -164,36 +138,25 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
? setTrackParams("subtitle", -1, -1)
|
||||
: setPlayerParams({ chosenSubtitleIndex: "-1" }),
|
||||
});
|
||||
|
||||
setSubtitleTracks(subtitles);
|
||||
}
|
||||
if (
|
||||
getAudioTracks &&
|
||||
(audioTracks === null || audioTracks.length === 0)
|
||||
) {
|
||||
if (getAudioTracks) {
|
||||
const audioData = await getAudioTracks();
|
||||
if (!audioData) return;
|
||||
|
||||
console.log("audioData", audioData);
|
||||
|
||||
const allAudio =
|
||||
mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || [];
|
||||
|
||||
const allAudio = mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || [];
|
||||
const audioTracks: Track[] = allAudio?.map((audio, idx) => {
|
||||
if (!mediaSource?.TranscodingUrl) {
|
||||
const vlcIndex = audioData?.at(idx)?.index ?? -1;
|
||||
return {
|
||||
name: audio.DisplayTitle ?? "Undefined Audio",
|
||||
index: audio.Index ?? -1,
|
||||
setTrack: () =>
|
||||
setTrackParams("audio", vlcIndex, audio.Index ?? -1),
|
||||
setTrack: () => setTrackParams("audio", vlcIndex, audio.Index ?? -1),
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: audio.DisplayTitle ?? "Undefined Audio",
|
||||
index: audio.Index ?? -1,
|
||||
setTrack: () =>
|
||||
setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }),
|
||||
setTrack: () => setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }),
|
||||
};
|
||||
});
|
||||
setAudioTracks(audioTracks);
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { TouchableOpacity, Platform } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||
import { useVideoContext } from "../contexts/VideoContext";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { BITRATES } from "@/components/BitrateSelector";
|
||||
import { useControlContext } from "../contexts/ControlContext";
|
||||
|
||||
interface DropdownViewProps {
|
||||
showControls: boolean;
|
||||
offline?: boolean; // used to disable external subs for downloads
|
||||
}
|
||||
|
||||
const DropdownView: React.FC<DropdownViewProps> = ({
|
||||
showControls,
|
||||
offline = false,
|
||||
}) => {
|
||||
const DropdownView = () => {
|
||||
const videoContext = useVideoContext();
|
||||
const { subtitleTracks, audioTracks } = videoContext;
|
||||
const ControlContext = useControlContext();
|
||||
const [item, mediaSource] = [ControlContext?.item, ControlContext?.mediaSource];
|
||||
const router = useRouter();
|
||||
|
||||
const { subtitleIndex, audioIndex } = useLocalSearchParams<{
|
||||
const { subtitleIndex, audioIndex, bitrateValue } = useLocalSearchParams<{
|
||||
itemId: string;
|
||||
audioIndex: string;
|
||||
subtitleIndex: string;
|
||||
@@ -25,6 +22,21 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
||||
bitrateValue: string;
|
||||
}>();
|
||||
|
||||
const changeBitrate = useCallback(
|
||||
(bitrate: string) => {
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id ?? "",
|
||||
audioIndex: audioIndex?.toString() ?? "",
|
||||
subtitleIndex: subtitleIndex.toString() ?? "",
|
||||
mediaSourceId: mediaSource?.Id ?? "",
|
||||
bitrateValue: bitrate.toString(),
|
||||
}).toString();
|
||||
// @ts-expect-error
|
||||
router.replace(`player/direct-player?${queryParams}`);
|
||||
},
|
||||
[item, mediaSource, subtitleIndex, audioIndex]
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
@@ -42,9 +54,27 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger key="subtitle-trigger">
|
||||
Subtitle
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubTrigger key="qualitytrigger">Quality</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
alignOffset={-10}
|
||||
avoidCollisions={true}
|
||||
collisionPadding={0}
|
||||
loop={true}
|
||||
sideOffset={10}
|
||||
>
|
||||
{BITRATES?.map((bitrate, idx: number) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={`quality-item-${idx}`}
|
||||
value={bitrateValue === (bitrate.value?.toString() ?? "")}
|
||||
onValueChange={() => changeBitrate(bitrate.value?.toString() ?? "")}
|
||||
>
|
||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>{bitrate.key}</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger key="subtitle-trigger">Subtitle</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
alignOffset={-10}
|
||||
avoidCollisions={true}
|
||||
@@ -58,17 +88,13 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
||||
value={subtitleIndex === sub.index.toString()}
|
||||
onValueChange={() => sub.setTrack()}
|
||||
>
|
||||
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>
|
||||
{sub.name}
|
||||
</DropdownMenu.ItemTitle>
|
||||
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>{sub.name}</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger key="audio-trigger">
|
||||
Audio
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubTrigger key="audio-trigger">Audio</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent
|
||||
alignOffset={-10}
|
||||
avoidCollisions={true}
|
||||
@@ -82,9 +108,7 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
||||
value={audioIndex === track.index.toString()}
|
||||
onValueChange={() => track.setTrack()}
|
||||
>
|
||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
||||
{track.name}
|
||||
</DropdownMenu.ItemTitle>
|
||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>{track.name}</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.SubContent>
|
||||
|
||||
@@ -12,6 +12,7 @@ Pod::Spec.new do |s|
|
||||
s.dependency 'ExpoModulesCore'
|
||||
s.ios.dependency 'VLCKit', s.version
|
||||
s.tvos.dependency 'VLCKit', s.version
|
||||
s.dependency 'Alamofire', '~> 5.10'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
|
||||
@@ -459,7 +459,9 @@ extension VlcPlayerView: SimpleAppLifecycleListener {
|
||||
}
|
||||
|
||||
// Current solution to fixing black screen when re-entering application
|
||||
if let videoTrack = self.vlc.player.videoTracks.first { $0.isSelected == true }, !self.vlc.isMediaPlaying() {
|
||||
if let videoTrack = self.vlc.player.videoTracks.first { $0.isSelected == true },
|
||||
!self.vlc.isMediaPlaying()
|
||||
{
|
||||
videoTrack.isSelected = false
|
||||
videoTrack.isSelectedExclusively = true
|
||||
self.vlc.player.play()
|
||||
|
||||
@@ -1,458 +1,458 @@
|
||||
{
|
||||
"login": {
|
||||
"username_required": "Nome utente è obbligatorio",
|
||||
"error_title": "Errore",
|
||||
"login_title": "Accesso",
|
||||
"login_to_title": "Accedi a",
|
||||
"username_placeholder": "Nome utente",
|
||||
"password_placeholder": "Password",
|
||||
"login_button": "Accedi",
|
||||
"quick_connect": "Connessione Rapida",
|
||||
"enter_code_to_login": "Inserire {{code}} per accedere",
|
||||
"failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida",
|
||||
"got_it": "Capito",
|
||||
"connection_failed": "Connessione fallita",
|
||||
"could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.",
|
||||
"an_unexpected_error_occured": "Si è verificato un errore inaspettato",
|
||||
"change_server": "Cambiare il server",
|
||||
"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",
|
||||
"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.",
|
||||
"there_is_a_server_error": "Si è verificato un errore del server",
|
||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?"
|
||||
"login": {
|
||||
"username_required": "Nome utente è obbligatorio",
|
||||
"error_title": "Errore",
|
||||
"login_title": "Accesso",
|
||||
"login_to_title": "Accedi a",
|
||||
"username_placeholder": "Nome utente",
|
||||
"password_placeholder": "Password",
|
||||
"login_button": "Accedi",
|
||||
"quick_connect": "Connessione Rapida",
|
||||
"enter_code_to_login": "Inserire {{code}} per accedere",
|
||||
"failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida",
|
||||
"got_it": "Capito",
|
||||
"connection_failed": "Connessione fallita",
|
||||
"could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.",
|
||||
"an_unexpected_error_occured": "Si è verificato un errore inaspettato",
|
||||
"change_server": "Cambiare il server",
|
||||
"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",
|
||||
"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.",
|
||||
"there_is_a_server_error": "Si è verificato un errore del server",
|
||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?"
|
||||
},
|
||||
"server": {
|
||||
"enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin",
|
||||
"server_url_placeholder": "http(s)://tuo-server.com",
|
||||
"connect_button": "Connetti",
|
||||
"previous_servers": "server precedente",
|
||||
"clear_button": "Cancella",
|
||||
"search_for_local_servers": "Ricerca dei server locali",
|
||||
"searching": "Cercando...",
|
||||
"servers": "Servers"
|
||||
},
|
||||
"home": {
|
||||
"no_internet": "Nessun Internet",
|
||||
"no_items": "Nessun oggetto",
|
||||
"no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.",
|
||||
"go_to_downloads": "Vai agli elementi scaricati",
|
||||
"oops": "Oops!",
|
||||
"error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.",
|
||||
"continue_watching": "Continua a guardare",
|
||||
"next_up": "Prossimo",
|
||||
"recently_added_in": "Aggiunti di recente a {{libraryName}}",
|
||||
"suggested_movies": "Film consigliati",
|
||||
"suggested_episodes": "Episodi consigliati",
|
||||
"intro": {
|
||||
"welcome_to_streamyfin": "Benvenuto a Streamyfin",
|
||||
"a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.",
|
||||
"features_title": "Funzioni",
|
||||
"features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:",
|
||||
"jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.",
|
||||
"downloads_feature_title": "Scaricamento",
|
||||
"downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.",
|
||||
"chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.",
|
||||
"centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate",
|
||||
"centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.",
|
||||
"done_button": "Fatto",
|
||||
"go_to_settings_button": "Vai alle impostazioni",
|
||||
"read_more": "Leggi di più"
|
||||
},
|
||||
"server": {
|
||||
"enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin",
|
||||
"server_url_placeholder": "http(s)://tuo-server.com",
|
||||
"connect_button": "Connetti",
|
||||
"previous_servers": "server precedente",
|
||||
"clear_button": "Cancella",
|
||||
"search_for_local_servers": "Ricerca dei server locali",
|
||||
"searching": "Cercando...",
|
||||
"servers": "Servers"
|
||||
},
|
||||
"home": {
|
||||
"no_internet": "Nessun Internet",
|
||||
"no_items": "Nessun oggetto",
|
||||
"no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.",
|
||||
"go_to_downloads": "Vai agli elementi scaricati",
|
||||
"oops": "Oops!",
|
||||
"error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.",
|
||||
"continue_watching": "Continua a guardare",
|
||||
"next_up": "Prossimo",
|
||||
"recently_added_in": "Aggiunti di recente a {{libraryName}}",
|
||||
"suggested_movies": "Film consigliati",
|
||||
"suggested_episodes": "Episodi consigliati",
|
||||
"intro": {
|
||||
"welcome_to_streamyfin": "Benvenuto a Streamyfin",
|
||||
"a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.",
|
||||
"features_title": "Funzioni",
|
||||
"features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:",
|
||||
"jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.",
|
||||
"downloads_feature_title": "Scaricamento",
|
||||
"downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.",
|
||||
"chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.",
|
||||
"centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate",
|
||||
"centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.",
|
||||
"done_button": "Fatto",
|
||||
"go_to_settings_button": "Vai alle impostazioni",
|
||||
"read_more": "Leggi di più"
|
||||
"settings": {
|
||||
"settings_title": "Impostazioni",
|
||||
"log_out_button": "Esci",
|
||||
"user_info": {
|
||||
"user_info_title": "Info utente",
|
||||
"user": "Utente",
|
||||
"server": "Server",
|
||||
"token": "Token",
|
||||
"app_version": "Versione dell'App"
|
||||
},
|
||||
"settings": {
|
||||
"settings_title": "Impostazioni",
|
||||
"log_out_button": "Esci",
|
||||
"user_info": {
|
||||
"user_info_title": "Info utente",
|
||||
"user": "Utente",
|
||||
"server": "Server",
|
||||
"token": "Token",
|
||||
"app_version": "Versione dell'App"
|
||||
},
|
||||
"quick_connect": {
|
||||
"quick_connect_title": "Connessione Rapida",
|
||||
"authorize_button": "Autorizza Connessione Rapida",
|
||||
"enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...",
|
||||
"success": "Successo",
|
||||
"quick_connect_autorized": "Connessione Rapida autorizzata",
|
||||
"error": "Errore",
|
||||
"invalid_code": "Codice invalido",
|
||||
"authorize": "Autorizza"
|
||||
},
|
||||
"media_controls": {
|
||||
"media_controls_title": "Controlli multimediali",
|
||||
"forward_skip_length": "Lunghezza del salto in avanti",
|
||||
"rewind_length": "Lunghezza del riavvolgimento",
|
||||
"seconds_unit": "s"
|
||||
},
|
||||
"audio": {
|
||||
"audio_title": "Audio",
|
||||
"set_audio_track": "Imposta la traccia audio dall'elemento precedente",
|
||||
"audio_language": "Lingua Audio",
|
||||
"audio_hint": "Scegli la lingua audio predefinita.",
|
||||
"none": "Nessuno",
|
||||
"language": "Lingua"
|
||||
},
|
||||
"subtitles": {
|
||||
"subtitle_title": "Sottotitoli",
|
||||
"subtitle_language": "Lingua dei sottotitoli",
|
||||
"subtitle_mode": "Modalità dei sottotitoli",
|
||||
"set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente",
|
||||
"subtitle_size": "Dimensione dei sottotitoli",
|
||||
"subtitle_hint": "Configura la preferenza dei sottotitoli.",
|
||||
"none": "Nessuno",
|
||||
"language": "Lingua",
|
||||
"loading": "Caricamento",
|
||||
"modes": {
|
||||
"Default": "Predefinito",
|
||||
"Smart": "Intelligente",
|
||||
"Always": "Sempre",
|
||||
"None": "Nessuno",
|
||||
"OnlyForced": "Solo forzati"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"other_title": "Altro",
|
||||
"auto_rotate": "Rotazione automatica",
|
||||
"video_orientation": "Orientamento del video",
|
||||
"orientation": "Orientamento",
|
||||
"orientations": {
|
||||
"DEFAULT": "Predefinito",
|
||||
"ALL": "Tutto",
|
||||
"PORTRAIT": "Verticale",
|
||||
"PORTRAIT_UP": "Verticale sopra",
|
||||
"PORTRAIT_DOWN": "Verticale sotto",
|
||||
"LANDSCAPE": "Orizzontale",
|
||||
"LANDSCAPE_LEFT": "Orizzontale sinitra",
|
||||
"LANDSCAPE_RIGHT": "Orizzontale destra",
|
||||
"OTHER": "Altro",
|
||||
"UNKNOWN": "Sconosciuto"
|
||||
},
|
||||
"safe_area_in_controls": "Area sicura per i controlli",
|
||||
"show_custom_menu_links": "Mostra i link del menu personalizzato",
|
||||
"hide_libraries": "Nascondi Librerie",
|
||||
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
|
||||
"disable_haptic_feedback": "Disabilita il feedback aptico",
|
||||
"default_quality": "Qualità predefinita"
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Scaricamento",
|
||||
"download_method": "Metodo per lo scaricamento",
|
||||
"remux_max_download": "Numero di Remux da scaricare al massimo",
|
||||
"auto_download": "Scaricamento automatico",
|
||||
"optimized_versions_server": "Versioni del server di ottimizzazione",
|
||||
"save_button": "Salva",
|
||||
"optimized_server": "Server di ottimizzazione",
|
||||
"optimized": "Ottimizzato",
|
||||
"default": "Predefinito",
|
||||
"optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.",
|
||||
"read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.",
|
||||
"url":"URL",
|
||||
"server_url_placeholder": "http(s)://dominio.org:porta"
|
||||
},
|
||||
"plugins": {
|
||||
"plugins_title": "Plugin",
|
||||
"jellyseerr": {
|
||||
"jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.",
|
||||
"server_url": "URL del Server",
|
||||
"server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)",
|
||||
"server_url_placeholder": "URL di Jellyseerr...",
|
||||
"password": "Password",
|
||||
"password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin",
|
||||
"save_button": "Salva",
|
||||
"clear_button": "Cancella",
|
||||
"login_button": "Accedi",
|
||||
"total_media_requests": "Totale di richieste di media",
|
||||
"movie_quota_limit": "Limite di quota per i film",
|
||||
"movie_quota_days": "Giorni di quota per i film",
|
||||
"tv_quota_limit": "Limite 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",
|
||||
"unlimited": "Illimitato"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Abilita la ricerca Marlin ",
|
||||
"url": "URL",
|
||||
"server_url_placeholder": "http(s)://dominio.org:porta",
|
||||
"marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.",
|
||||
"read_more_about_marlin": "Leggi di più su Marlin.",
|
||||
"save_button": "Salva",
|
||||
"toasts": {
|
||||
"saved": "Salvato"
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"storage_title": "Spazio",
|
||||
"app_usage": "App {{usedSpace}}%",
|
||||
"device_usage": "Dispositivo {{availableSpace}}%",
|
||||
"size_used": "{{used}} di {{total}} usato",
|
||||
"delete_all_downloaded_files": "Cancella Tutti i File Scaricati"
|
||||
},
|
||||
"intro": {
|
||||
"show_intro": "Mostra intro",
|
||||
"reset_intro": "Ripristina intro"
|
||||
},
|
||||
"logs": {
|
||||
"logs_title": "Log",
|
||||
"no_logs_available": "Nessun log disponibile",
|
||||
"delete_all_logs": "Cancella tutti i log"
|
||||
},
|
||||
"languages": {
|
||||
"title": "Lingue",
|
||||
"app_language": "Lingua dell'App",
|
||||
"app_language_description": "Selezione la lingua dell'app.",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"toasts":{
|
||||
"error_deleting_files": "Errore nella cancellazione dei file",
|
||||
"background_downloads_enabled": "Scaricamento in background abilitato",
|
||||
"background_downloads_disabled": "Scaricamento in background disabilitato",
|
||||
"connected": "Connesso",
|
||||
"could_not_connect": "Non è stato possibile connettersi",
|
||||
"invalid_url": "URL invalido"
|
||||
"quick_connect": {
|
||||
"quick_connect_title": "Connessione Rapida",
|
||||
"authorize_button": "Autorizza Connessione Rapida",
|
||||
"enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...",
|
||||
"success": "Successo",
|
||||
"quick_connect_autorized": "Connessione Rapida autorizzata",
|
||||
"error": "Errore",
|
||||
"invalid_code": "Codice invalido",
|
||||
"authorize": "Autorizza"
|
||||
},
|
||||
"media_controls": {
|
||||
"media_controls_title": "Controlli multimediali",
|
||||
"forward_skip_length": "Lunghezza del salto in avanti",
|
||||
"rewind_length": "Lunghezza del riavvolgimento",
|
||||
"seconds_unit": "s"
|
||||
},
|
||||
"audio": {
|
||||
"audio_title": "Audio",
|
||||
"set_audio_track": "Imposta la traccia audio dall'elemento precedente",
|
||||
"audio_language": "Lingua Audio",
|
||||
"audio_hint": "Scegli la lingua audio predefinita.",
|
||||
"none": "Nessuno",
|
||||
"language": "Lingua"
|
||||
},
|
||||
"subtitles": {
|
||||
"subtitle_title": "Sottotitoli",
|
||||
"subtitle_language": "Lingua dei sottotitoli",
|
||||
"subtitle_mode": "Modalità dei sottotitoli",
|
||||
"set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente",
|
||||
"subtitle_size": "Dimensione dei sottotitoli",
|
||||
"subtitle_hint": "Configura la preferenza dei sottotitoli.",
|
||||
"none": "Nessuno",
|
||||
"language": "Lingua",
|
||||
"loading": "Caricamento",
|
||||
"modes": {
|
||||
"Default": "Predefinito",
|
||||
"Smart": "Intelligente",
|
||||
"Always": "Sempre",
|
||||
"None": "Nessuno",
|
||||
"OnlyForced": "Solo forzati"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"other_title": "Altro",
|
||||
"auto_rotate": "Rotazione automatica",
|
||||
"video_orientation": "Orientamento del video",
|
||||
"orientation": "Orientamento",
|
||||
"orientations": {
|
||||
"DEFAULT": "Predefinito",
|
||||
"ALL": "Tutto",
|
||||
"PORTRAIT": "Verticale",
|
||||
"PORTRAIT_UP": "Verticale sopra",
|
||||
"PORTRAIT_DOWN": "Verticale sotto",
|
||||
"LANDSCAPE": "Orizzontale",
|
||||
"LANDSCAPE_LEFT": "Orizzontale sinitra",
|
||||
"LANDSCAPE_RIGHT": "Orizzontale destra",
|
||||
"OTHER": "Altro",
|
||||
"UNKNOWN": "Sconosciuto"
|
||||
},
|
||||
"safe_area_in_controls": "Area sicura per i controlli",
|
||||
"show_custom_menu_links": "Mostra i link del menu personalizzato",
|
||||
"hide_libraries": "Nascondi Librerie",
|
||||
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
|
||||
"disable_haptic_feedback": "Disabilita il feedback aptico",
|
||||
"default_quality": "Qualità predefinita"
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Scaricati",
|
||||
"tvseries": "Serie TV",
|
||||
"movies": "Film",
|
||||
"queue": "Coda",
|
||||
"queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app",
|
||||
"no_items_in_queue": "Nessun elemento in coda",
|
||||
"no_downloaded_items": "Nessun elemento scaricato",
|
||||
"delete_all_movies_button": "Cancella tutti i film",
|
||||
"delete_all_tvseries_button": "Cancella tutte le serie TV",
|
||||
"delete_all_button": "Cancella tutti",
|
||||
"active_download": "Scaricamento in corso",
|
||||
"no_active_downloads": "Nessun scaricamento in corso",
|
||||
"active_downloads": "Scaricamenti in corso",
|
||||
"new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti",
|
||||
"new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.",
|
||||
"back": "Indietro",
|
||||
"delete": "Cancella",
|
||||
"something_went_wrong": "Qualcosa è andato storto",
|
||||
"could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin",
|
||||
"eta": "ETA {{eta}}",
|
||||
"methods": "Metodi",
|
||||
"toasts": {
|
||||
"you_are_not_allowed_to_download_files": "Non è consentito scaricare file.",
|
||||
"deleted_all_movies_successfully": "Cancellati tutti i film con successo!",
|
||||
"failed_to_delete_all_movies": "Impossibile eliminare tutti i film",
|
||||
"deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!",
|
||||
"failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV",
|
||||
"download_cancelled": "Scaricamento annullato",
|
||||
"could_not_cancel_download": "Impossibile annullare lo scaricamento",
|
||||
"download_completed": "Scaricamento completato",
|
||||
"download_started_for": "Scaricamento iniziato per {{item}}",
|
||||
"item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato",
|
||||
"download_stated_for_item": "Scaricamento iniziato per {{item}}",
|
||||
"download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}",
|
||||
"download_completed_for_item": "Scaricamento completato per {{item}}",
|
||||
"queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione",
|
||||
"failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}",
|
||||
"server_responded_with_status_code": "Server responded with status {{statusCode}}",
|
||||
"no_response_received_from_server": "No response received from the server",
|
||||
"error_setting_up_the_request": "Error setting up the request",
|
||||
"failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto",
|
||||
"all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.",
|
||||
"an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi",
|
||||
"go_to_downloads": "Vai agli elementi scaricati"
|
||||
"downloads_title": "Scaricamento",
|
||||
"download_method": "Metodo per lo scaricamento",
|
||||
"remux_max_download": "Numero di Remux da scaricare al massimo",
|
||||
"auto_download": "Scaricamento automatico",
|
||||
"optimized_versions_server": "Versioni del server di ottimizzazione",
|
||||
"save_button": "Salva",
|
||||
"optimized_server": "Server di ottimizzazione",
|
||||
"optimized": "Ottimizzato",
|
||||
"default": "Predefinito",
|
||||
"optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.",
|
||||
"read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.",
|
||||
"url": "URL",
|
||||
"server_url_placeholder": "http(s)://dominio.org:porta"
|
||||
},
|
||||
"plugins": {
|
||||
"plugins_title": "Plugin",
|
||||
"jellyseerr": {
|
||||
"jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.",
|
||||
"server_url": "URL del Server",
|
||||
"server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)",
|
||||
"server_url_placeholder": "URL di Jellyseerr...",
|
||||
"password": "Password",
|
||||
"password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin",
|
||||
"save_button": "Salva",
|
||||
"clear_button": "Cancella",
|
||||
"login_button": "Accedi",
|
||||
"total_media_requests": "Totale di richieste di media",
|
||||
"movie_quota_limit": "Limite di quota per i film",
|
||||
"movie_quota_days": "Giorni di quota per i film",
|
||||
"tv_quota_limit": "Limite 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",
|
||||
"unlimited": "Illimitato"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Abilita la ricerca Marlin ",
|
||||
"url": "URL",
|
||||
"server_url_placeholder": "http(s)://dominio.org:porta",
|
||||
"marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.",
|
||||
"read_more_about_marlin": "Leggi di più su Marlin.",
|
||||
"save_button": "Salva",
|
||||
"toasts": {
|
||||
"saved": "Salvato"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"search_here": "Cerca qui...",
|
||||
"search": "Cerca...",
|
||||
"x_items": "{{count}} elementi",
|
||||
"library": "Libreria",
|
||||
"discover": "Scopri",
|
||||
"no_results": "Nessun risultato",
|
||||
"no_results_found_for": "Nessun risultato trovato per",
|
||||
"movies": "Film",
|
||||
"series": "Serie",
|
||||
"episodes": "Episodi",
|
||||
"collections": "Collezioni",
|
||||
"actors": "Attori",
|
||||
"request_movies": "Film Richiesti",
|
||||
"request_series": "Serie Richieste",
|
||||
"recently_added": "Aggiunti di Recente",
|
||||
"recent_requests": "Richiesti di Recente",
|
||||
"plex_watchlist": "Plex Watchlist",
|
||||
"trending": "In tendenza",
|
||||
"popular_movies": "Film Popolari",
|
||||
"movie_genres": "Generi Film",
|
||||
"upcoming_movies": "Film in arrivo",
|
||||
"studios": "Studio",
|
||||
"popular_tv": "Serie Popolari",
|
||||
"tv_genres": "Generi Televisivi",
|
||||
"upcoming_tv": "Serie in Arrivo",
|
||||
"networks": "Network",
|
||||
"tmdb_movie_keyword": "TMDB Parola chiave del film",
|
||||
"tmdb_movie_genre": "TMDB Genere Film",
|
||||
"tmdb_tv_keyword": "TMDB Parola chiave della serie",
|
||||
"tmdb_tv_genre": "TMDB Genere Televisivo",
|
||||
"tmdb_search": "TMDB Cerca",
|
||||
"tmdb_studio": "TMDB Studio",
|
||||
"tmdb_network": "TMDB Network",
|
||||
"tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film",
|
||||
"tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie"
|
||||
},
|
||||
"library": {
|
||||
"no_items_found": "Nessun elemento trovato",
|
||||
"no_results": "Nessun risultato",
|
||||
"no_libraries_found": "Nessuna libreria trovata",
|
||||
"item_types": {
|
||||
"movies": "film",
|
||||
"series": "serie TV",
|
||||
"boxsets": "cofanetti",
|
||||
"items": "elementi"
|
||||
},
|
||||
"options": {
|
||||
"display": "Display",
|
||||
"row": "Fila",
|
||||
"list": "Lista",
|
||||
"image_style": "Stile dell'immagine",
|
||||
"poster": "Poster",
|
||||
"cover": "Cover",
|
||||
"show_titles": "Mostra titoli",
|
||||
"show_stats": "Mostra statistiche"
|
||||
"storage": {
|
||||
"storage_title": "Spazio",
|
||||
"app_usage": "App {{usedSpace}}%",
|
||||
"device_usage": "Dispositivo {{availableSpace}}%",
|
||||
"size_used": "{{used}} di {{total}} usato",
|
||||
"delete_all_downloaded_files": "Cancella Tutti i File Scaricati"
|
||||
},
|
||||
"intro": {
|
||||
"show_intro": "Mostra intro",
|
||||
"reset_intro": "Ripristina intro"
|
||||
},
|
||||
"logs": {
|
||||
"logs_title": "Log",
|
||||
"no_logs_available": "Nessun log disponibile",
|
||||
"delete_all_logs": "Cancella tutti i log"
|
||||
},
|
||||
"languages": {
|
||||
"title": "Lingue",
|
||||
"app_language": "Lingua dell'App",
|
||||
"app_language_description": "Selezione la lingua dell'app.",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"filters": {
|
||||
"genres": "Generi",
|
||||
"years": "Anni",
|
||||
"sort_by": "Ordina per",
|
||||
"sort_order": "Criterio di ordinamento",
|
||||
"tags": "Tag"
|
||||
}
|
||||
},
|
||||
"favorites": {
|
||||
"series": "Serie TV",
|
||||
"movies": "Film",
|
||||
"episodes": "Episodi",
|
||||
"videos": "Video",
|
||||
"boxsets": "Boxset",
|
||||
"playlists": "Playlist"
|
||||
},
|
||||
"custom_links": {
|
||||
"no_links": "Nessun link"
|
||||
},
|
||||
"player": {
|
||||
"error": "Errore",
|
||||
"failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream",
|
||||
"an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.",
|
||||
"client_error": "Errore del client",
|
||||
"could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast",
|
||||
"message_from_server": "Messaggio dal server: {{messagge}}",
|
||||
"video_has_finished_playing": "La riproduzione del video è terminata!",
|
||||
"no_video_source": "Nessuna sorgente video...",
|
||||
"next_episode": "Prossimo Episodio",
|
||||
"refresh_tracks": "Aggiorna tracce",
|
||||
"subtitle_tracks": "Tracce di sottotitoli:",
|
||||
"audio_tracks": "Tracce audio:",
|
||||
"playback_state": "Stato della riproduzione:",
|
||||
"no_data_available": "Nessun dato disponibile",
|
||||
"index": "Indice:"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Il prossimo",
|
||||
"no_items_to_display": "Nessun elemento da visualizzare",
|
||||
"cast_and_crew": "Cast e Equipaggio",
|
||||
"series": "Serie",
|
||||
"seasons": "Stagioni",
|
||||
"season": "Stagione",
|
||||
"no_episodes_for_this_season": "Nessun episodio per questa stagione",
|
||||
"overview": "Panoramica",
|
||||
"more_with": "Altri con {{name}}",
|
||||
"similar_items": "Elementi simili",
|
||||
"no_similar_items_found": "Non sono stati trovati elementi simili",
|
||||
"video": "Video",
|
||||
"more_details": "Più dettagli",
|
||||
"quality": "Qualità",
|
||||
"audio": "Audio",
|
||||
"subtitles": "Sottotitoli",
|
||||
"show_more": "Mostra di più",
|
||||
"show_less": "Mostra di meno",
|
||||
"appeared_in": "Apparso in",
|
||||
"could_not_load_item": "Impossibile caricare l'elemento",
|
||||
"none": "Nessuno",
|
||||
"download": {
|
||||
"download_season": "Scarica Stagione",
|
||||
"download_series": "Scarica Serie",
|
||||
"download_episode": "Scarica Episodio",
|
||||
"download_movie": "Scarica Film",
|
||||
"download_x_item": "Scarica {{item_count}} elementi",
|
||||
"download_button": "Scarica",
|
||||
"using_optimized_server": "Utilizzando il server di ottimizzazione",
|
||||
"using_default_method": "Utilizzando il metodo predefinito"
|
||||
}
|
||||
},
|
||||
"live_tv": {
|
||||
"next": "Prossimo",
|
||||
"previous": "Precedente",
|
||||
"live_tv": "TV in diretta",
|
||||
"coming_soon": "Prossimamente",
|
||||
"on_now": "In onda ora",
|
||||
"shows": "Programmi",
|
||||
"movies": "Film",
|
||||
"sports": "Sport",
|
||||
"for_kids": "Per Bambini",
|
||||
"news": "Notiziari"
|
||||
},
|
||||
"jellyseerr":{
|
||||
"confirm": "Conferma",
|
||||
"cancel": "Cancella",
|
||||
"yes": "Si",
|
||||
"whats_wrong": "Cosa c'è che non va?",
|
||||
"issue_type": "Tipo di problema",
|
||||
"select_an_issue": "Seleziona un problema",
|
||||
"types": "Tipi",
|
||||
"describe_the_issue": "(facoltativo) Descrivere il problema...",
|
||||
"submit_button": "Invia",
|
||||
"report_issue_button": "Segnalare il problema",
|
||||
"request_button": "Richiedi",
|
||||
"are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?",
|
||||
"failed_to_login": "Accesso non riuscito",
|
||||
"cast": "Cast",
|
||||
"details": "Dettagli",
|
||||
"status": "Stato",
|
||||
"original_title": "Titolo originale",
|
||||
"series_type": "Tipo di Serie",
|
||||
"release_dates": "Date di Uscita",
|
||||
"first_air_date": "Prima Data di Messa in Onda",
|
||||
"next_air_date": "Prossima Data di Messa in Onda",
|
||||
"revenue": "Ricavi",
|
||||
"budget": "Budget",
|
||||
"original_language": "Lingua Originale",
|
||||
"production_country": "Paese di Produzione",
|
||||
"studios": "Studio",
|
||||
"network": "Network",
|
||||
"currently_streaming_on": "Attualmente in streaming su",
|
||||
"advanced": "Avanzate",
|
||||
"request_as": "Richiedi Come",
|
||||
"tags": "Tag",
|
||||
"quality_profile": "Profilo qualità",
|
||||
"root_folder": "Cartella radice",
|
||||
"season_x": "Stagione {{seasons}}",
|
||||
"season_number": "Stagione {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Episodio",
|
||||
"born": "Nato",
|
||||
"appearances": "Aspetto",
|
||||
"toasts": {
|
||||
"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.",
|
||||
"failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr",
|
||||
"issue_submitted": "Problema inviato!",
|
||||
"requested_item": "Richiesto {{item}}!",
|
||||
"you_dont_have_permission_to_request": "Non hai il permesso di richiedere!",
|
||||
"something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!"
|
||||
"error_deleting_files": "Errore nella cancellazione dei file",
|
||||
"background_downloads_enabled": "Scaricamento in background abilitato",
|
||||
"background_downloads_disabled": "Scaricamento in background disabilitato",
|
||||
"connected": "Connesso",
|
||||
"could_not_connect": "Non è stato possibile connettersi",
|
||||
"invalid_url": "URL invalido"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"home": "Home",
|
||||
"search": "Cerca",
|
||||
"library": "Libreria",
|
||||
"custom_links": "Collegamenti personalizzati",
|
||||
"favorites": "Preferiti"
|
||||
"downloads": {
|
||||
"downloads_title": "Scaricati",
|
||||
"tvseries": "Serie TV",
|
||||
"movies": "Film",
|
||||
"queue": "Coda",
|
||||
"queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app",
|
||||
"no_items_in_queue": "Nessun elemento in coda",
|
||||
"no_downloaded_items": "Nessun elemento scaricato",
|
||||
"delete_all_movies_button": "Cancella tutti i film",
|
||||
"delete_all_tvseries_button": "Cancella tutte le serie TV",
|
||||
"delete_all_button": "Cancella tutti",
|
||||
"active_download": "Scaricamento in corso",
|
||||
"no_active_downloads": "Nessun scaricamento in corso",
|
||||
"active_downloads": "Scaricamenti in corso",
|
||||
"new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti",
|
||||
"new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.",
|
||||
"back": "Indietro",
|
||||
"delete": "Cancella",
|
||||
"something_went_wrong": "Qualcosa è andato storto",
|
||||
"could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin",
|
||||
"eta": "ETA {{eta}}",
|
||||
"methods": "Metodi",
|
||||
"toasts": {
|
||||
"you_are_not_allowed_to_download_files": "Non è consentito scaricare file.",
|
||||
"deleted_all_movies_successfully": "Cancellati tutti i film con successo!",
|
||||
"failed_to_delete_all_movies": "Impossibile eliminare tutti i film",
|
||||
"deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!",
|
||||
"failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV",
|
||||
"download_cancelled": "Scaricamento annullato",
|
||||
"could_not_cancel_download": "Impossibile annullare lo scaricamento",
|
||||
"download_completed": "Scaricamento completato",
|
||||
"download_started_for": "Scaricamento iniziato per {{item}}",
|
||||
"item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato",
|
||||
"download_stated_for_item": "Scaricamento iniziato per {{item}}",
|
||||
"download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}",
|
||||
"download_completed_for_item": "Scaricamento completato per {{item}}",
|
||||
"queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione",
|
||||
"failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}",
|
||||
"server_responded_with_status_code": "Server responded with status {{statusCode}}",
|
||||
"no_response_received_from_server": "No response received from the server",
|
||||
"error_setting_up_the_request": "Error setting up the request",
|
||||
"failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto",
|
||||
"all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.",
|
||||
"an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi",
|
||||
"go_to_downloads": "Vai agli elementi scaricati"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"search_here": "Cerca qui...",
|
||||
"search": "Cerca...",
|
||||
"x_items": "{{count}} elementi",
|
||||
"library": "Libreria",
|
||||
"discover": "Scopri",
|
||||
"no_results": "Nessun risultato",
|
||||
"no_results_found_for": "Nessun risultato trovato per",
|
||||
"movies": "Film",
|
||||
"series": "Serie",
|
||||
"episodes": "Episodi",
|
||||
"collections": "Collezioni",
|
||||
"actors": "Attori",
|
||||
"request_movies": "Film Richiesti",
|
||||
"request_series": "Serie Richieste",
|
||||
"recently_added": "Aggiunti di Recente",
|
||||
"recent_requests": "Richiesti di Recente",
|
||||
"plex_watchlist": "Plex Watchlist",
|
||||
"trending": "In tendenza",
|
||||
"popular_movies": "Film Popolari",
|
||||
"movie_genres": "Generi Film",
|
||||
"upcoming_movies": "Film in arrivo",
|
||||
"studios": "Studio",
|
||||
"popular_tv": "Serie Popolari",
|
||||
"tv_genres": "Generi Televisivi",
|
||||
"upcoming_tv": "Serie in Arrivo",
|
||||
"networks": "Network",
|
||||
"tmdb_movie_keyword": "TMDB Parola chiave del film",
|
||||
"tmdb_movie_genre": "TMDB Genere Film",
|
||||
"tmdb_tv_keyword": "TMDB Parola chiave della serie",
|
||||
"tmdb_tv_genre": "TMDB Genere Televisivo",
|
||||
"tmdb_search": "TMDB Cerca",
|
||||
"tmdb_studio": "TMDB Studio",
|
||||
"tmdb_network": "TMDB Network",
|
||||
"tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film",
|
||||
"tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie"
|
||||
},
|
||||
"library": {
|
||||
"no_items_found": "Nessun elemento trovato",
|
||||
"no_results": "Nessun risultato",
|
||||
"no_libraries_found": "Nessuna libreria trovata",
|
||||
"item_types": {
|
||||
"movies": "film",
|
||||
"series": "serie TV",
|
||||
"boxsets": "cofanetti",
|
||||
"items": "elementi"
|
||||
},
|
||||
"options": {
|
||||
"display": "Display",
|
||||
"row": "Fila",
|
||||
"list": "Lista",
|
||||
"image_style": "Stile dell'immagine",
|
||||
"poster": "Poster",
|
||||
"cover": "Cover",
|
||||
"show_titles": "Mostra titoli",
|
||||
"show_stats": "Mostra statistiche"
|
||||
},
|
||||
"filters": {
|
||||
"genres": "Generi",
|
||||
"years": "Anni",
|
||||
"sort_by": "Ordina per",
|
||||
"sort_order": "Criterio di ordinamento",
|
||||
"tags": "Tag"
|
||||
}
|
||||
},
|
||||
"favorites": {
|
||||
"series": "Serie TV",
|
||||
"movies": "Film",
|
||||
"episodes": "Episodi",
|
||||
"videos": "Video",
|
||||
"boxsets": "Boxset",
|
||||
"playlists": "Playlist"
|
||||
},
|
||||
"custom_links": {
|
||||
"no_links": "Nessun link"
|
||||
},
|
||||
"player": {
|
||||
"error": "Errore",
|
||||
"failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream",
|
||||
"an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.",
|
||||
"client_error": "Errore del client",
|
||||
"could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast",
|
||||
"message_from_server": "Messaggio dal server",
|
||||
"video_has_finished_playing": "La riproduzione del video è terminata!",
|
||||
"no_video_source": "Nessuna sorgente video...",
|
||||
"next_episode": "Prossimo Episodio",
|
||||
"refresh_tracks": "Aggiorna tracce",
|
||||
"subtitle_tracks": "Tracce di sottotitoli:",
|
||||
"audio_tracks": "Tracce audio:",
|
||||
"playback_state": "Stato della riproduzione:",
|
||||
"no_data_available": "Nessun dato disponibile",
|
||||
"index": "Indice:"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Il prossimo",
|
||||
"no_items_to_display": "Nessun elemento da visualizzare",
|
||||
"cast_and_crew": "Cast e Equipaggio",
|
||||
"series": "Serie",
|
||||
"seasons": "Stagioni",
|
||||
"season": "Stagione",
|
||||
"no_episodes_for_this_season": "Nessun episodio per questa stagione",
|
||||
"overview": "Panoramica",
|
||||
"more_with": "Altri con {{name}}",
|
||||
"similar_items": "Elementi simili",
|
||||
"no_similar_items_found": "Non sono stati trovati elementi simili",
|
||||
"video": "Video",
|
||||
"more_details": "Più dettagli",
|
||||
"quality": "Qualità",
|
||||
"audio": "Audio",
|
||||
"subtitles": "Sottotitoli",
|
||||
"show_more": "Mostra di più",
|
||||
"show_less": "Mostra di meno",
|
||||
"appeared_in": "Apparso in",
|
||||
"could_not_load_item": "Impossibile caricare l'elemento",
|
||||
"none": "Nessuno",
|
||||
"download": {
|
||||
"download_season": "Scarica Stagione",
|
||||
"download_series": "Scarica Serie",
|
||||
"download_episode": "Scarica Episodio",
|
||||
"download_movie": "Scarica Film",
|
||||
"download_x_item": "Scarica {{item_count}} elementi",
|
||||
"download_button": "Scarica",
|
||||
"using_optimized_server": "Utilizzando il server di ottimizzazione",
|
||||
"using_default_method": "Utilizzando il metodo predefinito"
|
||||
}
|
||||
},
|
||||
"live_tv": {
|
||||
"next": "Prossimo",
|
||||
"previous": "Precedente",
|
||||
"live_tv": "TV in diretta",
|
||||
"coming_soon": "Prossimamente",
|
||||
"on_now": "In onda ora",
|
||||
"shows": "Programmi",
|
||||
"movies": "Film",
|
||||
"sports": "Sport",
|
||||
"for_kids": "Per Bambini",
|
||||
"news": "Notiziari"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"confirm": "Conferma",
|
||||
"cancel": "Cancella",
|
||||
"yes": "Si",
|
||||
"whats_wrong": "Cosa c'è che non va?",
|
||||
"issue_type": "Tipo di problema",
|
||||
"select_an_issue": "Seleziona un problema",
|
||||
"types": "Tipi",
|
||||
"describe_the_issue": "(facoltativo) Descrivere il problema...",
|
||||
"submit_button": "Invia",
|
||||
"report_issue_button": "Segnalare il problema",
|
||||
"request_button": "Richiedi",
|
||||
"are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?",
|
||||
"failed_to_login": "Accesso non riuscito",
|
||||
"cast": "Cast",
|
||||
"details": "Dettagli",
|
||||
"status": "Stato",
|
||||
"original_title": "Titolo originale",
|
||||
"series_type": "Tipo di Serie",
|
||||
"release_dates": "Date di Uscita",
|
||||
"first_air_date": "Prima Data di Messa in Onda",
|
||||
"next_air_date": "Prossima Data di Messa in Onda",
|
||||
"revenue": "Ricavi",
|
||||
"budget": "Budget",
|
||||
"original_language": "Lingua Originale",
|
||||
"production_country": "Paese di Produzione",
|
||||
"studios": "Studio",
|
||||
"network": "Network",
|
||||
"currently_streaming_on": "Attualmente in streaming su",
|
||||
"advanced": "Avanzate",
|
||||
"request_as": "Richiedi Come",
|
||||
"tags": "Tag",
|
||||
"quality_profile": "Profilo qualità",
|
||||
"root_folder": "Cartella radice",
|
||||
"season_x": "Stagione {{seasons}}",
|
||||
"season_number": "Stagione {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Episodio",
|
||||
"born": "Nato",
|
||||
"appearances": "Aspetto",
|
||||
"toasts": {
|
||||
"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.",
|
||||
"failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr",
|
||||
"issue_submitted": "Problema inviato!",
|
||||
"requested_item": "Richiesto {{item}}!",
|
||||
"you_dont_have_permission_to_request": "Non hai il permesso di richiedere!",
|
||||
"something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"home": "Home",
|
||||
"search": "Cerca",
|
||||
"library": "Libreria",
|
||||
"custom_links": "Collegamenti personalizzati",
|
||||
"favorites": "Preferiti"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@
|
||||
"an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。",
|
||||
"client_error": "クライアントエラー",
|
||||
"could_not_create_stream_for_chromecast": "Chromecastのストリームを作成できませんでした",
|
||||
"message_from_server": "サーバーからのメッセージ: {{message}}",
|
||||
"message_from_server": "サーバーからのメッセージ",
|
||||
"video_has_finished_playing": "ビデオの再生が終了しました!",
|
||||
"no_video_source": "動画ソースがありません...",
|
||||
"next_episode": "次のエピソード",
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"default": "Standaard",
|
||||
"optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.",
|
||||
"read_more_about_optimized_server": "Lees meer over de optimalisatieserver.",
|
||||
"url":"URL",
|
||||
"url": "URL",
|
||||
"server_url_placeholder": "http(s)://domein.org:poort"
|
||||
},
|
||||
"plugins": {
|
||||
@@ -204,7 +204,7 @@
|
||||
"app_language_description": "Selecteer een taal voor de app.",
|
||||
"system": "Systeem"
|
||||
},
|
||||
"toasts":{
|
||||
"toasts": {
|
||||
"error_deleting_files": "Fout bij het verwijden van bestanden",
|
||||
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
|
||||
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld",
|
||||
@@ -343,7 +343,7 @@
|
||||
"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",
|
||||
"could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast",
|
||||
"message_from_server": "Bericht van de server: {{message}}",
|
||||
"message_from_server": "Bericht van de server",
|
||||
"video_has_finished_playing": "Video is gedaan met spelen!",
|
||||
"no_video_source": "Geen video bron...",
|
||||
"next_episode": "Volgende Aflevering",
|
||||
@@ -399,7 +399,7 @@
|
||||
"for_kids": "Voor kinderen",
|
||||
"news": "Nieuws"
|
||||
},
|
||||
"jellyseerr":{
|
||||
"jellyseerr": {
|
||||
"confirm": "Bevestig",
|
||||
"cancel": "Annuleer",
|
||||
"yes": "Ja",
|
||||
|
||||
@@ -342,7 +342,7 @@
|
||||
"an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。",
|
||||
"client_error": "客户端错误",
|
||||
"could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流",
|
||||
"message_from_server": "来自服务器的消息:{{message}}",
|
||||
"message_from_server": "来自服务器的消息",
|
||||
"video_has_finished_playing": "视频播放完成!",
|
||||
"no_video_source": "无视频来源...",
|
||||
"next_episode": "下一集",
|
||||
|
||||
@@ -342,7 +342,7 @@
|
||||
"an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。",
|
||||
"client_error": "客戶端錯誤",
|
||||
"could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流",
|
||||
"message_from_server": "來自伺服器的消息:{{message}}",
|
||||
"message_from_server": "來自伺服器的消息",
|
||||
"video_has_finished_playing": "影片播放完畢!",
|
||||
"no_video_source": "無影片來源...",
|
||||
"next_episode": "下一集",
|
||||
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
|
||||
VideoCodec:
|
||||
"h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video",
|
||||
AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma",
|
||||
AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma,dts",
|
||||
},
|
||||
{
|
||||
Type: MediaTypes.Audio,
|
||||
|
||||
Reference in New Issue
Block a user