mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
fix: building
This commit is contained in:
330
DOWNLOAD_SYSTEM_REFACTOR.md
Normal file
330
DOWNLOAD_SYSTEM_REFACTOR.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# 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<JobStatus[]>([]);
|
||||
|
||||
// Task ID to Process ID mapping for event correlation
|
||||
const taskMap = Map<number, string>();
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -7,16 +7,72 @@ enum DownloadError: Error {
|
||||
case downloadFailed
|
||||
}
|
||||
|
||||
public class BackgroundDownloaderModule: Module, URLSessionDownloadDelegate {
|
||||
private var session: URLSession?
|
||||
private static var backgroundCompletionHandler: (() -> Void)?
|
||||
private var downloadTasks: [Int: DownloadTaskInfo] = [:]
|
||||
struct DownloadTaskInfo {
|
||||
let url: String
|
||||
let destinationPath: String?
|
||||
}
|
||||
|
||||
// Separate delegate class to handle URLSession callbacks
|
||||
class DownloadSessionDelegate: NSObject, URLSessionDownloadDelegate {
|
||||
weak var module: BackgroundDownloaderModule?
|
||||
|
||||
struct DownloadTaskInfo {
|
||||
let url: String
|
||||
let destinationPath: String?
|
||||
init(module: BackgroundDownloaderModule) {
|
||||
self.module = module
|
||||
super.init()
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didWriteData bytesWritten: Int64,
|
||||
totalBytesWritten: Int64,
|
||||
totalBytesExpectedToWrite: Int64
|
||||
) {
|
||||
module?.handleProgress(
|
||||
taskId: downloadTask.taskIdentifier,
|
||||
bytesWritten: totalBytesWritten,
|
||||
totalBytes: totalBytesExpectedToWrite
|
||||
)
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didFinishDownloadingTo location: URL
|
||||
) {
|
||||
module?.handleDownloadComplete(
|
||||
taskId: downloadTask.taskIdentifier,
|
||||
location: location,
|
||||
downloadTask: downloadTask
|
||||
)
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didCompleteWithError error: Error?
|
||||
) {
|
||||
if let error = error {
|
||||
module?.handleError(taskId: task.taskIdentifier, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
|
||||
DispatchQueue.main.async {
|
||||
if let completion = BackgroundDownloaderModule.backgroundCompletionHandler {
|
||||
completion()
|
||||
BackgroundDownloaderModule.backgroundCompletionHandler = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class BackgroundDownloaderModule: Module {
|
||||
private var session: URLSession?
|
||||
private var sessionDelegate: DownloadSessionDelegate?
|
||||
fileprivate static var backgroundCompletionHandler: (() -> Void)?
|
||||
private var downloadTasks: [Int: DownloadTaskInfo] = [:]
|
||||
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("BackgroundDownloader")
|
||||
|
||||
@@ -84,7 +140,7 @@ public class BackgroundDownloaderModule: Module, URLSessionDownloadDelegate {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
self.session?.getAllTasks { tasks in
|
||||
let activeDownloads = tasks.compactMap { task -> [String: Any]? in
|
||||
guard let downloadTask = task as? URLSessionDownloadTask,
|
||||
guard task is URLSessionDownloadTask,
|
||||
let info = self.downloadTasks[task.taskIdentifier] else {
|
||||
return nil
|
||||
}
|
||||
@@ -109,9 +165,10 @@ public class BackgroundDownloaderModule: Module, URLSessionDownloadDelegate {
|
||||
config.sessionSendsLaunchEvents = true
|
||||
config.isDiscretionary = false
|
||||
|
||||
self.sessionDelegate = DownloadSessionDelegate(module: self)
|
||||
self.session = URLSession(
|
||||
configuration: config,
|
||||
delegate: self,
|
||||
delegate: self.sessionDelegate,
|
||||
delegateQueue: nil
|
||||
)
|
||||
}
|
||||
@@ -131,31 +188,21 @@ public class BackgroundDownloaderModule: Module, URLSessionDownloadDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public func urlSession(
|
||||
_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didWriteData bytesWritten: Int64,
|
||||
totalBytesWritten: Int64,
|
||||
totalBytesExpectedToWrite: Int64
|
||||
) {
|
||||
let progress = totalBytesExpectedToWrite > 0
|
||||
? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
|
||||
// Handler methods called by the delegate
|
||||
func handleProgress(taskId: Int, bytesWritten: Int64, totalBytes: Int64) {
|
||||
let progress = totalBytes > 0
|
||||
? Double(bytesWritten) / Double(totalBytes)
|
||||
: 0.0
|
||||
|
||||
self.sendEvent("onDownloadProgress", [
|
||||
"taskId": downloadTask.taskIdentifier,
|
||||
"bytesWritten": totalBytesWritten,
|
||||
"totalBytes": totalBytesExpectedToWrite,
|
||||
"taskId": taskId,
|
||||
"bytesWritten": bytesWritten,
|
||||
"totalBytes": totalBytes,
|
||||
"progress": progress
|
||||
])
|
||||
}
|
||||
|
||||
public func urlSession(
|
||||
_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didFinishDownloadingTo location: URL
|
||||
) {
|
||||
let taskId = downloadTask.taskIdentifier
|
||||
func handleDownloadComplete(taskId: Int, location: URL, downloadTask: URLSessionDownloadTask) {
|
||||
guard let taskInfo = downloadTasks[taskId] else {
|
||||
self.sendEvent("onDownloadError", [
|
||||
"taskId": taskId,
|
||||
@@ -210,34 +257,17 @@ public class BackgroundDownloaderModule: Module, URLSessionDownloadDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didCompleteWithError error: Error?
|
||||
) {
|
||||
if let error = error {
|
||||
let taskId = task.taskIdentifier
|
||||
|
||||
let isCancelled = (error as NSError).code == NSURLErrorCancelled
|
||||
|
||||
if !isCancelled {
|
||||
self.sendEvent("onDownloadError", [
|
||||
"taskId": taskId,
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
}
|
||||
|
||||
downloadTasks.removeValue(forKey: taskId)
|
||||
}
|
||||
}
|
||||
|
||||
public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
|
||||
DispatchQueue.main.async {
|
||||
if let completion = BackgroundDownloaderModule.backgroundCompletionHandler {
|
||||
completion()
|
||||
BackgroundDownloaderModule.backgroundCompletionHandler = nil
|
||||
}
|
||||
func handleError(taskId: Int, error: Error) {
|
||||
let isCancelled = (error as NSError).code == NSURLErrorCancelled
|
||||
|
||||
if !isCancelled {
|
||||
self.sendEvent("onDownloadError", [
|
||||
"taskId": taskId,
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
}
|
||||
|
||||
downloadTasks.removeValue(forKey: taskId)
|
||||
}
|
||||
|
||||
static func setBackgroundCompletionHandler(_ handler: @escaping () -> Void) {
|
||||
|
||||
1461
providers/DownloadProvider.deprecated.tsx
Normal file
1461
providers/DownloadProvider.deprecated.tsx
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
188
providers/Downloads/MIGRATION.md
Normal file
188
providers/Downloads/MIGRATION.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Download Provider Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The DownloadProvider has been completely rewritten to use the new native `BackgroundDownloader` module instead of the third-party `@kesha-antonov/react-native-background-downloader` library.
|
||||
|
||||
## What Changed
|
||||
|
||||
### New Implementation
|
||||
|
||||
- **Native Module**: Uses our custom `BackgroundDownloader` Expo module built with NSURLSession
|
||||
- **Simplified**: Focuses only on downloading video files
|
||||
- **Background Support**: True iOS background downloads with system integration
|
||||
- **Event-Driven**: Uses native event emitters for progress, completion, and errors
|
||||
|
||||
### Removed Features (For Now)
|
||||
|
||||
The following features from the old implementation have been temporarily removed to simplify the initial version:
|
||||
|
||||
- ✗ Trickplay image downloads
|
||||
- ✗ Subtitle downloads
|
||||
- ✗ Series primary image caching
|
||||
- ✗ Intro/credit segment fetching
|
||||
- ✗ Download queue management with concurrent limits
|
||||
- ✗ Pause/Resume functionality
|
||||
- ✗ Speed calculation and ETA
|
||||
- ✗ Cache directory management
|
||||
|
||||
### Maintained Features
|
||||
|
||||
- ✓ Download video files with progress tracking
|
||||
- ✓ Database persistence (same structure)
|
||||
- ✓ Movies and Episodes support
|
||||
- ✓ Download notifications
|
||||
- ✓ File deletion and management
|
||||
- ✓ Downloaded items listing
|
||||
- ✓ Same context API
|
||||
|
||||
## API Compatibility
|
||||
|
||||
The public API remains mostly the same to avoid breaking existing code:
|
||||
|
||||
### Working Methods
|
||||
|
||||
```typescript
|
||||
const {
|
||||
// Core functionality
|
||||
startBackgroundDownload,
|
||||
cancelDownload,
|
||||
|
||||
// Database operations
|
||||
getDownloadedItems,
|
||||
getDownloadsDatabase,
|
||||
getDownloadedItemById,
|
||||
getDownloadedItemSize,
|
||||
|
||||
// File management
|
||||
deleteFile,
|
||||
deleteItems,
|
||||
deleteAllFiles,
|
||||
|
||||
// State
|
||||
processes,
|
||||
APP_CACHE_DOWNLOAD_DIRECTORY,
|
||||
appSizeUsage,
|
||||
} = useDownload();
|
||||
```
|
||||
|
||||
### Deprecated (No-op) Methods
|
||||
|
||||
These methods exist but do nothing in the new version:
|
||||
|
||||
- `startDownload()` - Use `startBackgroundDownload()` instead
|
||||
- `pauseDownload()` - Not supported yet
|
||||
- `resumeDownload()` - Not supported yet
|
||||
- `deleteFileByType()` - Not needed (only video files)
|
||||
- `cleanCacheDirectory()` - Not needed
|
||||
- `updateDownloadedItem()` - Not needed
|
||||
- `dumpDownloadDiagnostics()` - Not needed
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### For Developers
|
||||
|
||||
1. **No code changes needed** if you're using `startBackgroundDownload()` and basic file management
|
||||
2. **Remove calls** to deprecated methods (they won't break but do nothing)
|
||||
3. **Test downloads** to ensure they work in your workflows
|
||||
|
||||
### For Users
|
||||
|
||||
- **No action required** - the new system uses the same database format
|
||||
- **Existing downloads** will still be accessible
|
||||
- **New downloads** will use the improved background system
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned features to add back:
|
||||
|
||||
1. **Pause/Resume**: Using NSURLSession's built-in pause/resume
|
||||
2. **Queue Management**: Better control over concurrent downloads
|
||||
3. **Trickplay**: Re-add trickplay image downloading
|
||||
4. **Subtitles**: Download and link subtitle files
|
||||
5. **Progress Persistence**: Resume downloads after app restart
|
||||
6. **Cellular Control**: Respect cellular data settings
|
||||
7. **Speed/ETA**: Better download metrics
|
||||
|
||||
## Database Structure
|
||||
|
||||
The database structure remains unchanged:
|
||||
|
||||
```typescript
|
||||
interface DownloadsDatabase {
|
||||
movies: Record<string, DownloadedItem>;
|
||||
series: Record<string, DownloadedSeries>;
|
||||
other: Record<string, DownloadedItem>;
|
||||
}
|
||||
|
||||
interface DownloadedItem {
|
||||
item: BaseItemDto;
|
||||
mediaSource: MediaSourceInfo;
|
||||
videoFilePath: string;
|
||||
videoFileSize: number;
|
||||
videoFileName?: string;
|
||||
trickPlayData?: TrickPlayData;
|
||||
introSegments?: MediaTimeSegment[];
|
||||
creditSegments?: MediaTimeSegment[];
|
||||
userData: UserData;
|
||||
}
|
||||
```
|
||||
|
||||
## Known Differences
|
||||
|
||||
1. **Progress Updates**: More frequent and accurate with native module
|
||||
2. **Background Handling**: Better iOS background download support
|
||||
3. **Error Messages**: Different error format from native module
|
||||
4. **File Paths**: Uses `Paths.document` instead of cache directory
|
||||
5. **No Queue**: Downloads start immediately (no queuing system yet)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Downloads not starting
|
||||
|
||||
- Check that the iOS app has been rebuilt with the new native module
|
||||
- Verify network permissions
|
||||
- Check console logs for errors
|
||||
|
||||
### Progress not updating
|
||||
|
||||
- Ensure event listeners are properly registered
|
||||
- Check that the task ID mapping is correct
|
||||
- Verify the download is still active
|
||||
|
||||
### Files not found
|
||||
|
||||
- Old downloads might be in a different location
|
||||
- Re-download content if files are missing
|
||||
- Check file permissions
|
||||
|
||||
## Old Implementation
|
||||
|
||||
The old implementation has been preserved at:
|
||||
- `providers/DownloadProvider.deprecated.tsx`
|
||||
|
||||
You can reference it if needed, but it should not be used in production.
|
||||
|
||||
## Testing
|
||||
|
||||
After migration, test these scenarios:
|
||||
|
||||
- [ ] Download a movie
|
||||
- [ ] Download an episode
|
||||
- [ ] Download multiple items
|
||||
- [ ] Cancel a download
|
||||
- [ ] Delete a downloaded item
|
||||
- [ ] View downloaded items list
|
||||
- [ ] Background app during download
|
||||
- [ ] Force quit and restart app
|
||||
- [ ] Verify notifications appear
|
||||
- [ ] Check file sizes are correct
|
||||
|
||||
## Questions?
|
||||
|
||||
If you encounter issues with the migration, please:
|
||||
1. Check the console logs
|
||||
2. Verify the native module is installed
|
||||
3. Review the old implementation for reference
|
||||
4. Open an issue with details
|
||||
|
||||
228
providers/Downloads/README.md
Normal file
228
providers/Downloads/README.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Download System
|
||||
|
||||
This directory contains the types and utilities for the download system in Streamyfin.
|
||||
|
||||
## Architecture
|
||||
|
||||
### DownloadProvider
|
||||
|
||||
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.
|
||||
|
||||
**Location**: `providers/DownloadProvider.tsx`
|
||||
|
||||
### Key Features
|
||||
|
||||
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
|
||||
|
||||
### Database Structure
|
||||
|
||||
Downloads are persisted in MMKV storage with the key `downloads.v2.json`:
|
||||
|
||||
```typescript
|
||||
interface DownloadsDatabase {
|
||||
movies: Record<string, DownloadedItem>;
|
||||
series: Record<string, DownloadedSeries>;
|
||||
other: Record<string, DownloadedItem>;
|
||||
}
|
||||
```
|
||||
|
||||
### Download Flow
|
||||
|
||||
1. **Start Download**
|
||||
```typescript
|
||||
await startBackgroundDownload(url, item, mediaSource, maxBitrate);
|
||||
```
|
||||
|
||||
2. **Track Progress**
|
||||
- Native module emits progress events
|
||||
- Provider updates `processes` state
|
||||
- UI reflects current progress
|
||||
|
||||
3. **Handle Completion**
|
||||
- File is moved to permanent location
|
||||
- Database is updated
|
||||
- User receives notification
|
||||
- Process is cleaned up
|
||||
|
||||
4. **Error Handling**
|
||||
- Errors are caught and logged
|
||||
- User receives error notification
|
||||
- Process is marked as failed and removed
|
||||
|
||||
## Types
|
||||
|
||||
### JobStatus
|
||||
|
||||
Represents an active download job:
|
||||
|
||||
```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
|
||||
|
||||
```typescript
|
||||
import { useDownload } from '@/providers/DownloadProvider';
|
||||
|
||||
function MyComponent() {
|
||||
const { startBackgroundDownload } = useDownload();
|
||||
|
||||
const handleDownload = async () => {
|
||||
await startBackgroundDownload(
|
||||
downloadUrl,
|
||||
jellyfinItem,
|
||||
mediaSource,
|
||||
selectedBitrate
|
||||
);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Monitor Progress
|
||||
|
||||
```typescript
|
||||
function DownloadsList() {
|
||||
const { processes } = useDownload();
|
||||
|
||||
return (
|
||||
<View>
|
||||
{processes.map(process => (
|
||||
<ProgressBar
|
||||
key={process.id}
|
||||
progress={process.progress}
|
||||
title={process.item.Name}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### List Downloaded Items
|
||||
|
||||
```typescript
|
||||
function DownloadedList() {
|
||||
const { getDownloadedItems } = useDownload();
|
||||
const items = getDownloadedItems();
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={items}
|
||||
renderItem={({ item }) => (
|
||||
<ItemCard item={item.item} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Downloads
|
||||
|
||||
```typescript
|
||||
function DeleteButton({ itemId }: { itemId: string }) {
|
||||
const { deleteFile } = useDownload();
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteFile(itemId);
|
||||
};
|
||||
|
||||
return <Button onPress={handleDelete} title="Delete" />;
|
||||
}
|
||||
```
|
||||
|
||||
## File Storage
|
||||
|
||||
Downloads are stored in the app's Documents directory:
|
||||
|
||||
```
|
||||
Documents/
|
||||
└── [filename].mp4
|
||||
```
|
||||
|
||||
Filenames are generated based on item type:
|
||||
- Movies: `{title}_{year}.mp4`
|
||||
- Episodes: `{series}_s{season}e{episode}.mp4`
|
||||
|
||||
## Native Module Integration
|
||||
|
||||
The provider uses the `BackgroundDownloader` native module:
|
||||
|
||||
```typescript
|
||||
import { BackgroundDownloader } from '@/modules';
|
||||
|
||||
// Start download
|
||||
const taskId = await BackgroundDownloader.startDownload(url, destPath);
|
||||
|
||||
// Listen for events
|
||||
BackgroundDownloader.addProgressListener(event => {
|
||||
// Handle progress
|
||||
});
|
||||
|
||||
BackgroundDownloader.addCompleteListener(event => {
|
||||
// Handle completion
|
||||
});
|
||||
|
||||
BackgroundDownloader.addErrorListener(event => {
|
||||
// Handle error
|
||||
});
|
||||
```
|
||||
|
||||
## Platform Support
|
||||
|
||||
- **iOS**: Full support with background downloads
|
||||
- **Android**: Planned
|
||||
- **tvOS**: Disabled (returns no-op functions)
|
||||
|
||||
## Migration
|
||||
|
||||
If upgrading from the old download system, see [MIGRATION.md](./MIGRATION.md) for details.
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- [ ] Add pause/resume functionality
|
||||
- [ ] Implement download queue with concurrent limits
|
||||
- [ ] Add trickplay image downloads
|
||||
- [ ] Add subtitle downloads
|
||||
- [ ] Add intro/credit segment detection
|
||||
- [ ] Persist downloads across app restarts
|
||||
- [ ] Add cellular data controls
|
||||
- [ ] Improve download speed calculation
|
||||
- [ ] Add download size estimates
|
||||
|
||||
Reference in New Issue
Block a user