import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4"; import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { queueActions, queueAtom } from "@/utils/atoms/queue"; import { useSettings } from "@/utils/atoms/settings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import native from "@/utils/profiles/native"; import Ionicons from "@expo/vector-icons/Ionicons"; import { BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet"; import { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; import { router, useFocusEffect } from "expo-router"; import { useAtom } from "jotai"; import { useCallback, useMemo, useRef, useState } from "react"; import { Alert, TouchableOpacity, View, ViewProps } from "react-native"; import { MMKV } from "react-native-mmkv"; import { toast } from "sonner-native"; import { AudioTrackSelector } from "./AudioTrackSelector"; import { Bitrate, BitrateSelector } from "./BitrateSelector"; import { Button } from "./Button"; import { Text } from "./common/Text"; import { Loader } from "./Loader"; import { MediaSourceSelector } from "./MediaSourceSelector"; import ProgressCircle from "./ProgressCircle"; import { SubtitleTrackSelector } from "./SubtitleTrackSelector"; import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server"; interface DownloadProps extends ViewProps { item: BaseItemDto; } export const DownloadItem: React.FC = ({ item, ...props }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const [queue, setQueue] = useAtom(queueAtom); const [settings] = useSettings(); const { processes, startBackgroundDownload } = useDownload(); const { startRemuxing } = useRemuxHlsToMp4(); const [selectedMediaSource, setSelectedMediaSource] = useState< MediaSourceInfo | undefined | null >(undefined); const [selectedAudioStream, setSelectedAudioStream] = useState(-1); const [selectedSubtitleStream, setSelectedSubtitleStream] = useState(0); const [maxBitrate, setMaxBitrate] = useState({ key: "Max", value: undefined, }); useFocusEffect( useCallback(() => { if (!settings) return; const { bitrate, mediaSource, audioIndex, subtitleIndex } = getDefaultPlaySettings(item, settings); // 4. Set states setSelectedMediaSource(mediaSource ?? undefined); setSelectedAudioStream(audioIndex ?? 0); setSelectedSubtitleStream(subtitleIndex ?? -1); setMaxBitrate(bitrate); }, [item, settings]) ); const userCanDownload = useMemo(() => { return user?.Policy?.EnableContentDownloading; }, [user]); /** * Bottom sheet */ const bottomSheetModalRef = useRef(null); const handlePresentModalPress = useCallback(() => { bottomSheetModalRef.current?.present(); }, []); const handleSheetChanges = useCallback((index: number) => {}, []); const closeModal = useCallback(() => { bottomSheetModalRef.current?.dismiss(); }, []); /** * Start download */ const initiateDownload = useCallback(async () => { if (!api || !user?.Id || !item.Id || !selectedMediaSource?.Id) { throw new Error( "DownloadItem ~ initiateDownload: No api or user or item" ); } const res = await getStreamUrl({ api, item, startTimeTicks: item?.UserData?.PlaybackPositionTicks!, userId: user?.Id, audioStreamIndex: selectedAudioStream, maxStreamingBitrate: maxBitrate.value, mediaSourceId: selectedMediaSource.Id, subtitleStreamIndex: selectedSubtitleStream, deviceProfile: native, }); if (!res) { Alert.alert( "Something went wrong", "Could not get stream url from Jellyfin" ); return; } const { mediaSource, url } = res; if (!url || !mediaSource) throw new Error("No url"); saveDownloadItemInfoToDiskTmp(item, mediaSource, url); if (settings?.downloadMethod === "optimized") { return await startBackgroundDownload(url, item, mediaSource); } else { return await startRemuxing(item, url, mediaSource); } }, [ api, item, startBackgroundDownload, user?.Id, selectedMediaSource, selectedAudioStream, selectedSubtitleStream, maxBitrate, settings?.downloadMethod, ]); /** * Check if item is downloaded */ const { downloadedFiles } = useDownload(); const isDownloaded = useMemo(() => { if (!downloadedFiles) return false; return downloadedFiles.some((file) => file.item.Id === item.Id); }, [downloadedFiles, item.Id]); const renderBackdrop = useCallback( (props: BottomSheetBackdropProps) => ( ), [] ); const process = useMemo(() => { if (!processes) return null; return processes.find((process) => process?.item?.Id === item.Id); }, [processes, item.Id]); return ( {process && process?.item.Id === item.Id ? ( { router.push("/downloads"); }} > {process.progress === 0 ? ( ) : ( )} ) : queue.some((i) => i.id === item.Id) ? ( { router.push("/downloads"); }} > ) : isDownloaded ? ( { router.push("/downloads"); }} > ) : ( )} Download options setMaxBitrate(val)} selected={maxBitrate} /> {selectedMediaSource && ( )} {settings?.downloadMethod === "optimized" ? ( Using optimized server ) : ( Using default method )} ); };