forked from Ninjalama/streamyfin_mirror
Compare commits
1 Commits
feature/mp
...
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.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true
|
||||||
},
|
},
|
||||||
|
"prettier.printWidth": 120,
|
||||||
"[swift]": {
|
"[swift]": {
|
||||||
"editor.defaultFormatter": "sswg.swift-lang"
|
"editor.defaultFormatter": "sswg.swift-lang"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,40 +12,26 @@ import {
|
|||||||
ProgressUpdatePayload,
|
ProgressUpdatePayload,
|
||||||
VlcPlayerViewRef,
|
VlcPlayerViewRef,
|
||||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
} 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 { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import native from "@/utils/profiles/native";
|
import native from "@/utils/profiles/native";
|
||||||
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
||||||
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
||||||
import {
|
import { getPlaystateApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
getPlaystateApi,
|
|
||||||
getUserLibraryApi,
|
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useGlobalSearchParams, useNavigation } from "expo-router";
|
import { useGlobalSearchParams, useNavigation } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, {
|
import React, { useCallback, useMemo, useRef, useState, useEffect } from "react";
|
||||||
useCallback,
|
import { Alert, View, Platform } from "react-native";
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
} from "react";
|
|
||||||
import { Alert, View, AppState, AppStateStatus, Platform } from "react-native";
|
|
||||||
import { useSharedValue } from "react-native-reanimated";
|
import { useSharedValue } from "react-native-reanimated";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
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() {
|
export default function page() {
|
||||||
console.log("Direct Player");
|
|
||||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
@@ -93,111 +79,101 @@ export default function page() {
|
|||||||
offline: string;
|
offline: string;
|
||||||
}>();
|
}>();
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
const offline = offlineStr === "true";
|
const offline = offlineStr === "true";
|
||||||
|
|
||||||
const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined;
|
const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined;
|
||||||
const subtitleIndex = subtitleIndexStr ? parseInt(subtitleIndexStr, 10) : -1;
|
const subtitleIndex = subtitleIndexStr ? parseInt(subtitleIndexStr, 10) : -1;
|
||||||
const bitrateValue = bitrateValueStr
|
const bitrateValue = bitrateValueStr ? parseInt(bitrateValueStr, 10) : BITRATES[0].value;
|
||||||
? parseInt(bitrateValueStr, 10)
|
|
||||||
: BITRATES[0].value;
|
|
||||||
|
|
||||||
const {
|
const [item, setItem] = useState<BaseItemDto | null>(null);
|
||||||
data: item,
|
const [itemStatus, setItemStatus] = useState({
|
||||||
isLoading: isLoadingItem,
|
isLoading: true,
|
||||||
isError: isErrorItem,
|
isError: false,
|
||||||
} = 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 [stream, setStream] = useState<{
|
|
||||||
mediaSource: MediaSourceInfo;
|
|
||||||
url: string;
|
|
||||||
sessionId: string | undefined;
|
|
||||||
} | null>(null);
|
|
||||||
const [isLoadingStream, setIsLoadingStream] = useState(true);
|
|
||||||
const [isErrorStream, setIsErrorStream] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStream = async () => {
|
const fetchItemData = async () => {
|
||||||
setIsLoadingStream(true);
|
setItemStatus({ isLoading: true, isError: false });
|
||||||
setIsErrorStream(false);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let fetchedItem: BaseItemDto | null = null;
|
||||||
if (offline && !Platform.isTV) {
|
if (offline && !Platform.isTV) {
|
||||||
const data = await getDownloadedItem.getDownloadedItem(itemId);
|
const data = await getDownloadedItem.getDownloadedItem(itemId);
|
||||||
if (!data?.mediaSource) {
|
if (data) fetchedItem = data.item as BaseItemDto;
|
||||||
setStream(null);
|
} else {
|
||||||
return;
|
const res = await getUserLibraryApi(api!).getItem({
|
||||||
}
|
itemId,
|
||||||
|
userId: user?.Id,
|
||||||
const url = await getDownloadedFileUrl(data.item.Id!);
|
});
|
||||||
|
fetchedItem = res.data;
|
||||||
if (item) {
|
|
||||||
setStream({
|
|
||||||
mediaSource: data.mediaSource as MediaSourceInfo,
|
|
||||||
url,
|
|
||||||
sessionId: undefined,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setItem(fetchedItem);
|
||||||
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,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching stream:", error);
|
console.error("Failed to fetch item:", error);
|
||||||
setIsErrorStream(true);
|
setItemStatus({ isLoading: false, isError: true });
|
||||||
setStream(null);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingStream(false);
|
setItemStatus({ isLoading: false, isError: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchStream();
|
if (itemId) {
|
||||||
}, [itemId, mediaSourceId]);
|
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 () => {
|
const togglePlay = useCallback(async () => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
@@ -208,37 +184,11 @@ export default function page() {
|
|||||||
} else {
|
} else {
|
||||||
videoRef.current?.play();
|
videoRef.current?.play();
|
||||||
}
|
}
|
||||||
|
}, [isPlaying, api, item, stream, videoRef, audioIndex, subtitleIndex, mediaSourceId, offline, progress]);
|
||||||
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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const reportPlaybackStopped = useCallback(async () => {
|
const reportPlaybackStopped = useCallback(async () => {
|
||||||
if (offline) return;
|
if (offline) return;
|
||||||
|
|
||||||
const currentTimeInTicks = msToTicks(progress.get());
|
const currentTimeInTicks = msToTicks(progress.get());
|
||||||
|
|
||||||
await getPlaystateApi(api!).onPlaybackStopped({
|
await getPlaystateApi(api!).onPlaybackStopped({
|
||||||
itemId: item?.Id!,
|
itemId: item?.Id!,
|
||||||
mediaSourceId: mediaSourceId,
|
mediaSourceId: mediaSourceId,
|
||||||
@@ -255,12 +205,18 @@ export default function page() {
|
|||||||
videoRef.current?.stop();
|
videoRef.current?.stop();
|
||||||
}, [videoRef, reportPlaybackStopped]);
|
}, [videoRef, reportPlaybackStopped]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
||||||
|
return () => {
|
||||||
|
beforeRemoveListener();
|
||||||
|
};
|
||||||
|
}, [navigation, stop]);
|
||||||
|
|
||||||
const onProgress = useCallback(
|
const onProgress = useCallback(
|
||||||
async (data: ProgressUpdatePayload) => {
|
async (data: ProgressUpdatePayload) => {
|
||||||
if (isSeeking.get() || isPlaybackStopped) return;
|
if (isSeeking.get() || isPlaybackStopped) return;
|
||||||
|
|
||||||
const { currentTime } = data.nativeEvent;
|
const { currentTime } = data.nativeEvent;
|
||||||
|
|
||||||
if (isBuffering) {
|
if (isBuffering) {
|
||||||
setIsBuffering(false);
|
setIsBuffering(false);
|
||||||
}
|
}
|
||||||
@@ -284,9 +240,57 @@ export default function page() {
|
|||||||
playSessionId: stream.sessionId,
|
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({
|
useWebSocket({
|
||||||
isPlaying: isPlaying,
|
isPlaying: isPlaying,
|
||||||
togglePlay: togglePlay,
|
togglePlay: togglePlay,
|
||||||
@@ -294,75 +298,41 @@ export default function page() {
|
|||||||
offline,
|
offline,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
const onPlaybackStateChanged = useCallback(
|
||||||
const { pipStarted } = e.nativeEvent;
|
async (e: PlaybackStatePayload) => {
|
||||||
setIsPipStarted(pipStarted);
|
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onPlaybackStateChanged = useCallback(async (e: PlaybackStatePayload) => {
|
if (state === "Playing") {
|
||||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
setIsPlaying(true);
|
||||||
|
await changePlaybackState(true);
|
||||||
|
if (!Platform.isTV) await activateKeepAwakeAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state === "Playing") {
|
if (state === "Paused") {
|
||||||
setIsPlaying(true);
|
setIsPlaying(false);
|
||||||
if (!Platform.isTV) await activateKeepAwakeAsync()
|
await changePlaybackState(false);
|
||||||
return;
|
if (!Platform.isTV) await deactivateKeepAwake();
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state === "Paused") {
|
if (isPlaying) {
|
||||||
setIsPlaying(false);
|
setIsPlaying(true);
|
||||||
if (!Platform.isTV) await deactivateKeepAwake();
|
setIsBuffering(false);
|
||||||
return;
|
} else if (isBuffering) {
|
||||||
}
|
setIsBuffering(true);
|
||||||
|
}
|
||||||
if (isPlaying) {
|
},
|
||||||
setIsPlaying(true);
|
[changePlaybackState]
|
||||||
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
|
|
||||||
);
|
);
|
||||||
const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex);
|
|
||||||
|
|
||||||
const notTranscoding = !stream?.mediaSource.TranscodingUrl;
|
const allAudio = stream?.mediaSource.MediaStreams?.filter((audio) => audio.Type === "Audio") || [];
|
||||||
if (
|
|
||||||
chosenSubtitleTrack &&
|
|
||||||
(notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
|
|
||||||
) {
|
|
||||||
const finalIndex = notTranscoding
|
|
||||||
? allSubs.indexOf(chosenSubtitleTrack)
|
|
||||||
: textSubs.indexOf(chosenSubtitleTrack);
|
|
||||||
initOptions.push(`--sub-track=${finalIndex}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notTranscoding && chosenAudioTrack) {
|
// Move all the external subtitles last, because vlc places them last.
|
||||||
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
const allSubs =
|
||||||
}
|
stream?.mediaSource.MediaStreams?.filter((sub) => sub.Type === "Subtitle").sort(
|
||||||
|
(a, b) => Number(a.IsExternal) - Number(b.IsExternal)
|
||||||
|
) || [];
|
||||||
|
|
||||||
const externalSubtitles = allSubs
|
const externalSubtitles = allSubs
|
||||||
.filter((sub: any) => sub.DeliveryMethod === "External")
|
.filter((sub: any) => sub.DeliveryMethod === "External")
|
||||||
@@ -371,6 +341,22 @@ export default function page() {
|
|||||||
DeliveryUrl: api?.basePath + sub.DeliveryUrl,
|
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);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
|
|
||||||
// Add useEffect to handle mounting
|
// Add useEffect to handle mounting
|
||||||
@@ -379,22 +365,15 @@ export default function page() {
|
|||||||
return () => setIsMounted(false);
|
return () => setIsMounted(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
if (itemStatus.isLoading || streamStatus.isLoading) {
|
||||||
useEffect(() => {
|
|
||||||
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
|
||||||
return () => {
|
|
||||||
beforeRemoveListener();
|
|
||||||
};
|
|
||||||
}, [navigation]);
|
|
||||||
|
|
||||||
if (!item || isLoadingItem || !stream)
|
|
||||||
return (
|
return (
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
||||||
<Loader />
|
<Loader />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isErrorItem || isErrorStream)
|
if (!item || !stream || itemStatus.isError || streamStatus.isError)
|
||||||
return (
|
return (
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
||||||
<Text className="text-white">{t("player.error")}</Text>
|
<Text className="text-white">{t("player.error")}</Text>
|
||||||
@@ -435,10 +414,7 @@ export default function page() {
|
|||||||
}}
|
}}
|
||||||
onVideoError={(e) => {
|
onVideoError={(e) => {
|
||||||
console.error("Video Error:", e.nativeEvent);
|
console.error("Video Error:", e.nativeEvent);
|
||||||
Alert.alert(
|
Alert.alert(t("player.error"), t("player.an_error_occured_while_playing_the_video"));
|
||||||
t("player.error"),
|
|
||||||
t("player.an_error_occured_while_playing_the_video")
|
|
||||||
);
|
|
||||||
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -470,7 +446,6 @@ export default function page() {
|
|||||||
setSubtitleTrack={videoRef.current.setSubtitleTrack}
|
setSubtitleTrack={videoRef.current.setSubtitleTrack}
|
||||||
setSubtitleURL={videoRef.current.setSubtitleURL}
|
setSubtitleURL={videoRef.current.setSubtitleURL}
|
||||||
setAudioTrack={videoRef.current.setAudioTrack}
|
setAudioTrack={videoRef.current.setAudioTrack}
|
||||||
stop={stop}
|
|
||||||
isVlc
|
isVlc
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
(q: string, bitrateValue: number | undefined) => {
|
(q: string) => {
|
||||||
router.push(`/player/direct-player?${q}`);
|
router.push(`/player/direct-player?${q}`);
|
||||||
},
|
},
|
||||||
[router]
|
[router]
|
||||||
@@ -94,7 +94,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
goToPlayer(queryString);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
goToPlayer(queryString);
|
||||||
break;
|
break;
|
||||||
case cancelButtonIndex:
|
case cancelButtonIndex:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ const ANIMATION_DURATION = 500;
|
|||||||
const MIN_PLAYBACK_WIDTH = 15;
|
const MIN_PLAYBACK_WIDTH = 15;
|
||||||
|
|
||||||
export const PlayButton: React.FC<Props> = ({
|
export const PlayButton: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
selectedOptions,
|
selectedOptions,
|
||||||
...props
|
...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
(q: string, bitrateValue: number | undefined) => {
|
(q: string) => {
|
||||||
router.push(`/player/direct-player?${q}`);
|
router.push(`/player/direct-player?${q}`);
|
||||||
},
|
},
|
||||||
[router]
|
[router]
|
||||||
@@ -78,7 +78,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
goToPlayer(queryString, selectedOptions.bitrate?.value);
|
goToPlayer(queryString);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,9 +88,9 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
if (userData && userData.PlaybackPositionTicks) {
|
if (userData && userData.PlaybackPositionTicks) {
|
||||||
return userData.PlaybackPositionTicks > 0
|
return userData.PlaybackPositionTicks > 0
|
||||||
? Math.max(
|
? Math.max(
|
||||||
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
||||||
MIN_PLAYBACK_WIDTH
|
MIN_PLAYBACK_WIDTH
|
||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -87,40 +87,38 @@ interface Props {
|
|||||||
setSubtitleURL?: (url: string, customName: string) => void;
|
setSubtitleURL?: (url: string, customName: string) => void;
|
||||||
setSubtitleTrack?: (index: number) => void;
|
setSubtitleTrack?: (index: number) => void;
|
||||||
setAudioTrack?: (index: number) => void;
|
setAudioTrack?: (index: number) => void;
|
||||||
stop: (() => Promise<void>) | (() => void);
|
|
||||||
isVlc?: boolean;
|
isVlc?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTROLS_TIMEOUT = 4000;
|
const CONTROLS_TIMEOUT = 4000;
|
||||||
|
|
||||||
export const Controls: React.FC<Props> = ({
|
export const Controls: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
seek,
|
seek,
|
||||||
startPictureInPicture,
|
startPictureInPicture,
|
||||||
play,
|
play,
|
||||||
pause,
|
pause,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
isSeeking,
|
isSeeking,
|
||||||
progress,
|
progress,
|
||||||
isBuffering,
|
isBuffering,
|
||||||
cacheProgress,
|
cacheProgress,
|
||||||
showControls,
|
showControls,
|
||||||
setShowControls,
|
setShowControls,
|
||||||
ignoreSafeAreas,
|
ignoreSafeAreas,
|
||||||
setIgnoreSafeAreas,
|
setIgnoreSafeAreas,
|
||||||
mediaSource,
|
mediaSource,
|
||||||
isVideoLoaded,
|
isVideoLoaded,
|
||||||
getAudioTracks,
|
getAudioTracks,
|
||||||
getSubtitleTracks,
|
getSubtitleTracks,
|
||||||
setSubtitleURL,
|
setSubtitleURL,
|
||||||
setSubtitleTrack,
|
setSubtitleTrack,
|
||||||
setAudioTrack,
|
setAudioTrack,
|
||||||
stop,
|
offline = false,
|
||||||
offline = false,
|
enableTrickplay = true,
|
||||||
enableTrickplay = true,
|
isVlc = false,
|
||||||
isVlc = false,
|
}) => {
|
||||||
}) => {
|
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -189,75 +187,60 @@ export const Controls: React.FC<Props> = ({
|
|||||||
isVlc
|
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(() => {
|
const goToPreviousItem = useCallback(() => {
|
||||||
if (!previousItem || !settings) return;
|
if (!previousItem) return;
|
||||||
|
goToItemCommon(previousItem);
|
||||||
lightHapticFeedback();
|
}, [previousItem, goToItemCommon]);
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const goToNextItem = useCallback(() => {
|
const goToNextItem = useCallback(() => {
|
||||||
if (!nextItem || !settings) return;
|
if (!nextItem) return;
|
||||||
|
goToItemCommon(nextItem);
|
||||||
|
}, [nextItem, goToItemCommon]);
|
||||||
|
|
||||||
lightHapticFeedback();
|
const goToItem = useCallback(
|
||||||
|
async (itemId: string) => {
|
||||||
const previousIndexes: previousIndexes = {
|
const gotoItem = await getItemById(api, itemId);
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
if (!gotoItem) return;
|
||||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
goToItemCommon(gotoItem);
|
||||||
};
|
},
|
||||||
|
[goToItemCommon, api]
|
||||||
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 updateTimes = useCallback(
|
const updateTimes = useCallback(
|
||||||
(currentProgress: number, maxValue: number) => {
|
(currentProgress: number, maxValue: number) => {
|
||||||
@@ -381,49 +364,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [settings, isPlaying, isVlc]);
|
}, [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(() => {
|
const toggleIgnoreSafeAreas = useCallback(() => {
|
||||||
setIgnoreSafeAreas((prev) => !prev);
|
setIgnoreSafeAreas((prev) => !prev);
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
@@ -497,7 +437,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
}, [trickPlayUrl, trickplayInfo, time]);
|
}, [trickPlayUrl, trickplayInfo, time]);
|
||||||
|
|
||||||
const onClose = async () => {
|
const onClose = async () => {
|
||||||
stop();
|
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
await ScreenOrientation.lockAsync(
|
await ScreenOrientation.lockAsync(
|
||||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||||
@@ -549,7 +488,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
setSubtitleTrack={setSubtitleTrack}
|
setSubtitleTrack={setSubtitleTrack}
|
||||||
setSubtitleURL={setSubtitleURL}
|
setSubtitleURL={setSubtitleURL}
|
||||||
>
|
>
|
||||||
<DropdownView showControls={showControls} />
|
<DropdownView />
|
||||||
</VideoProvider>
|
</VideoProvider>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -790,8 +729,8 @@ export const Controls: React.FC<Props> = ({
|
|||||||
!nextItem
|
!nextItem
|
||||||
? false
|
? false
|
||||||
: isVlc
|
: isVlc
|
||||||
? remainingTime < 10000
|
? remainingTime < 10000
|
||||||
: remainingTime < 10
|
: remainingTime < 10
|
||||||
}
|
}
|
||||||
onFinish={goToNextItem}
|
onFinish={goToNextItem}
|
||||||
onPress={goToNextItem}
|
onPress={goToNextItem}
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
import { TrackInfo } from "@/modules/vlc-player";
|
import { TrackInfo } from "@/modules/vlc-player";
|
||||||
import {
|
import React, { createContext, useContext, useState, ReactNode, useEffect, useMemo } from "react";
|
||||||
BaseItemDto,
|
|
||||||
MediaSourceInfo,
|
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
|
||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useState,
|
|
||||||
ReactNode,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
} from "react";
|
|
||||||
import { useControlContext } from "./ControlContext";
|
import { useControlContext } from "./ControlContext";
|
||||||
import { Track } from "../types";
|
import { Track } from "../types";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
@@ -27,14 +16,8 @@ const VideoContext = createContext<VideoContextProps | undefined>(undefined);
|
|||||||
|
|
||||||
interface VideoProviderProps {
|
interface VideoProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
getAudioTracks:
|
getAudioTracks: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]) | undefined;
|
||||||
| (() => Promise<TrackInfo[] | null>)
|
getSubtitleTracks: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]) | undefined;
|
||||||
| (() => TrackInfo[])
|
|
||||||
| undefined;
|
|
||||||
getSubtitleTracks:
|
|
||||||
| (() => Promise<TrackInfo[] | null>)
|
|
||||||
| (() => TrackInfo[])
|
|
||||||
| undefined;
|
|
||||||
setAudioTrack: ((index: number) => void) | undefined;
|
setAudioTrack: ((index: number) => void) | undefined;
|
||||||
setSubtitleTrack: ((index: number) => void) | undefined;
|
setSubtitleTrack: ((index: number) => void) | undefined;
|
||||||
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
|
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
|
||||||
@@ -55,23 +38,19 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
||||||
const mediaSource = ControlContext?.mediaSource;
|
const mediaSource = ControlContext?.mediaSource;
|
||||||
|
|
||||||
const allSubs =
|
const allSubs = mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || [];
|
||||||
mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") || [];
|
|
||||||
|
|
||||||
const { itemId, audioIndex, bitrateValue, subtitleIndex } =
|
const { itemId, audioIndex, bitrateValue, subtitleIndex } = useLocalSearchParams<{
|
||||||
useLocalSearchParams<{
|
itemId: string;
|
||||||
itemId: string;
|
audioIndex: string;
|
||||||
audioIndex: string;
|
subtitleIndex: string;
|
||||||
subtitleIndex: string;
|
mediaSourceId: string;
|
||||||
mediaSourceId: string;
|
bitrateValue: string;
|
||||||
bitrateValue: string;
|
}>();
|
||||||
}>();
|
|
||||||
|
|
||||||
const onTextBasedSubtitle = useMemo(
|
const onTextBasedSubtitle = useMemo(
|
||||||
() =>
|
() =>
|
||||||
allSubs.find(
|
allSubs.find((s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream) || subtitleIndex === "-1",
|
||||||
(s) => s.Index?.toString() === subtitleIndex && s.IsTextSubtitleStream
|
|
||||||
) || subtitleIndex === "-1",
|
|
||||||
[allSubs, subtitleIndex]
|
[allSubs, subtitleIndex]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,21 +74,14 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
router.replace(`player/direct-player?${queryParams}`);
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTrackParams = (
|
const setTrackParams = (type: "audio" | "subtitle", index: number, serverIndex: number) => {
|
||||||
type: "audio" | "subtitle",
|
|
||||||
index: number,
|
|
||||||
serverIndex: number
|
|
||||||
) => {
|
|
||||||
const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack;
|
const setTrack = type === "audio" ? setAudioTrack : setSubtitleTrack;
|
||||||
const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex";
|
const paramKey = type === "audio" ? "audioIndex" : "subtitleIndex";
|
||||||
|
|
||||||
// If we're transcoding and we're going from a image based subtitle
|
// 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.
|
// to a text based subtitle, we need to change the player params.
|
||||||
|
|
||||||
const shouldChangePlayerParams =
|
const shouldChangePlayerParams = type === "subtitle" && mediaSource?.TranscodingUrl && !onTextBasedSubtitle;
|
||||||
type === "subtitle" &&
|
|
||||||
mediaSource?.TranscodingUrl &&
|
|
||||||
!onTextBasedSubtitle;
|
|
||||||
|
|
||||||
console.log("Set player params", index, serverIndex);
|
console.log("Set player params", index, serverIndex);
|
||||||
if (shouldChangePlayerParams) {
|
if (shouldChangePlayerParams) {
|
||||||
@@ -129,23 +101,22 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
if (getSubtitleTracks) {
|
if (getSubtitleTracks) {
|
||||||
const subtitleData = await 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;
|
let textSubIndex = 0;
|
||||||
const subtitles: Track[] = allSubs?.map((sub) => {
|
const processedSubs: Track[] = sortedSubs?.map((sub) => {
|
||||||
// Always increment for non-transcoding subtitles
|
// Always increment for non-transcoding subtitles
|
||||||
// Only increment for text-based subtitles when transcoding
|
// Only increment for text-based subtitles when transcoding
|
||||||
const shouldIncrement =
|
const shouldIncrement = !mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream;
|
||||||
!mediaSource?.TranscodingUrl || sub.IsTextSubtitleStream;
|
|
||||||
|
|
||||||
const displayTitle = sub.DisplayTitle || "Undefined Subtitle";
|
|
||||||
const vlcIndex = subtitleData?.at(textSubIndex)?.index ?? -1;
|
const vlcIndex = subtitleData?.at(textSubIndex)?.index ?? -1;
|
||||||
|
|
||||||
const finalIndex = shouldIncrement ? vlcIndex : sub.Index ?? -1;
|
const finalIndex = shouldIncrement ? vlcIndex : sub.Index ?? -1;
|
||||||
|
|
||||||
if (shouldIncrement) textSubIndex++;
|
if (shouldIncrement) textSubIndex++;
|
||||||
return {
|
return {
|
||||||
name: displayTitle,
|
name: sub.DisplayTitle || "Undefined Subtitle",
|
||||||
index: sub.Index ?? -1,
|
index: sub.Index ?? -1,
|
||||||
originalIndex: finalIndex,
|
|
||||||
setTrack: () =>
|
setTrack: () =>
|
||||||
shouldIncrement
|
shouldIncrement
|
||||||
? setTrackParams("subtitle", finalIndex, sub.Index ?? -1)
|
? 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
|
// Add a "Disable Subtitles" option
|
||||||
subtitles.unshift({
|
subtitles.unshift({
|
||||||
name: "Disable",
|
name: "Disable",
|
||||||
@@ -164,36 +138,25 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
? setTrackParams("subtitle", -1, -1)
|
? setTrackParams("subtitle", -1, -1)
|
||||||
: setPlayerParams({ chosenSubtitleIndex: "-1" }),
|
: setPlayerParams({ chosenSubtitleIndex: "-1" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
setSubtitleTracks(subtitles);
|
setSubtitleTracks(subtitles);
|
||||||
}
|
}
|
||||||
if (
|
if (getAudioTracks) {
|
||||||
getAudioTracks &&
|
|
||||||
(audioTracks === null || audioTracks.length === 0)
|
|
||||||
) {
|
|
||||||
const audioData = await 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) => {
|
const audioTracks: Track[] = allAudio?.map((audio, idx) => {
|
||||||
if (!mediaSource?.TranscodingUrl) {
|
if (!mediaSource?.TranscodingUrl) {
|
||||||
const vlcIndex = audioData?.at(idx)?.index ?? -1;
|
const vlcIndex = audioData?.at(idx)?.index ?? -1;
|
||||||
return {
|
return {
|
||||||
name: audio.DisplayTitle ?? "Undefined Audio",
|
name: audio.DisplayTitle ?? "Undefined Audio",
|
||||||
index: audio.Index ?? -1,
|
index: audio.Index ?? -1,
|
||||||
setTrack: () =>
|
setTrack: () => setTrackParams("audio", vlcIndex, audio.Index ?? -1),
|
||||||
setTrackParams("audio", vlcIndex, audio.Index ?? -1),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: audio.DisplayTitle ?? "Undefined Audio",
|
name: audio.DisplayTitle ?? "Undefined Audio",
|
||||||
index: audio.Index ?? -1,
|
index: audio.Index ?? -1,
|
||||||
setTrack: () =>
|
setTrack: () => setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }),
|
||||||
setPlayerParams({ chosenAudioIndex: audio.Index?.toString() }),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setAudioTracks(audioTracks);
|
setAudioTracks(audioTracks);
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
import { TouchableOpacity, Platform } from "react-native";
|
import { TouchableOpacity, Platform } from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||||
import { useVideoContext } from "../contexts/VideoContext";
|
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 {
|
const DropdownView = () => {
|
||||||
showControls: boolean;
|
|
||||||
offline?: boolean; // used to disable external subs for downloads
|
|
||||||
}
|
|
||||||
|
|
||||||
const DropdownView: React.FC<DropdownViewProps> = ({
|
|
||||||
showControls,
|
|
||||||
offline = false,
|
|
||||||
}) => {
|
|
||||||
const videoContext = useVideoContext();
|
const videoContext = useVideoContext();
|
||||||
const { subtitleTracks, audioTracks } = videoContext;
|
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;
|
itemId: string;
|
||||||
audioIndex: string;
|
audioIndex: string;
|
||||||
subtitleIndex: string;
|
subtitleIndex: string;
|
||||||
@@ -25,6 +22,21 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
bitrateValue: string;
|
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 (
|
return (
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
@@ -42,9 +54,27 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger key="subtitle-trigger">
|
<DropdownMenu.SubTrigger key="qualitytrigger">Quality</DropdownMenu.SubTrigger>
|
||||||
Subtitle
|
<DropdownMenu.SubContent
|
||||||
</DropdownMenu.SubTrigger>
|
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
|
<DropdownMenu.SubContent
|
||||||
alignOffset={-10}
|
alignOffset={-10}
|
||||||
avoidCollisions={true}
|
avoidCollisions={true}
|
||||||
@@ -58,17 +88,13 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
value={subtitleIndex === sub.index.toString()}
|
value={subtitleIndex === sub.index.toString()}
|
||||||
onValueChange={() => sub.setTrack()}
|
onValueChange={() => sub.setTrack()}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>
|
<DropdownMenu.ItemTitle key={`subtitle-item-title-${idx}`}>{sub.name}</DropdownMenu.ItemTitle>
|
||||||
{sub.name}
|
|
||||||
</DropdownMenu.ItemTitle>
|
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
</DropdownMenu.Sub>
|
</DropdownMenu.Sub>
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger key="audio-trigger">
|
<DropdownMenu.SubTrigger key="audio-trigger">Audio</DropdownMenu.SubTrigger>
|
||||||
Audio
|
|
||||||
</DropdownMenu.SubTrigger>
|
|
||||||
<DropdownMenu.SubContent
|
<DropdownMenu.SubContent
|
||||||
alignOffset={-10}
|
alignOffset={-10}
|
||||||
avoidCollisions={true}
|
avoidCollisions={true}
|
||||||
@@ -82,9 +108,7 @@ const DropdownView: React.FC<DropdownViewProps> = ({
|
|||||||
value={audioIndex === track.index.toString()}
|
value={audioIndex === track.index.toString()}
|
||||||
onValueChange={() => track.setTrack()}
|
onValueChange={() => track.setTrack()}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>
|
<DropdownMenu.ItemTitle key={`audio-item-title-${idx}`}>{track.name}</DropdownMenu.ItemTitle>
|
||||||
{track.name}
|
|
||||||
</DropdownMenu.ItemTitle>
|
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Pod::Spec.new do |s|
|
|||||||
s.dependency 'ExpoModulesCore'
|
s.dependency 'ExpoModulesCore'
|
||||||
s.ios.dependency 'VLCKit', s.version
|
s.ios.dependency 'VLCKit', s.version
|
||||||
s.tvos.dependency 'VLCKit', s.version
|
s.tvos.dependency 'VLCKit', s.version
|
||||||
|
s.dependency 'Alamofire', '~> 5.10'
|
||||||
|
|
||||||
# Swift/Objective-C compatibility
|
# Swift/Objective-C compatibility
|
||||||
s.pod_target_xcconfig = {
|
s.pod_target_xcconfig = {
|
||||||
|
|||||||
@@ -459,7 +459,9 @@ extension VlcPlayerView: SimpleAppLifecycleListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Current solution to fixing black screen when re-entering application
|
// 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.isSelected = false
|
||||||
videoTrack.isSelectedExclusively = true
|
videoTrack.isSelectedExclusively = true
|
||||||
self.vlc.player.play()
|
self.vlc.player.play()
|
||||||
|
|||||||
@@ -1,458 +1,458 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"username_required": "Nome utente è obbligatorio",
|
"username_required": "Nome utente è obbligatorio",
|
||||||
"error_title": "Errore",
|
"error_title": "Errore",
|
||||||
"login_title": "Accesso",
|
"login_title": "Accesso",
|
||||||
"login_to_title": "Accedi a",
|
"login_to_title": "Accedi a",
|
||||||
"username_placeholder": "Nome utente",
|
"username_placeholder": "Nome utente",
|
||||||
"password_placeholder": "Password",
|
"password_placeholder": "Password",
|
||||||
"login_button": "Accedi",
|
"login_button": "Accedi",
|
||||||
"quick_connect": "Connessione Rapida",
|
"quick_connect": "Connessione Rapida",
|
||||||
"enter_code_to_login": "Inserire {{code}} per accedere",
|
"enter_code_to_login": "Inserire {{code}} per accedere",
|
||||||
"failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida",
|
"failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida",
|
||||||
"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_occured": "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_occured_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?"
|
||||||
|
},
|
||||||
|
"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": {
|
"settings": {
|
||||||
"enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin",
|
"settings_title": "Impostazioni",
|
||||||
"server_url_placeholder": "http(s)://tuo-server.com",
|
"log_out_button": "Esci",
|
||||||
"connect_button": "Connetti",
|
"user_info": {
|
||||||
"previous_servers": "server precedente",
|
"user_info_title": "Info utente",
|
||||||
"clear_button": "Cancella",
|
"user": "Utente",
|
||||||
"search_for_local_servers": "Ricerca dei server locali",
|
"server": "Server",
|
||||||
"searching": "Cercando...",
|
"token": "Token",
|
||||||
"servers": "Servers"
|
"app_version": "Versione dell'App"
|
||||||
},
|
|
||||||
"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": {
|
"quick_connect": {
|
||||||
"settings_title": "Impostazioni",
|
"quick_connect_title": "Connessione Rapida",
|
||||||
"log_out_button": "Esci",
|
"authorize_button": "Autorizza Connessione Rapida",
|
||||||
"user_info": {
|
"enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...",
|
||||||
"user_info_title": "Info utente",
|
"success": "Successo",
|
||||||
"user": "Utente",
|
"quick_connect_autorized": "Connessione Rapida autorizzata",
|
||||||
"server": "Server",
|
"error": "Errore",
|
||||||
"token": "Token",
|
"invalid_code": "Codice invalido",
|
||||||
"app_version": "Versione dell'App"
|
"authorize": "Autorizza"
|
||||||
},
|
},
|
||||||
"quick_connect": {
|
"media_controls": {
|
||||||
"quick_connect_title": "Connessione Rapida",
|
"media_controls_title": "Controlli multimediali",
|
||||||
"authorize_button": "Autorizza Connessione Rapida",
|
"forward_skip_length": "Lunghezza del salto in avanti",
|
||||||
"enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...",
|
"rewind_length": "Lunghezza del riavvolgimento",
|
||||||
"success": "Successo",
|
"seconds_unit": "s"
|
||||||
"quick_connect_autorized": "Connessione Rapida autorizzata",
|
},
|
||||||
"error": "Errore",
|
"audio": {
|
||||||
"invalid_code": "Codice invalido",
|
"audio_title": "Audio",
|
||||||
"authorize": "Autorizza"
|
"set_audio_track": "Imposta la traccia audio dall'elemento precedente",
|
||||||
},
|
"audio_language": "Lingua Audio",
|
||||||
"media_controls": {
|
"audio_hint": "Scegli la lingua audio predefinita.",
|
||||||
"media_controls_title": "Controlli multimediali",
|
"none": "Nessuno",
|
||||||
"forward_skip_length": "Lunghezza del salto in avanti",
|
"language": "Lingua"
|
||||||
"rewind_length": "Lunghezza del riavvolgimento",
|
},
|
||||||
"seconds_unit": "s"
|
"subtitles": {
|
||||||
},
|
"subtitle_title": "Sottotitoli",
|
||||||
"audio": {
|
"subtitle_language": "Lingua dei sottotitoli",
|
||||||
"audio_title": "Audio",
|
"subtitle_mode": "Modalità dei sottotitoli",
|
||||||
"set_audio_track": "Imposta la traccia audio dall'elemento precedente",
|
"set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente",
|
||||||
"audio_language": "Lingua Audio",
|
"subtitle_size": "Dimensione dei sottotitoli",
|
||||||
"audio_hint": "Scegli la lingua audio predefinita.",
|
"subtitle_hint": "Configura la preferenza dei sottotitoli.",
|
||||||
"none": "Nessuno",
|
"none": "Nessuno",
|
||||||
"language": "Lingua"
|
"language": "Lingua",
|
||||||
},
|
"loading": "Caricamento",
|
||||||
"subtitles": {
|
"modes": {
|
||||||
"subtitle_title": "Sottotitoli",
|
"Default": "Predefinito",
|
||||||
"subtitle_language": "Lingua dei sottotitoli",
|
"Smart": "Intelligente",
|
||||||
"subtitle_mode": "Modalità dei sottotitoli",
|
"Always": "Sempre",
|
||||||
"set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente",
|
"None": "Nessuno",
|
||||||
"subtitle_size": "Dimensione dei sottotitoli",
|
"OnlyForced": "Solo forzati"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
||||||
"downloads_title": "Scaricati",
|
"downloads_title": "Scaricamento",
|
||||||
"tvseries": "Serie TV",
|
"download_method": "Metodo per lo scaricamento",
|
||||||
"movies": "Film",
|
"remux_max_download": "Numero di Remux da scaricare al massimo",
|
||||||
"queue": "Coda",
|
"auto_download": "Scaricamento automatico",
|
||||||
"queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app",
|
"optimized_versions_server": "Versioni del server di ottimizzazione",
|
||||||
"no_items_in_queue": "Nessun elemento in coda",
|
"save_button": "Salva",
|
||||||
"no_downloaded_items": "Nessun elemento scaricato",
|
"optimized_server": "Server di ottimizzazione",
|
||||||
"delete_all_movies_button": "Cancella tutti i film",
|
"optimized": "Ottimizzato",
|
||||||
"delete_all_tvseries_button": "Cancella tutte le serie TV",
|
"default": "Predefinito",
|
||||||
"delete_all_button": "Cancella tutti",
|
"optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.",
|
||||||
"active_download": "Scaricamento in corso",
|
"read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.",
|
||||||
"no_active_downloads": "Nessun scaricamento in corso",
|
"url": "URL",
|
||||||
"active_downloads": "Scaricamenti in corso",
|
"server_url_placeholder": "http(s)://dominio.org:porta"
|
||||||
"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.",
|
"plugins": {
|
||||||
"back": "Indietro",
|
"plugins_title": "Plugin",
|
||||||
"delete": "Cancella",
|
"jellyseerr": {
|
||||||
"something_went_wrong": "Qualcosa è andato storto",
|
"jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin",
|
"server_url": "URL del Server",
|
||||||
"eta": "ETA {{eta}}",
|
"server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)",
|
||||||
"methods": "Metodi",
|
"server_url_placeholder": "URL di Jellyseerr...",
|
||||||
"toasts": {
|
"password": "Password",
|
||||||
"you_are_not_allowed_to_download_files": "Non è consentito scaricare file.",
|
"password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin",
|
||||||
"deleted_all_movies_successfully": "Cancellati tutti i film con successo!",
|
"save_button": "Salva",
|
||||||
"failed_to_delete_all_movies": "Impossibile eliminare tutti i film",
|
"clear_button": "Cancella",
|
||||||
"deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!",
|
"login_button": "Accedi",
|
||||||
"failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV",
|
"total_media_requests": "Totale di richieste di media",
|
||||||
"download_cancelled": "Scaricamento annullato",
|
"movie_quota_limit": "Limite di quota per i film",
|
||||||
"could_not_cancel_download": "Impossibile annullare lo scaricamento",
|
"movie_quota_days": "Giorni di quota per i film",
|
||||||
"download_completed": "Scaricamento completato",
|
"tv_quota_limit": "Limite di quota per le serie TV",
|
||||||
"download_started_for": "Scaricamento iniziato per {{item}}",
|
"tv_quota_days": "Giorni di quota per le serie TV",
|
||||||
"item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato",
|
"reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr",
|
||||||
"download_stated_for_item": "Scaricamento iniziato per {{item}}",
|
"unlimited": "Illimitato"
|
||||||
"download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}",
|
},
|
||||||
"download_completed_for_item": "Scaricamento completato per {{item}}",
|
"marlin_search": {
|
||||||
"queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione",
|
"enable_marlin_search": "Abilita la ricerca Marlin ",
|
||||||
"failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}",
|
"url": "URL",
|
||||||
"server_responded_with_status_code": "Server responded with status {{statusCode}}",
|
"server_url_placeholder": "http(s)://dominio.org:porta",
|
||||||
"no_response_received_from_server": "No response received from the server",
|
"marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.",
|
||||||
"error_setting_up_the_request": "Error setting up the request",
|
"read_more_about_marlin": "Leggi di più su Marlin.",
|
||||||
"failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto",
|
"save_button": "Salva",
|
||||||
"all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.",
|
"toasts": {
|
||||||
"an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi",
|
"saved": "Salvato"
|
||||||
"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": {
|
"storage": {
|
||||||
"display": "Display",
|
"storage_title": "Spazio",
|
||||||
"row": "Fila",
|
"app_usage": "App {{usedSpace}}%",
|
||||||
"list": "Lista",
|
"device_usage": "Dispositivo {{availableSpace}}%",
|
||||||
"image_style": "Stile dell'immagine",
|
"size_used": "{{used}} di {{total}} usato",
|
||||||
"poster": "Poster",
|
"delete_all_downloaded_files": "Cancella Tutti i File Scaricati"
|
||||||
"cover": "Cover",
|
},
|
||||||
"show_titles": "Mostra titoli",
|
"intro": {
|
||||||
"show_stats": "Mostra statistiche"
|
"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": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.",
|
"error_deleting_files": "Errore nella cancellazione dei file",
|
||||||
"jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.",
|
"background_downloads_enabled": "Scaricamento in background abilitato",
|
||||||
"failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr",
|
"background_downloads_disabled": "Scaricamento in background disabilitato",
|
||||||
"issue_submitted": "Problema inviato!",
|
"connected": "Connesso",
|
||||||
"requested_item": "Richiesto {{item}}!",
|
"could_not_connect": "Non è stato possibile connettersi",
|
||||||
"you_dont_have_permission_to_request": "Non hai il permesso di richiedere!",
|
"invalid_url": "URL invalido"
|
||||||
"something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tabs": {
|
"downloads": {
|
||||||
"home": "Home",
|
"downloads_title": "Scaricati",
|
||||||
"search": "Cerca",
|
"tvseries": "Serie TV",
|
||||||
"library": "Libreria",
|
"movies": "Film",
|
||||||
"custom_links": "Collegamenti personalizzati",
|
"queue": "Coda",
|
||||||
"favorites": "Preferiti"
|
"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": "動画の再生中にエラーが発生しました。設定でログを確認してください。",
|
"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": "サーバーからのメッセージ",
|
||||||
"video_has_finished_playing": "ビデオの再生が終了しました!",
|
"video_has_finished_playing": "ビデオの再生が終了しました!",
|
||||||
"no_video_source": "動画ソースがありません...",
|
"no_video_source": "動画ソースがありません...",
|
||||||
"next_episode": "次のエピソード",
|
"next_episode": "次のエピソード",
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
"default": "Standaard",
|
"default": "Standaard",
|
||||||
"optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.",
|
"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.",
|
"read_more_about_optimized_server": "Lees meer over de optimalisatieserver.",
|
||||||
"url":"URL",
|
"url": "URL",
|
||||||
"server_url_placeholder": "http(s)://domein.org:poort"
|
"server_url_placeholder": "http(s)://domein.org:poort"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
"app_language_description": "Selecteer een taal voor de app.",
|
"app_language_description": "Selecteer een taal voor de app.",
|
||||||
"system": "Systeem"
|
"system": "Systeem"
|
||||||
},
|
},
|
||||||
"toasts":{
|
"toasts": {
|
||||||
"error_deleting_files": "Fout bij het verwijden van bestanden",
|
"error_deleting_files": "Fout bij het verwijden van bestanden",
|
||||||
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
|
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
|
||||||
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld",
|
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld",
|
||||||
@@ -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.",
|
"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}}",
|
"message_from_server": "Bericht van de server",
|
||||||
"video_has_finished_playing": "Video is gedaan met spelen!",
|
"video_has_finished_playing": "Video is gedaan met spelen!",
|
||||||
"no_video_source": "Geen video bron...",
|
"no_video_source": "Geen video bron...",
|
||||||
"next_episode": "Volgende Aflevering",
|
"next_episode": "Volgende Aflevering",
|
||||||
@@ -399,7 +399,7 @@
|
|||||||
"for_kids": "Voor kinderen",
|
"for_kids": "Voor kinderen",
|
||||||
"news": "Nieuws"
|
"news": "Nieuws"
|
||||||
},
|
},
|
||||||
"jellyseerr":{
|
"jellyseerr": {
|
||||||
"confirm": "Bevestig",
|
"confirm": "Bevestig",
|
||||||
"cancel": "Annuleer",
|
"cancel": "Annuleer",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
|
|||||||
@@ -342,7 +342,7 @@
|
|||||||
"an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。",
|
"an_error_occured_while_playing_the_video": "播放视频时发生错误。请检查设置中的日志。",
|
||||||
"client_error": "客户端错误",
|
"client_error": "客户端错误",
|
||||||
"could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流",
|
"could_not_create_stream_for_chromecast": "无法为 Chromecast 建立串流",
|
||||||
"message_from_server": "来自服务器的消息:{{message}}",
|
"message_from_server": "来自服务器的消息",
|
||||||
"video_has_finished_playing": "视频播放完成!",
|
"video_has_finished_playing": "视频播放完成!",
|
||||||
"no_video_source": "无视频来源...",
|
"no_video_source": "无视频来源...",
|
||||||
"next_episode": "下一集",
|
"next_episode": "下一集",
|
||||||
|
|||||||
@@ -342,7 +342,7 @@
|
|||||||
"an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。",
|
"an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。",
|
||||||
"client_error": "客戶端錯誤",
|
"client_error": "客戶端錯誤",
|
||||||
"could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流",
|
"could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流",
|
||||||
"message_from_server": "來自伺服器的消息:{{message}}",
|
"message_from_server": "來自伺服器的消息",
|
||||||
"video_has_finished_playing": "影片播放完畢!",
|
"video_has_finished_playing": "影片播放完畢!",
|
||||||
"no_video_source": "無影片來源...",
|
"no_video_source": "無影片來源...",
|
||||||
"next_episode": "下一集",
|
"next_episode": "下一集",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default {
|
|||||||
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
|
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
|
||||||
VideoCodec:
|
VideoCodec:
|
||||||
"h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video",
|
"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,
|
Type: MediaTypes.Audio,
|
||||||
|
|||||||
Reference in New Issue
Block a user