From 684e67175043cbe390d3ba69aa6093afc20d8d99 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 8 Dec 2024 16:29:17 +0100 Subject: [PATCH] fix: design issues regarding downloads --- .../(tabs)/(home)/downloads/[seriesId].tsx | 38 +-- components/DownloadItem.tsx | 219 ++++++++++-------- components/ItemContent.tsx | 10 +- components/downloads/DownloadSize.tsx | 71 +++--- components/downloads/EpisodeCard.tsx | 58 ++--- components/series/SeasonPicker.tsx | 7 +- providers/DownloadProvider.tsx | 138 +++++------ utils/download.ts | 64 ++--- 8 files changed, 299 insertions(+), 306 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx index 9de3b673..e9c95657 100644 --- a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx +++ b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx @@ -2,7 +2,7 @@ import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; import { router, useLocalSearchParams, useNavigation } from "expo-router"; import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { ScrollView, TouchableOpacity, View } from "react-native"; +import { ScrollView, TouchableOpacity, View, Alert } from "react-native"; import { EpisodeCard } from "@/components/downloads/EpisodeCard"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { @@ -78,14 +78,28 @@ export default function page() { } }, [series]); - const deleteSeries = useCallback( - async () => deleteItems(groupBySeason), - [groupBySeason] - ); + const deleteSeries = useCallback(() => { + Alert.alert( + "Delete season", + "Are you sure you want to delete the entire season?", + [ + { + text: "Cancel", + style: "cancel", + }, + { + text: "Delete", + onPress: () => deleteItems(groupBySeason), + style: "destructive", + }, + ] + ); + }, [groupBySeason]); + return ( - + {series.length > 0 && ( - + s.item)} @@ -101,18 +115,16 @@ export default function page() { {groupBySeason.length} - + - + )} - + {groupBySeason.map((episode, index) => ( - - - + ))} diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx index 401cec6d..9b065975 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -18,7 +18,7 @@ import { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; -import {Href, router, useFocusEffect} from "expo-router"; +import { Href, router, useFocusEffect } from "expo-router"; import { useAtom } from "jotai"; import React, { useCallback, useMemo, useRef, useState } from "react"; import { Alert, TouchableOpacity, View, ViewProps } from "react-native"; @@ -34,8 +34,8 @@ import { SubtitleTrackSelector } from "./SubtitleTrackSelector"; interface DownloadProps extends ViewProps { items: BaseItemDto[]; - MissingDownloadIconComponent: () => React.ReactElement; - DownloadedIconComponent: () => React.ReactElement; + MissingDownloadIconComponent: () => React.ReactElement; + DownloadedIconComponent: () => React.ReactElement; } export const DownloadItems: React.FC = ({ @@ -51,16 +51,25 @@ export const DownloadItems: React.FC = ({ const { processes, startBackgroundDownload, downloadedFiles } = useDownload(); const { startRemuxing } = useRemuxHlsToMp4(); - const [selectedMediaSource, setSelectedMediaSource] = useState(undefined); + const [selectedMediaSource, setSelectedMediaSource] = useState< + MediaSourceInfo | undefined | null + >(undefined); const [selectedAudioStream, setSelectedAudioStream] = useState(-1); - const [selectedSubtitleStream, setSelectedSubtitleStream] = useState(0); + const [selectedSubtitleStream, setSelectedSubtitleStream] = + useState(0); const [maxBitrate, setMaxBitrate] = useState({ key: "Max", value: undefined, }); - const userCanDownload = useMemo(() => user?.Policy?.EnableContentDownloading, [user]); - const usingOptimizedServer = useMemo(() => settings?.downloadMethod === "optimized", [settings]); + const userCanDownload = useMemo( + () => user?.Policy?.EnableContentDownloading, + [user] + ); + const usingOptimizedServer = useMemo( + () => settings?.downloadMethod === "optimized", + [settings] + ); /** * Bottom sheet @@ -78,73 +87,76 @@ export const DownloadItems: React.FC = ({ }, []); // region computed - const itemIds = useMemo(() => items.map(i => i.Id), [items]); - const pendingItems = useMemo(() => - items.filter(i => !downloadedFiles?.some(f => f.item.Id === i.Id)), + const itemIds = useMemo(() => items.map((i) => i.Id), [items]); + const pendingItems = useMemo( + () => + items.filter((i) => !downloadedFiles?.some((f) => f.item.Id === i.Id)), [items, downloadedFiles] ); const isDownloaded = useMemo(() => { - if (!downloadedFiles) - return false; + if (!downloadedFiles) return false; return pendingItems.length == 0; }, [downloadedFiles, pendingItems]); - const itemsProcesses = useMemo(() => - processes?.filter(p => itemIds.includes(p.item.Id)), + const itemsProcesses = useMemo( + () => processes?.filter((p) => itemIds.includes(p.item.Id)), [processes, itemIds] ); const progress = useMemo(() => { - if (itemIds.length == 1) - return itemsProcesses.reduce((acc, p) => acc + p.progress, 0) - return ((itemIds.length - queue.filter(q => itemIds.includes(q.item.Id)).length) / itemIds.length) * 100 - }, - [queue, itemsProcesses, itemIds] - ); + if (itemIds.length == 1) + return itemsProcesses.reduce((acc, p) => acc + p.progress, 0); + return ( + ((itemIds.length - + queue.filter((q) => itemIds.includes(q.item.Id)).length) / + itemIds.length) * + 100 + ); + }, [queue, itemsProcesses, itemIds]); const itemsQueued = useMemo(() => { - return pendingItems.length > 0 && pendingItems.every(p => queue.some(q => p.Id == q.item.Id)) - }, - [queue, pendingItems] - ); + return ( + pendingItems.length > 0 && + pendingItems.every((p) => queue.some((q) => p.Id == q.item.Id)) + ); + }, [queue, pendingItems]); // endregion computed // region helper functions const navigateToDownloads = () => router.push("/downloads"); const onDownloadedPress = () => { - const firstItem = items?.[0] + const firstItem = items?.[0]; router.push( firstItem.Type !== "Episode" ? "/downloads" - : { - pathname: `/downloads/${firstItem.SeriesId}`, - params: { - episodeSeasonIndex: firstItem.ParentIndexNumber - } - } as Href + : ({ + pathname: `/downloads/${firstItem.SeriesId}`, + params: { + episodeSeasonIndex: firstItem.ParentIndexNumber, + }, + } as Href) ); - } + }; const acceptDownloadOptions = useCallback(() => { if (userCanDownload === true) { - if (pendingItems.some(i => !i.Id)) { + if (pendingItems.some((i) => !i.Id)) { throw new Error("No item id"); } closeModal(); - if (usingOptimizedServer) - initiateDownload(...pendingItems); + if (usingOptimizedServer) initiateDownload(...pendingItems); else { queueActions.enqueue( queue, setQueue, - ...pendingItems.map(item => ({ + ...pendingItems.map((item) => ({ id: item.Id!, execute: async () => await initiateDownload(item), item, })) - ) + ); } } else { toast.error("You are not allowed to download files."); @@ -160,70 +172,83 @@ export const DownloadItems: React.FC = ({ maxBitrate, selectedMediaSource, selectedAudioStream, - selectedSubtitleStream - ]) + selectedSubtitleStream, + ]); /** * Start download */ - const initiateDownload = useCallback(async (...items: BaseItemDto[]) => { - if (!api || !user?.Id || items.some(p => !p.Id) || (pendingItems.length === 1 && !selectedMediaSource?.Id)) { - throw new Error("DownloadItem ~ initiateDownload: No api or user or item"); - } - let mediaSource = selectedMediaSource - let audioIndex: number | undefined = selectedAudioStream - let subtitleIndex: number | undefined = selectedSubtitleStream - - for (const item of items) { - if (pendingItems.length > 1) { - ({ mediaSource, audioIndex, subtitleIndex } = getDefaultPlaySettings(item, settings!)); - } - - const res = await getStreamUrl({ - api, - item, - startTimeTicks: 0, - userId: user?.Id, - audioStreamIndex: audioIndex, - maxStreamingBitrate: maxBitrate.value, - mediaSourceId: mediaSource?.Id, - subtitleStreamIndex: subtitleIndex, - deviceProfile: download, - }); - - if (!res) { - Alert.alert( - "Something went wrong", - "Could not get stream url from Jellyfin" + const initiateDownload = useCallback( + async (...items: BaseItemDto[]) => { + if ( + !api || + !user?.Id || + items.some((p) => !p.Id) || + (pendingItems.length === 1 && !selectedMediaSource?.Id) + ) { + throw new Error( + "DownloadItem ~ initiateDownload: No api or user or item" ); - continue; } + let mediaSource = selectedMediaSource; + let audioIndex: number | undefined = selectedAudioStream; + let subtitleIndex: number | undefined = selectedSubtitleStream; - const {mediaSource: source, url} = res; + for (const item of items) { + if (pendingItems.length > 1) { + ({ mediaSource, audioIndex, subtitleIndex } = getDefaultPlaySettings( + item, + settings! + )); + } - if (!url || !source) throw new Error("No url"); + const res = await getStreamUrl({ + api, + item, + startTimeTicks: 0, + userId: user?.Id, + audioStreamIndex: audioIndex, + maxStreamingBitrate: maxBitrate.value, + mediaSourceId: mediaSource?.Id, + subtitleStreamIndex: subtitleIndex, + deviceProfile: download, + }); - saveDownloadItemInfoToDiskTmp(item, source, url); + if (!res) { + Alert.alert( + "Something went wrong", + "Could not get stream url from Jellyfin" + ); + continue; + } - if (usingOptimizedServer) { - await startBackgroundDownload(url, item, source); - } else { - await startRemuxing(item, url, source); + const { mediaSource: source, url } = res; + + if (!url || !source) throw new Error("No url"); + + saveDownloadItemInfoToDiskTmp(item, source, url); + + if (usingOptimizedServer) { + await startBackgroundDownload(url, item, source); + } else { + await startRemuxing(item, url, source); + } } - } - }, [ - api, - user?.Id, - pendingItems, - selectedMediaSource, - selectedAudioStream, - selectedSubtitleStream, - settings, - maxBitrate, - usingOptimizedServer, - startBackgroundDownload, - startRemuxing, - ]); + }, + [ + api, + user?.Id, + pendingItems, + selectedMediaSource, + selectedAudioStream, + selectedSubtitleStream, + settings, + maxBitrate, + usingOptimizedServer, + startBackgroundDownload, + startRemuxing, + ] + ); const renderBackdrop = useCallback( (props: BottomSheetBackdropProps) => ( @@ -255,7 +280,7 @@ export const DownloadItems: React.FC = ({ return ( {processes && itemsProcesses.length > 0 ? ( @@ -343,7 +368,9 @@ export const DownloadItems: React.FC = ({ - {usingOptimizedServer ? "Using optimized server" : "Using default method"} + {usingOptimizedServer + ? "Using optimized server" + : "Using default method"} @@ -353,7 +380,9 @@ export const DownloadItems: React.FC = ({ ); }; -export const DownloadSingleItem: React.FC<{ item: BaseItemDto }> = ({ item }) => { +export const DownloadSingleItem: React.FC<{ item: BaseItemDto }> = ({ + item, +}) => { return ( = ({ item }) => )} /> - ) -} + ); +}; diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index cab8e641..b3eb242e 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -1,6 +1,6 @@ import { AudioTrackSelector } from "@/components/AudioTrackSelector"; import { Bitrate, BitrateSelector } from "@/components/BitrateSelector"; -import {DownloadSingleItem} from "@/components/DownloadItem"; +import { DownloadSingleItem } from "@/components/DownloadItem"; import { OverviewText } from "@/components/OverviewText"; import { ParallaxScrollView } from "@/components/ParallaxPage"; import { PlayButton } from "@/components/PlayButton"; @@ -228,6 +228,10 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( {item.Type !== "Program" && ( <> + {item.Type === "Episode" && ( + + )} + {item.People && item.People.length > 0 && ( @@ -243,10 +247,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( )} - {item.Type === "Episode" && ( - - )} - )} diff --git a/components/downloads/DownloadSize.tsx b/components/downloads/DownloadSize.tsx index d1b11bf0..7e9f2929 100644 --- a/components/downloads/DownloadSize.tsx +++ b/components/downloads/DownloadSize.tsx @@ -1,62 +1,47 @@ +import { Text } from "@/components/common/Text"; +import { bytesToReadable, useDownload } from "@/providers/DownloadProvider"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import React, {useEffect, useMemo, useState} from "react"; -import {Text} from "@/components/common/Text"; -import useDownloadHelper from "@/utils/download"; -import {bytesToReadable, useDownload} from "@/providers/DownloadProvider"; -import {TextProps} from "react-native"; +import React, { useEffect, useMemo, useState } from "react"; +import { TextProps } from "react-native"; interface DownloadSizeProps extends TextProps { items: BaseItemDto[]; } -interface DownloadSizes { - knownSize: number; - itemsNeedingSize: BaseItemDto[]; -} - -export const DownloadSize: React.FC = ({ items, ...props }) => { - const { downloadedFiles, saveDownloadedItemInfo } = useDownload(); - const { getDownloadSize } = useDownloadHelper(); +export const DownloadSize: React.FC = ({ + items, + ...props +}) => { + const { downloadedFiles, getDownloadedItemSize } = useDownload(); const [size, setSize] = useState(); - const itemIds = useMemo(() => items.map(i => i.Id), [items]) + const itemIds = useMemo(() => items.map((i) => i.Id), [items]); useEffect(() => { - if (!downloadedFiles) - return + if (!downloadedFiles) return; - const {knownSize, itemsNeedingSize} = downloadedFiles - .filter(f => itemIds.includes(f.item.Id)) - ?.reduce((acc, file) => { - if (file?.size && file.size > 0) - acc.knownSize += file.size - else - acc.itemsNeedingSize.push(file.item) - return acc - }, { - knownSize: 0, - itemsNeedingSize: [] - }) + let s = 0; - getDownloadSize( - (item, size) => saveDownloadedItemInfo(item, size), - ...itemsNeedingSize - ).then(sizeSum => { - setSize(bytesToReadable((sizeSum + knownSize))) - }) - }, - [items, itemIds] - ); + for (const item of items) { + if (!item.Id) continue; + const size = getDownloadedItemSize(item.Id); + if (size) { + s += size; + } + } + setSize(bytesToReadable(s)); + }, [itemIds]); const sizeText = useMemo(() => { - if (!size) - return "reading size..." - return size - }, [size]) + if (!size) return "..."; + return size; + }, [size]); return ( <> - {sizeText} + + {sizeText} + ); -}; \ No newline at end of file +}; diff --git a/components/downloads/EpisodeCard.tsx b/components/downloads/EpisodeCard.tsx index bbe9358b..80949a3d 100644 --- a/components/downloads/EpisodeCard.tsx +++ b/components/downloads/EpisodeCard.tsx @@ -1,31 +1,28 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import * as Haptics from "expo-haptics"; -import React, {useCallback, useMemo} from "react"; -import { TouchableOpacity, View } from "react-native"; +import React, { useCallback, useMemo } from "react"; +import { TouchableOpacity, TouchableOpacityProps, View } from "react-native"; import { ActionSheetProvider, useActionSheet, } from "@expo/react-native-action-sheet"; import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener"; -import {useDownload} from "@/providers/DownloadProvider"; +import { useDownload } from "@/providers/DownloadProvider"; import { storage } from "@/utils/mmkv"; import { Image } from "expo-image"; import { Ionicons } from "@expo/vector-icons"; -import {Text} from "@/components/common/Text"; -import {runtimeTicksToSeconds} from "@/utils/time"; -import {DownloadSize} from "@/components/downloads/DownloadSize"; +import { Text } from "@/components/common/Text"; +import { runtimeTicksToSeconds } from "@/utils/time"; +import { DownloadSize } from "@/components/downloads/DownloadSize"; +import { TouchableItemRouter } from "../common/TouchableItemRouter"; +import ContinueWatchingPoster from "../ContinueWatchingPoster"; -interface EpisodeCardProps { +interface EpisodeCardProps extends TouchableOpacityProps { item: BaseItemDto; } -/** - * EpisodeCard component displays an episode with action sheet options. - * @param {EpisodeCardProps} props - The component props. - * @returns {React.ReactElement} The rendered EpisodeCard component. - */ -export const EpisodeCard: React.FC = ({ item }) => { +export const EpisodeCard: React.FC = ({ item, ...props }) => { const { deleteFile } = useDownload(); const { openFile } = useDownloadedFileOpener(); const { showActionSheetWithOptions } = useActionSheet(); @@ -77,35 +74,14 @@ export const EpisodeCard: React.FC = ({ item }) => { - {base64Image ? ( - - - - ) : ( - - - - )} + - + {item.Name} @@ -115,10 +91,12 @@ export const EpisodeCard: React.FC = ({ item }) => { {runtimeTicksToSeconds(item.RunTimeTicks)} - - {item.Overview} + + + {item.Overview} + ); }; diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index 28ca9ccb..43d55cc3 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -112,7 +112,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { minHeight: 144 * nrOfEpisodes, }} > - + = ({ item, initialSeasonIndex }) => { }} /> ( )} DownloadedIconComponent={() => ( )} diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index 48f7a536..48caa133 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -1,6 +1,6 @@ import { useSettings } from "@/utils/atoms/settings"; import { getOrSetDeviceId } from "@/utils/device"; -import {useLog, writeToLog} from "@/utils/log"; +import { useLog, writeToLog } from "@/utils/log"; import { cancelAllJobs, cancelJobById, @@ -30,7 +30,7 @@ import { import axios from "axios"; import * as FileSystem from "expo-file-system"; import { useRouter } from "expo-router"; -import {atom, useAtom} from "jotai"; +import { atom, useAtom } from "jotai"; import React, { createContext, useCallback, @@ -47,16 +47,15 @@ import { getItemImage } from "@/utils/getItemImage"; import useImageStorage from "@/hooks/useImageStorage"; import { storage } from "@/utils/mmkv"; import useDownloadHelper from "@/utils/download"; -import {FileInfo} from "expo-file-system"; +import { FileInfo } from "expo-file-system"; import * as Haptics from "expo-haptics"; export type DownloadedItem = { item: Partial; mediaSource: MediaSourceInfo; - size: number | undefined; }; -export const processesAtom = atom([]) +export const processesAtom = atom([]); function onAppStateChange(status: AppStateStatus) { focusManager.setFocused(status === "active"); @@ -73,7 +72,7 @@ function useDownloadProvider() { const [api] = useAtom(apiAtom); const { logs } = useLog(); - const {saveSeriesPrimaryImage} = useDownloadHelper(); + const { saveSeriesPrimaryImage } = useDownloadHelper(); const { saveImage } = useImageStorage(); const [processes, setProcesses] = useAtom(processesAtom); @@ -267,7 +266,10 @@ function useDownloadProvider() { ); }) .done(async (doneHandler) => { - await saveDownloadedItemInfo(process.item, doneHandler.bytesDownloaded); + await saveDownloadedItemInfo( + process.item, + doneHandler.bytesDownloaded + ); toast.success(`Download completed for ${process.item.Name}`, { duration: 3000, action: { @@ -397,16 +399,21 @@ function useDownloadProvider() { deleteLocalFiles(), removeDownloadedItemsFromStorage(), cancelAllServerJobs(), - queryClient.invalidateQueries({queryKey: ["downloadedItems"]}), - ]).then(() => - toast.success("All files, folders, and jobs deleted successfully") - ).catch((reason) => { - console.error("Failed to delete all files, folders, and jobs:", reason); - toast.error("An error occurred while deleting files and jobs"); - }); + queryClient.invalidateQueries({ queryKey: ["downloadedItems"] }), + ]) + .then(() => + toast.success("All files, folders, and jobs deleted successfully") + ) + .catch((reason) => { + console.error("Failed to delete all files, folders, and jobs:", reason); + toast.error("An error occurred while deleting files and jobs"); + }); }; - const forEveryDirectoryFile = async (includeMMKV: boolean = true, callback: (file: FileInfo) => void) => { + const forEveryDirectoryFile = async ( + includeMMKV: boolean = true, + callback: (file: FileInfo) => void + ) => { const baseDirectory = FileSystem.documentDirectory; if (!baseDirectory) { throw new Error("Base directory not found"); @@ -416,34 +423,29 @@ function useDownloadProvider() { for (const item of dirContents) { // Exclude mmkv directory. // Deleting this deletes all user information as well. Logout should handle this. - if (item == "mmkv" && !includeMMKV) - continue + if (item == "mmkv" && !includeMMKV) continue; const itemInfo = await FileSystem.getInfoAsync(`${baseDirectory}${item}`); if (itemInfo.exists) { - callback(itemInfo) + callback(itemInfo); } } - } + }; const deleteLocalFiles = async (): Promise => { await forEveryDirectoryFile(false, (file) => { - console.warn("Deleting file", file.uri) - FileSystem.deleteAsync(file.uri, {idempotent: true}) - } - ) + console.warn("Deleting file", file.uri); + FileSystem.deleteAsync(file.uri, { idempotent: true }); + }); }; const removeDownloadedItemsFromStorage = async () => { // delete any saved images first - Promise.all([ - deleteFileByType("Movie"), - deleteFileByType("Episode"), - ]).then(() => - storage.delete("downloadedItems") - ).catch((reason) => { - console.error("Failed to remove downloadedItems from storage:", reason); - throw reason - }) + Promise.all([deleteFileByType("Movie"), deleteFileByType("Episode")]) + .then(() => storage.delete("downloadedItems")) + .catch((reason) => { + console.error("Failed to remove downloadedItems from storage:", reason); + throw reason; + }); }; const cancelAllServerJobs = async (): Promise => { @@ -452,7 +454,7 @@ function useDownloadProvider() { } if (!settings?.optimizedVersionsServerUrl) { console.error("No server URL configured"); - return + return; } const deviceId = await getOrSetDeviceId(); @@ -513,41 +515,38 @@ function useDownloadProvider() { }; const deleteItems = async (items: BaseItemDto[]) => { - Promise.all(items.map(i => { - if (i.Id) - return deleteFile(i.Id) - return - })).then(() => + Promise.all( + items.map((i) => { + if (i.Id) return deleteFile(i.Id); + return; + }) + ).then(() => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) - ) - } + ); + }; - const deleteFileByType = async (type: BaseItemDto['Type']) => { + const deleteFileByType = async (type: BaseItemDto["Type"]) => { await Promise.all( downloadedFiles - ?.filter(file => file.item.Type == type) - ?.flatMap(file => { + ?.filter((file) => file.item.Type == type) + ?.flatMap((file) => { const promises = []; if (type == "Episode" && file.item.SeriesId) - promises.push(deleteFile(file.item.SeriesId)) - promises.push(deleteFile(file.item.Id!)) + promises.push(deleteFile(file.item.SeriesId)); + promises.push(deleteFile(file.item.Id!)); return promises; - }) - || [] + }) || [] ); - } + }; const appSizeUsage = useMemo(async () => { const sizes: number[] = []; - await forEveryDirectoryFile( - true, - file => { - if (file.exists) sizes.push(file.size) - } - ) + await forEveryDirectoryFile(true, (file) => { + if (file.exists) sizes.push(file.size); + }); return sizes.reduce((sum, size) => sum + size, 0); - }, [logs, downloadedFiles]) + }, [logs, downloadedFiles]); function getDownloadedItem(itemId: string): DownloadedItem | null { try { @@ -594,7 +593,7 @@ function useDownloadProvider() { "Media source not found in tmp storage. Did you forget to save it before starting download?" ); - const newItem = { item, size, mediaSource: data.mediaSource }; + const newItem = { item, mediaSource: data.mediaSource }; if (existingItemIndex !== -1) { items[existingItemIndex] = newItem; @@ -605,6 +604,8 @@ function useDownloadProvider() { deleteDownloadItemInfoFromDiskTmp(item.Id!); storage.set("downloadedItems", JSON.stringify(items)); + storage.set("downloadedItemSize-" + item.Id, size.toString()); + queryClient.invalidateQueries({ queryKey: ["downloadedItems"] }); refetch(); } catch (error) { @@ -615,6 +616,11 @@ function useDownloadProvider() { } } + function getDownloadedItemSize(itemId: string): number { + const size = storage.getString("downloadedItemSize-" + itemId); + return size ? parseInt(size) : 0; + } + return { processes, startBackgroundDownload, @@ -628,7 +634,8 @@ function useDownloadProvider() { startDownload, getDownloadedItem, deleteFileByType, - appSizeUsage + appSizeUsage, + getDownloadedItemSize, }; } @@ -653,15 +660,12 @@ export function useDownload() { export function bytesToReadable(bytes: number): string { const gb = bytes / 1e9; - if (gb >= 1) - return `${gb.toFixed(2)} GB` + if (gb >= 1) return `${gb.toFixed(2)} GB`; - const mb = bytes / 1024 / 1024 - if (mb >= 1) - return `${mb.toFixed(2)} MB` + const mb = bytes / 1024 / 1024; + if (mb >= 1) return `${mb.toFixed(2)} MB`; - const kb = bytes / 1024 - if (kb >= 1) - return `${kb.toFixed(2)} KB` - return `${bytes.toFixed(2)} B` -} \ No newline at end of file + const kb = bytes / 1024; + if (kb >= 1) return `${kb.toFixed(2)} KB`; + return `${bytes.toFixed(2)} B`; +} diff --git a/utils/download.ts b/utils/download.ts index 92d3a391..94a06b7a 100644 --- a/utils/download.ts +++ b/utils/download.ts @@ -1,49 +1,33 @@ -import {getPrimaryImageUrlById} from "@/utils/jellyfin/image/getPrimaryImageUrlById"; -import {BaseItemDto} from "@jellyfin/sdk/lib/generated-client"; import useImageStorage from "@/hooks/useImageStorage"; -import {apiAtom} from "@/providers/JellyfinProvider"; -import {useAtom} from "jotai"; -import {storage} from "@/utils/mmkv"; -import {getDownloadedFileUrl} from "@/hooks/useDownloadedFileOpener"; -import * as FileSystem from 'expo-file-system'; -import {FileInfo} from "expo-file-system"; - +import { apiAtom } from "@/providers/JellyfinProvider"; +import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; +import { storage } from "@/utils/mmkv"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { useAtom } from "jotai"; const useDownloadHelper = () => { const [api] = useAtom(apiAtom); - const {saveImage} = useImageStorage(); + const { saveImage } = useImageStorage(); const saveSeriesPrimaryImage = async (item: BaseItemDto) => { - if (item.Type === "Episode" && item.SeriesId && !storage.getString(item.SeriesId)) { - await saveImage(item.SeriesId, getPrimaryImageUrlById({ api, id: item.SeriesId })) + console.log(`Attempting to save primary image for item: ${item.Id}`); + if ( + item.Type === "Episode" && + item.SeriesId && + !storage.getString(item.SeriesId) + ) { + console.log(`Saving primary image for series: ${item.SeriesId}`); + await saveImage( + item.SeriesId, + getPrimaryImageUrlById({ api, id: item.SeriesId }) + ); + console.log(`Primary image saved for series: ${item.SeriesId}`); + } else { + console.log(`Skipping primary image save for item: ${item.Id}`); } - } + }; - const getDownloadSize = async ( - onNewItemSizeFetched: (item: BaseItemDto, size: number) => void, - ...items: BaseItemDto[] - ) => { - const sizes: number[] = []; + return { saveSeriesPrimaryImage }; +}; - await Promise.all(items.map(item => { - return new Promise(async (resolve, reject) => { - const url = await getDownloadedFileUrl(item.Id!); - if (url) { - const fileInfo: FileInfo = await FileSystem.getInfoAsync(url); - if (fileInfo.exists) { - onNewItemSizeFetched(item, fileInfo.size) - sizes.push(fileInfo.size); - resolve(sizes) - } - } - reject(); - }) - })); - - return sizes.reduce((sum, size) => sum + size, 0); - } - - return { saveSeriesPrimaryImage, getDownloadSize } -} - -export default useDownloadHelper; \ No newline at end of file +export default useDownloadHelper;