fix: building

This commit is contained in:
Fredrik Burmester
2025-10-02 20:54:25 +02:00
parent ec622aba55
commit 8d59065c49
6 changed files with 2722 additions and 1229 deletions

330
DOWNLOAD_SYSTEM_REFACTOR.md Normal file
View 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.

View File

@@ -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) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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

View 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