diff --git a/components/downloads/DownloadCard.tsx b/components/downloads/DownloadCard.tsx index b90c4836..42446285 100644 --- a/components/downloads/DownloadCard.tsx +++ b/components/downloads/DownloadCard.tsx @@ -17,7 +17,6 @@ import { useDownload } from "@/providers/DownloadProvider"; import { JobStatus } from "@/providers/Downloads/types"; import { storage } from "@/utils/mmkv"; import { formatTimeString } from "@/utils/time"; -import { Button } from "../Button"; const bytesToMB = (bytes: number) => { return bytes / 1024 / 1024; @@ -28,14 +27,13 @@ interface DownloadCardProps extends TouchableOpacityProps { } export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { - const { startDownload, pauseDownload, resumeDownload, removeProcess } = - useDownload(); + const { pauseDownload, resumeDownload, removeProcess } = useDownload(); const router = useRouter(); const queryClient = useQueryClient(); - const handlePause = async (id: string) => { + const handlePause = async () => { try { - await pauseDownload(id); + await pauseDownload(); toast.success(t("home.downloads.toasts.download_paused")); } catch (error) { console.error("Error pausing download:", error); @@ -43,9 +41,9 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { } }; - const handleResume = async (id: string) => { + const handleResume = async () => { try { - await resumeDownload(id); + await resumeDownload(); toast.success(t("home.downloads.toasts.download_resumed")); } catch (error) { console.error("Error resuming download:", error); @@ -113,18 +111,12 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { {/* Action buttons in bottom right corner */} {process.status === "downloading" && Platform.OS !== "ios" && ( - handlePause(process.id)} - className='p-1' - > + handlePause()} className='p-1'> )} {process.status === "paused" && Platform.OS !== "ios" && ( - handleResume(process.id)} - className='p-1' - > + handleResume()} className='p-1'> )} @@ -181,18 +173,6 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { - {process.status === "completed" && ( - - - - )} ); diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index 3391f271..eb63ba6a 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -81,6 +81,7 @@ function useDownloadProvider() { updateProcess, removeProcess, onSuccess: successHapticFeedback, + api: api || undefined, }); // Get download operation functions diff --git a/providers/Downloads/README.md b/providers/Downloads/README.md index 349e6053..0cbd7bcf 100644 --- a/providers/Downloads/README.md +++ b/providers/Downloads/README.md @@ -1,228 +1,149 @@ -# Download System +# Downloads Module -This directory contains the types and utilities for the download system in Streamyfin. +This module handles all download functionality for the Streamyfin app, including video downloads, subtitles, trickplay images, and cover images. ## Architecture -### DownloadProvider +The downloads module is structured with a clean separation of concerns: -The `DownloadProvider` is a React context provider that manages all download operations in the app. It uses a custom native `BackgroundDownloader` module for iOS to enable true background downloads. +### Core Files -**Location**: `providers/DownloadProvider.tsx` +- **`database.ts`** - Pure functions for MMKV database operations +- **`fileOperations.ts`** - Pure functions for file system operations +- **`utils.ts`** - Pure utility functions (filename generation, URI conversion) +- **`additionalDownloads.ts`** - Pure functions for downloading additional assets +- **`notifications.ts`** - Pure functions for notification handling +- **`types.ts`** - TypeScript type definitions -### Key Features +### Hooks -1. **Background Downloads**: Downloads continue even when app is backgrounded -2. **Progress Tracking**: Real-time progress updates via native events -3. **Persistent Storage**: Downloads are saved to device storage and tracked in a JSON database -4. **Type Safety**: Full TypeScript support with proper types -5. **Notifications**: System notifications for download completion/errors +- **`useDownloadOperations.ts`** - Hook providing download operations (start, cancel, delete) +- **`useDownloadEventHandlers.ts`** - Hook setting up native download event listeners -### Database Structure +### Main Provider -Downloads are persisted in MMKV storage with the key `downloads.v2.json`: +- **`DownloadProvider.tsx`** - React context provider that orchestrates all download functionality -```typescript -interface DownloadsDatabase { - movies: Record; - series: Record; - other: Record; -} -``` +## Features -### Download Flow +### Video Downloads +- Background download support using native module +- Progress tracking and reporting +- Pause/resume capability (future enhancement) +- Download queue management -1. **Start Download** - ```typescript - await startBackgroundDownload(url, item, mediaSource, maxBitrate); - ``` +### Additional Assets (Automatic) +When a video download completes, the following are automatically downloaded: -2. **Track Progress** - - Native module emits progress events - - Provider updates `processes` state - - UI reflects current progress +1. **Trickplay Images** - Preview thumbnail sheets for video scrubbing +2. **Subtitles** - External subtitle files (for non-transcoded content) +3. **Cover Images** - Primary item images and series images +4. **Segments** - Intro and credit skip timestamps -3. **Handle Completion** - - File is moved to permanent location - - Database is updated - - User receives notification - - Process is cleaned up +### File Management +- Automatic cleanup of all associated files (video, subtitles, trickplay) +- Size calculation including all assets +- Batch delete operations -4. **Error Handling** - - Errors are caught and logged - - User receives error notification - - Process is marked as failed and removed +## Implementation Details -## Types +### Pure Functions +All core logic is implemented as pure functions that: +- Take explicit parameters +- Return explicit values +- Have no side effects +- Are easily testable -### JobStatus +### Imperative Design +The module uses imperative function calls rather than reactive patterns: +- Direct function invocation +- Explicit error handling +- Clear control flow +- Minimal side effects -Represents an active download job: +### Storage +- **MMKV** - Used for persistent database storage +- **expo-file-system** - Used for file operations +- **Native module** - Used for background downloads -```typescript -type JobStatus = { - id: string; // Item ID - inputUrl: string; // Download URL - item: BaseItemDto; // Jellyfin item - itemId: string; // Item ID - deviceId: string; // Device identifier - progress: number; // 0-100 - status: DownloadStatus; // Current status - timestamp: Date; // Created/updated time - mediaSource: MediaSourceInfo; // Media source info - maxBitrate: Bitrate; // Selected bitrate - bytesDownloaded?: number; // Bytes downloaded - lastProgressUpdateTime?: Date; // Last update time -}; -``` - -### DownloadedItem - -Represents a completed download in the database: - -```typescript -interface DownloadedItem { - item: BaseItemDto; - mediaSource: MediaSourceInfo; - videoFilePath: string; - videoFileSize: number; - videoFileName?: string; - trickPlayData?: TrickPlayData; - introSegments?: MediaTimeSegment[]; - creditSegments?: MediaTimeSegment[]; - userData: UserData; -} -``` - -## Usage Examples - -### Basic Download +## Usage ```typescript import { useDownload } from '@/providers/DownloadProvider'; function MyComponent() { - const { startBackgroundDownload } = useDownload(); + const { + startBackgroundDownload, + cancelDownload, + deleteFile, + getDownloadedItems, + processes, + } = useDownload(); - const handleDownload = async () => { - await startBackgroundDownload( - downloadUrl, - jellyfinItem, - mediaSource, - selectedBitrate - ); - }; -} -``` + // Start a download + await startBackgroundDownload(url, item, mediaSource, bitrate); -### Monitor Progress + // Cancel a download + await cancelDownload(itemId); -```typescript -function DownloadsList() { - const { processes } = useDownload(); + // Delete a download + await deleteFile(itemId); - return ( - - {processes.map(process => ( - - ))} - - ); -} -``` - -### List Downloaded Items - -```typescript -function DownloadedList() { - const { getDownloadedItems } = useDownload(); + // Get all downloads const items = getDownloadedItems(); - - return ( - ( - - )} - /> - ); } ``` -### Delete Downloads +## Event Flow -```typescript -function DeleteButton({ itemId }: { itemId: string }) { - const { deleteFile } = useDownload(); +1. **Start Download** + - Pre-download cover images + - Start video download via native module + - Track progress via event listeners - const handleDelete = async () => { - await deleteFile(itemId); - }; +2. **Download Progress** + - Native module emits progress events + - React state updated with progress percentage + - UI reflects current download state - return