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