import { Text } from "@/components/common/Text"; import { MovieCard } from "@/components/downloads/MovieCard"; import { SeriesCard } from "@/components/downloads/SeriesCard"; import { Loader } from "@/components/Loader"; import { getAllDownloadedItems } from "@/hooks/useDownloadM3U8Files"; import { runningProcesses } from "@/utils/atoms/downloads"; import { queueAtom } from "@/utils/atoms/queue"; import { Ionicons } from "@expo/vector-icons"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQuery } from "@tanstack/react-query"; import * as FileSystem from "expo-file-system"; import { router } from "expo-router"; import { FFmpegKit } from "ffmpeg-kit-react-native"; import { useAtom } from "jotai"; import { useEffect, useMemo } from "react"; import { ScrollView, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; const downloads: React.FC = () => { const [process, setProcess] = useAtom(runningProcesses); const [queue, setQueue] = useAtom(queueAtom); const { data: downloadedFiles, isLoading } = useQuery({ queryKey: ["downloaded_files", process?.item.Id], queryFn: getAllDownloadedItems, staleTime: 0, }); const movies = useMemo( () => downloadedFiles?.filter((f) => f.Type === "Movie") || [], [downloadedFiles] ); const groupedBySeries = useMemo(() => { const episodes = downloadedFiles?.filter((f) => f.Type === "Episode"); const series: { [key: string]: BaseItemDto[] } = {}; episodes?.forEach((e) => { if (!series[e.SeriesName!]) series[e.SeriesName!] = []; series[e.SeriesName!].push(e); }); return Object.values(series); }, [downloadedFiles]); const eta = useMemo(() => { const length = process?.item?.RunTimeTicks || 0; if (!process?.speed || !process?.progress) return ""; const timeLeft = (length - length * (process.progress / 100)) / process.speed; return formatNumber(timeLeft / 10000); }, [process]); useEffect(() => { (async () => { const dir = FileSystem.documentDirectory; if (dir) { const items = await FileSystem.readDirectoryAsync(dir); if (items.length === 0) { console.log("No items found in the document directory."); return; } for (const item of items) { const fullPath = `${dir}${item}`; const info = await FileSystem.getInfoAsync(fullPath); if (info.exists) { if (info.isDirectory) { // List items in the directory const subItems = await FileSystem.readDirectoryAsync(fullPath); if (subItems.length === 0) { console.log(`Directory ${item} is empty.`); } else { console.log(`Items in ${item}:`, subItems); // If item ends in m3u8, print the content of the file const m3u8Files = subItems.filter((subItem) => subItem.endsWith(".m3u8") ); if (m3u8Files.length === 0) { console.log(`No .m3u8 files found in ${item}.`); } else { for (let subItem of m3u8Files) { console.log( `Content of ${subItem}:`, await FileSystem.readAsStringAsync( `${fullPath}/${subItem}` ) ); } } } } else { console.log(`${item} is a file`); } } else { console.log(`${item} does not exist.`); } } } else { console.log("Document directory is not available."); } })(); }, []); const insets = useSafeAreaInsets(); if (isLoading) { return ( ); } return ( Queue {queue.map((q) => ( router.push(`/(auth)/items/page?id=${q.item.Id}`) } className="relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between" > {q.item.Name} {q.item.Type} { setQueue((prev) => prev.filter((i) => i.id !== q.id)); }} > ))} {queue.length === 0 && ( No items in queue )} Active download {process?.item ? ( router.push(`/(auth)/items/page?id=${process.item.Id}`) } className="relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between" > {process.item.Name} {process.item.Type} {process.progress.toFixed(0)}% {process.speed?.toFixed(2)}x ETA {eta} { FFmpegKit.cancel(); setProcess(null); }} > ) : ( No active downloads )} {movies.length > 0 && ( Movies {movies?.length} {movies?.map((item: BaseItemDto) => ( ))} )} {groupedBySeries?.map((items: BaseItemDto[], index: number) => ( ))} ); }; export default downloads; /* * Format a number (Date.getTime) to a human readable string ex. 2m 34s * @param {number} num - The number to format * * @returns {string} - The formatted string */ const formatNumber = (num: number) => { const minutes = Math.floor(num / 60000); const seconds = ((num % 60000) / 1000).toFixed(0); return `${minutes}m ${seconds}s`; };