mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-18 18:24:18 +01: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
|
case downloadFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BackgroundDownloaderModule: Module, URLSessionDownloadDelegate {
|
struct DownloadTaskInfo {
|
||||||
private var session: URLSession?
|
let url: String
|
||||||
private static var backgroundCompletionHandler: (() -> Void)?
|
let destinationPath: String?
|
||||||
private var downloadTasks: [Int: DownloadTaskInfo] = [:]
|
}
|
||||||
|
|
||||||
|
// Separate delegate class to handle URLSession callbacks
|
||||||
|
class DownloadSessionDelegate: NSObject, URLSessionDownloadDelegate {
|
||||||
|
weak var module: BackgroundDownloaderModule?
|
||||||
|
|
||||||
struct DownloadTaskInfo {
|
init(module: BackgroundDownloaderModule) {
|
||||||
let url: String
|
self.module = module
|
||||||
let destinationPath: String?
|
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 {
|
public func definition() -> ModuleDefinition {
|
||||||
Name("BackgroundDownloader")
|
Name("BackgroundDownloader")
|
||||||
|
|
||||||
@@ -84,7 +140,7 @@ public class BackgroundDownloaderModule: Module, URLSessionDownloadDelegate {
|
|||||||
return try await withCheckedThrowingContinuation { continuation in
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
self.session?.getAllTasks { tasks in
|
self.session?.getAllTasks { tasks in
|
||||||
let activeDownloads = tasks.compactMap { task -> [String: Any]? 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 {
|
let info = self.downloadTasks[task.taskIdentifier] else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -109,9 +165,10 @@ public class BackgroundDownloaderModule: Module, URLSessionDownloadDelegate {
|
|||||||
config.sessionSendsLaunchEvents = true
|
config.sessionSendsLaunchEvents = true
|
||||||
config.isDiscretionary = false
|
config.isDiscretionary = false
|
||||||
|
|
||||||
|
self.sessionDelegate = DownloadSessionDelegate(module: self)
|
||||||
self.session = URLSession(
|
self.session = URLSession(
|
||||||
configuration: config,
|
configuration: config,
|
||||||
delegate: self,
|
delegate: self.sessionDelegate,
|
||||||
delegateQueue: nil
|
delegateQueue: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -131,31 +188,21 @@ public class BackgroundDownloaderModule: Module, URLSessionDownloadDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func urlSession(
|
// Handler methods called by the delegate
|
||||||
_ session: URLSession,
|
func handleProgress(taskId: Int, bytesWritten: Int64, totalBytes: Int64) {
|
||||||
downloadTask: URLSessionDownloadTask,
|
let progress = totalBytes > 0
|
||||||
didWriteData bytesWritten: Int64,
|
? Double(bytesWritten) / Double(totalBytes)
|
||||||
totalBytesWritten: Int64,
|
|
||||||
totalBytesExpectedToWrite: Int64
|
|
||||||
) {
|
|
||||||
let progress = totalBytesExpectedToWrite > 0
|
|
||||||
? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
|
|
||||||
: 0.0
|
: 0.0
|
||||||
|
|
||||||
self.sendEvent("onDownloadProgress", [
|
self.sendEvent("onDownloadProgress", [
|
||||||
"taskId": downloadTask.taskIdentifier,
|
"taskId": taskId,
|
||||||
"bytesWritten": totalBytesWritten,
|
"bytesWritten": bytesWritten,
|
||||||
"totalBytes": totalBytesExpectedToWrite,
|
"totalBytes": totalBytes,
|
||||||
"progress": progress
|
"progress": progress
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
public func urlSession(
|
func handleDownloadComplete(taskId: Int, location: URL, downloadTask: URLSessionDownloadTask) {
|
||||||
_ session: URLSession,
|
|
||||||
downloadTask: URLSessionDownloadTask,
|
|
||||||
didFinishDownloadingTo location: URL
|
|
||||||
) {
|
|
||||||
let taskId = downloadTask.taskIdentifier
|
|
||||||
guard let taskInfo = downloadTasks[taskId] else {
|
guard let taskInfo = downloadTasks[taskId] else {
|
||||||
self.sendEvent("onDownloadError", [
|
self.sendEvent("onDownloadError", [
|
||||||
"taskId": taskId,
|
"taskId": taskId,
|
||||||
@@ -210,34 +257,17 @@ public class BackgroundDownloaderModule: Module, URLSessionDownloadDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func urlSession(
|
func handleError(taskId: Int, error: Error) {
|
||||||
_ session: URLSession,
|
let isCancelled = (error as NSError).code == NSURLErrorCancelled
|
||||||
task: URLSessionTask,
|
|
||||||
didCompleteWithError error: Error?
|
if !isCancelled {
|
||||||
) {
|
self.sendEvent("onDownloadError", [
|
||||||
if let error = error {
|
"taskId": taskId,
|
||||||
let taskId = task.taskIdentifier
|
"error": error.localizedDescription
|
||||||
|
])
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadTasks.removeValue(forKey: taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func setBackgroundCompletionHandler(_ handler: @escaping () -> Void) {
|
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