diff --git a/DOWNLOAD_SYSTEM_REFACTOR.md b/DOWNLOAD_SYSTEM_REFACTOR.md deleted file mode 100644 index e0ee68f2..00000000 --- a/DOWNLOAD_SYSTEM_REFACTOR.md +++ /dev/null @@ -1,330 +0,0 @@ -# Download System Refactor - Summary - -## Overview - -The download system has been completely refactored to use a new native iOS module for background downloads, replacing the third-party library with a custom, streamlined solution. - -## What Was Done - -### 1. Created New Native Module (`BackgroundDownloader`) - -**Location**: `modules/background-downloader/` - -A complete Expo native module for iOS background file downloads: - -- **Swift Implementation**: `BackgroundDownloaderModule.swift` - - Uses `NSURLSession` with background configuration - - Implements `URLSessionDownloadDelegate` for progress tracking - - Handles iOS background session events via AppDelegate subscriber - - Session ID: `com.fredrikburmester.streamyfin.backgrounddownloader` - -- **TypeScript Interface**: Full type-safe API with events - - `startDownload(url, destinationPath?)` - Start a download - - `cancelDownload(taskId)` - Cancel a specific download - - `cancelAllDownloads()` - Cancel all downloads - - `getActiveDownloads()` - List active downloads - -- **Events**: - - `onDownloadProgress` - Progress updates with bytes/percentage - - `onDownloadComplete` - Completion with file path - - `onDownloadError` - Error handling - - `onDownloadStarted` - Download initiation confirmation - -- **Documentation**: Complete README with usage examples - -### 2. Rewrote DownloadProvider - -**Location**: `providers/DownloadProvider.tsx` - -A simplified, focused implementation: - -**Features Included**: -- ✅ Video file downloads with progress tracking -- ✅ Background download support -- ✅ Database persistence (same format as before) -- ✅ Movies and TV episodes support -- ✅ Download notifications (success/error) -- ✅ File deletion and management -- ✅ Size calculation -- ✅ Same context API for backward compatibility - -**Features Removed (for simplicity)**: -- ❌ Trickplay image downloads -- ❌ Subtitle downloads -- ❌ Queue management with concurrent limits -- ❌ Pause/Resume (can be added back easily) -- ❌ Download speed/ETA calculations -- ❌ Cache directory management - -**Key Improvements**: -- Event-driven architecture (no polling) -- Better background handling via native module -- Cleaner, more maintainable code -- Proper TypeScript typing throughout -- Simplified state management - -### 3. Preserved Old Implementation - -**Location**: `providers/DownloadProvider.deprecated.tsx` - -The old implementation has been preserved for reference but should not be used. - -### 4. Documentation - -Created comprehensive documentation: - -- **Module README**: `modules/background-downloader/README.md` - - API reference - - Usage examples - - Implementation details - -- **Migration Guide**: `providers/Downloads/MIGRATION.md` - - What changed - - API compatibility matrix - - Migration steps - - Troubleshooting guide - -- **System README**: `providers/Downloads/README.md` - - Architecture overview - - Type definitions - - Usage examples - - File storage details - -## Technical Details - -### Background Download Implementation - -The native module uses iOS's recommended approach: - -1. **Background URLSession**: Persistent identifier ensures downloads continue -2. **Delegate Pattern**: Progress and completion via delegate callbacks -3. **AppDelegate Integration**: Handles system wake-ups for download events -4. **Completion Handler**: Properly signals iOS when background work is done - -### State Management - -```typescript -// Active downloads tracked in Jotai atom -const processesAtom = atom([]); - -// Task ID to Process ID mapping for event correlation -const taskMap = Map(); - -// Persistent database in MMKV -storage.set('downloads.v2.json', JSON.stringify(database)); -``` - -### Download Flow - -``` -User initiates download - ↓ -Create JobStatus & add to processes - ↓ -Generate destination path - ↓ -Start native BackgroundDownloader - ↓ -Map taskId ↔ processId - ↓ -Receive progress events → Update UI - ↓ -Receive completion event - ↓ -Move file to permanent location - ↓ -Save to database - ↓ -Send notification - ↓ -Clean up process -``` - -## Configuration - -### iOS Background Modes - -Already configured in `app.json`: - -```json -{ - "ios": { - "infoPlist": { - "UIBackgroundModes": ["audio", "fetch"] - } - } -} -``` - -The "fetch" mode enables background URLSession downloads. - -### Module Integration - -The module is: -- ✅ Auto-linked via Expo -- ✅ Exported from `modules/index.ts` -- ✅ Type-safe with full TypeScript support -- ✅ Registered as AppDelegate subscriber - -## Breaking Changes - -### None for Normal Usage - -The public API remains the same for the most common operations: - -```typescript -const { - startBackgroundDownload, - cancelDownload, - getDownloadedItems, - deleteFile, - processes -} = useDownload(); -``` - -### No-op Methods - -These methods exist but do nothing (for compatibility): -- `pauseDownload()` -- `resumeDownload()` -- `startDownload()` (use `startBackgroundDownload` instead) -- `deleteFileByType()` -- `cleanCacheDirectory()` -- `updateDownloadedItem()` -- `dumpDownloadDiagnostics()` - -## Testing Checklist - -Before deployment, test: - -- [ ] Download a movie -- [ ] Download a TV episode -- [ ] Download multiple items simultaneously -- [ ] Cancel an active download -- [ ] Delete a downloaded item -- [ ] View list of downloads -- [ ] Background the app during download -- [ ] Force quit and restart (download should be cancelled) -- [ ] Verify notifications appear -- [ ] Check file integrity and playback -- [ ] Verify database persistence -- [ ] Check storage calculations - -## Next Steps - -### Immediate - -1. **Rebuild iOS app**: - ```bash - npx expo prebuild -p ios - cd ios && pod install && cd .. - npx expo run:ios - ``` - -2. **Test thoroughly**: Use the testing checklist above - -3. **Monitor for issues**: Check console logs and user reports - -### Future Enhancements - -Priority features to add back: - -1. **Pause/Resume** (High Priority) - - Easy to implement with NSURLSession - - User-requested feature - -2. **Queue Management** (Medium Priority) - - Concurrent download limits - - Automatic queue processing - -3. **Progress Persistence** (Medium Priority) - - Resume interrupted downloads after app restart - - Save download state to database - -4. **Trickplay & Subtitles** (Low Priority) - - Re-add auxiliary file downloads - - Integrate with video playback - -5. **Download Analytics** (Low Priority) - - Speed calculation - - ETA estimation - - Failure rate tracking - -## Known Limitations - -1. **iOS Only**: Android support not yet implemented -2. **No Pause**: Cannot pause/resume downloads yet -3. **No Queue**: All downloads start immediately -4. **Force Quit**: Downloads cancelled if app is force-quit (iOS limitation) -5. **No Persistence**: Downloads lost if app crashes or is terminated - -## Performance Improvements - -Over the old system: - -- **Better Background Support**: True iOS background sessions -- **Event-Driven**: No polling, immediate updates -- **Lower Overhead**: Removed unused features -- **Native Integration**: Tighter iOS system integration -- **Cleaner Code**: Easier to maintain and extend - -## Dependencies Changed - -### Removed -- `@kesha-antonov/react-native-background-downloader` - -### Added -- Custom `BackgroundDownloader` native module (local) - -No external dependencies added, reducing maintenance burden. - -## File Changes Summary - -### New Files -- `modules/background-downloader/` (entire module) -- `providers/Downloads/MIGRATION.md` -- `providers/Downloads/README.md` -- `DOWNLOAD_SYSTEM_REFACTOR.md` (this file) - -### Modified Files -- `modules/index.ts` (exported new module) -- `providers/DownloadProvider.tsx` (complete rewrite) - -### Renamed Files -- `providers/DownloadProvider.tsx` → `providers/DownloadProvider.deprecated.tsx` - -### Unchanged -- `providers/Downloads/types.ts` (types remain the same) -- Database format and storage location -- Public API for most common operations - -## Rollback Plan - -If issues arise: - -1. Rename `DownloadProvider.deprecated.tsx` back to `DownloadProvider.tsx` -2. Remove the `background-downloader` module export from `modules/index.ts` -3. Re-install `@kesha-antonov/react-native-background-downloader` -4. Rebuild the app - -Note: The database format is unchanged, so existing downloads will work with either version. - -## Success Metrics - -The refactor is successful if: - -- ✅ Downloads work in foreground -- ✅ Downloads continue in background -- ✅ Progress updates are accurate -- ✅ Notifications work correctly -- ✅ Files are saved and playable -- ✅ No crashes or memory leaks -- ✅ Performance is equal or better -- ✅ Code is cleaner and more maintainable - -## Conclusion - -This refactor provides a solid foundation for the download system moving forward. The native module approach gives us full control over the download experience and makes it easier to add features in the future. - -The simplified DownloadProvider focuses on core functionality while maintaining API compatibility, making this a low-risk, high-reward change. - diff --git a/app.config.js b/app.config.js index 6621e022..d72e1e73 100644 --- a/app.config.js +++ b/app.config.js @@ -6,9 +6,6 @@ module.exports = ({ config }) => { "react-native-google-cast", { useDefaultExpandedMediaControls: true }, ]); - - // Add the background downloader plugin only for non-TV builds - config.plugins.push("./plugins/withRNBackgroundDownloader.js"); } return { android: { diff --git a/components/downloads/DownloadCard.tsx b/components/downloads/DownloadCard.tsx index 6d0682a4..83b346fa 100644 --- a/components/downloads/DownloadCard.tsx +++ b/components/downloads/DownloadCard.tsx @@ -6,7 +6,6 @@ import { t } from "i18next"; import { useMemo } from "react"; import { ActivityIndicator, - Platform, TouchableOpacity, type TouchableOpacityProps, View, @@ -28,30 +27,10 @@ interface DownloadCardProps extends TouchableOpacityProps { } export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { - const { pauseDownload, resumeDownload, removeProcess } = useDownload(); + const { removeProcess } = useDownload(); const router = useRouter(); const queryClient = useQueryClient(); - const handlePause = async () => { - try { - await pauseDownload(); - toast.success(t("home.downloads.toasts.download_paused")); - } catch (error) { - console.error("Error pausing download:", error); - toast.error(t("home.downloads.toasts.could_not_pause_download")); - } - }; - - const handleResume = async () => { - try { - await resumeDownload(); - toast.success(t("home.downloads.toasts.download_resumed")); - } catch (error) { - console.error("Error resuming download:", error); - toast.error(t("home.downloads.toasts.could_not_resume_download")); - } - }; - const handleDelete = async (id: string) => { try { await removeProcess(id); @@ -118,22 +97,6 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { {/* Action buttons in bottom right corner */} - {process.status === "downloading" && Platform.OS !== "ios" && ( - handlePause()} - className='p-2 bg-neutral-800 rounded-full' - > - - - )} - {process.status === "paused" && Platform.OS !== "ios" && ( - handleResume()} - className='p-2 bg-neutral-800 rounded-full' - > - - - )} handleDelete(process.id)} className='p-2 bg-neutral-800 rounded-full' diff --git a/plugins/withRNBackgroundDownloader.js b/plugins/withRNBackgroundDownloader.js deleted file mode 100644 index af24d642..00000000 --- a/plugins/withRNBackgroundDownloader.js +++ /dev/null @@ -1,68 +0,0 @@ -const { withAppDelegate, withXcodeProject } = require("expo/config-plugins"); -const fs = require("node:fs"); -const path = require("node:path"); - -/** @param {import("expo/config-plugins").ExpoConfig} config */ -function withRNBackgroundDownloader(config) { - /* 1️⃣ Add handleEventsForBackgroundURLSession to AppDelegate.swift */ - config = withAppDelegate(config, (mod) => { - const tag = "handleEventsForBackgroundURLSession"; - if (!mod.modResults.contents.includes(tag)) { - mod.modResults.contents = mod.modResults.contents.replace( - /\}\s*$/, // insert before final } - ` - func application( - _ application: UIApplication, - handleEventsForBackgroundURLSession identifier: String, - completionHandler: @escaping () -> Void - ) { - RNBackgroundDownloader.setCompletionHandlerWithIdentifier(identifier, completionHandler: completionHandler) - } -}`, - ); - } - return mod; - }); - - /* 2️⃣ Ensure bridging header exists & is attached to *every* app target */ - config = withXcodeProject(config, (mod) => { - const project = mod.modResults; - const projectName = config.name || "App"; - // Fix: Go up one more directory to get to ios/, not ios/ProjectName.xcodeproj/ - const iosDir = path.dirname(path.dirname(project.filepath)); - const headerRel = `${projectName}/${projectName}-Bridging-Header.h`; - const headerAbs = path.join(iosDir, headerRel); - - // create / append import if missing - let headerText = ""; - try { - headerText = fs.readFileSync(headerAbs, "utf8"); - } catch (error) { - if (error.code !== "ENOENT") { - throw error; - } - } - if (!headerText.includes("RNBackgroundDownloader.h")) { - fs.mkdirSync(path.dirname(headerAbs), { recursive: true }); - fs.appendFileSync(headerAbs, '#import "RNBackgroundDownloader.h"\n'); - } - - // Expo 53's xcode‑js doesn't expose pbxTargets(). - // Setting the property once at the project level is sufficient. - ["Debug", "Release"].forEach((cfg) => { - // Use the detected projectName to set the bridging header path instead of a hardcoded value - const bridgingHeaderPath = `${projectName}/${projectName}-Bridging-Header.h`; - project.updateBuildProperty( - "SWIFT_OBJC_BRIDGING_HEADER", - bridgingHeaderPath, - cfg, - ); - }); - - return mod; - }); - - return config; -} - -module.exports = withRNBackgroundDownloader; diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index a4d25667..617217a9 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -135,8 +135,6 @@ function useDownloadProvider() { appSizeUsage, // Deprecated/not implemented in simple version startDownload: async () => {}, - pauseDownload: async () => {}, - resumeDownload: async () => {}, cleanCacheDirectory: async () => {}, updateDownloadedItem: () => {}, dumpDownloadDiagnostics: async () => "", @@ -161,8 +159,6 @@ export function useDownload() { cancelDownload: async () => {}, triggerRefresh: () => {}, startDownload: async () => {}, - pauseDownload: async () => {}, - resumeDownload: async () => {}, getDownloadedItemSize: () => 0, getDownloadedItemById: () => undefined, APP_CACHE_DOWNLOAD_DIRECTORY: "", diff --git a/providers/Downloads/types.ts b/providers/Downloads/types.ts index 409c8291..e0cfefc6 100644 --- a/providers/Downloads/types.ts +++ b/providers/Downloads/types.ts @@ -113,7 +113,6 @@ export type JobStatus = { /** Current status of the download job */ status: | "downloading" // The job is actively downloading - | "paused" // The job is paused | "error" // The job encountered an error | "pending" // The job is waiting to start | "completed" // The job has finished downloading @@ -133,14 +132,4 @@ export type JobStatus = { /** Estimated total size of the download in bytes (optional) this is used when we * download transcoded content because we don't know the size of the file until it's downloaded */ estimatedTotalSizeBytes?: number; - /** Timestamp when the download was paused (optional) */ - pausedAt?: Date; - /** Progress percentage when download was paused (optional) */ - pausedProgress?: number; - /** Bytes downloaded when download was paused (optional) */ - pausedBytes?: number; - /** Bytes downloaded in the current session (since last resume). Used for session-only speed calculation. */ - lastSessionBytes?: number; - /** Timestamp when the session-only bytes were last updated. */ - lastSessionUpdateTime?: Date; };