mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-04 21:18:31 +01:00
fix: concurrent downloads
This commit is contained in:
@@ -25,7 +25,6 @@ const getETA = (download: DownloadInfo): string | null => {
|
|||||||
!download.secondsDownloaded ||
|
!download.secondsDownloaded ||
|
||||||
!download.secondsTotal
|
!download.secondsTotal
|
||||||
) {
|
) {
|
||||||
console.log(download);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const elapsed = Date.now() / 1000 - download.startTime;
|
const elapsed = Date.now() / 1000 - download.startTime;
|
||||||
|
|||||||
@@ -1,13 +1,67 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
import ExpoModulesCore
|
import ExpoModulesCore
|
||||||
|
|
||||||
|
class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
|
||||||
|
weak var module: HlsDownloaderModule?
|
||||||
|
var taskIdentifier: Int = 0
|
||||||
|
var providedId: String = ""
|
||||||
|
var downloadedSeconds: Double = 0
|
||||||
|
var totalSeconds: Double = 0
|
||||||
|
var startTime: Double = 0
|
||||||
|
|
||||||
|
init(module: HlsDownloaderModule) {
|
||||||
|
self.module = module
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func urlSession(
|
||||||
|
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange,
|
||||||
|
totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange
|
||||||
|
) {
|
||||||
|
module?.urlSession(
|
||||||
|
session, assetDownloadTask: assetDownloadTask, didLoad: timeRange,
|
||||||
|
totalTimeRangesLoaded: loadedTimeRanges, timeRangeExpectedToLoad: timeRangeExpectedToLoad)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func urlSession(
|
||||||
|
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask,
|
||||||
|
didFinishDownloadingTo location: URL
|
||||||
|
) {
|
||||||
|
module?.urlSession(
|
||||||
|
session, assetDownloadTask: assetDownloadTask, didFinishDownloadingTo: location)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func urlSession(
|
||||||
|
_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?
|
||||||
|
) {
|
||||||
|
module?.urlSession(session, task: task, didCompleteWithError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class HlsDownloaderModule: Module {
|
public class HlsDownloaderModule: Module {
|
||||||
|
private lazy var delegateHandler: HlsDownloaderDelegate = {
|
||||||
|
return HlsDownloaderDelegate(module: self)
|
||||||
|
}()
|
||||||
|
|
||||||
var activeDownloads:
|
var activeDownloads:
|
||||||
[Int: (
|
[Int: (
|
||||||
task: AVAssetDownloadTask, delegate: HLSDownloadDelegate, metadata: [String: Any],
|
task: AVAssetDownloadTask, delegate: HLSDownloadDelegate, metadata: [String: Any],
|
||||||
startTime: Double
|
startTime: Double
|
||||||
)] = [:]
|
)] = [:]
|
||||||
|
|
||||||
|
private lazy var downloadSession: AVAssetDownloadURLSession = {
|
||||||
|
let configuration = URLSessionConfiguration.background(
|
||||||
|
withIdentifier: "com.example.hlsdownload")
|
||||||
|
configuration.allowsCellularAccess = true
|
||||||
|
configuration.sessionSendsLaunchEvents = true
|
||||||
|
configuration.isDiscretionary = false
|
||||||
|
return AVAssetDownloadURLSession(
|
||||||
|
configuration: configuration,
|
||||||
|
assetDownloadDelegate: delegateHandler,
|
||||||
|
delegateQueue: OperationQueue.main
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
|
||||||
public func definition() -> ModuleDefinition {
|
public func definition() -> ModuleDefinition {
|
||||||
Name("HlsDownloader")
|
Name("HlsDownloader")
|
||||||
|
|
||||||
@@ -16,20 +70,15 @@ public class HlsDownloaderModule: Module {
|
|||||||
Function("downloadHLSAsset") {
|
Function("downloadHLSAsset") {
|
||||||
(providedId: String, url: String, metadata: [String: Any]?) -> Void in
|
(providedId: String, url: String, metadata: [String: Any]?) -> Void in
|
||||||
let startTime = Date().timeIntervalSince1970
|
let startTime = Date().timeIntervalSince1970
|
||||||
|
|
||||||
// First check if the asset already exists
|
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
let downloadsDir = docs.appendingPathComponent("downloads", isDirectory: true)
|
let downloadsDir = docs.appendingPathComponent("downloads", isDirectory: true)
|
||||||
let potentialExistingLocation = downloadsDir.appendingPathComponent(
|
let potentialExistingLocation = downloadsDir.appendingPathComponent(
|
||||||
providedId, isDirectory: true)
|
providedId, isDirectory: true)
|
||||||
|
|
||||||
if fm.fileExists(atPath: potentialExistingLocation.path) {
|
if fm.fileExists(atPath: potentialExistingLocation.path) {
|
||||||
// Check if the download is complete by looking for the master playlist
|
|
||||||
if let files = try? fm.contentsOfDirectory(atPath: potentialExistingLocation.path),
|
if let files = try? fm.contentsOfDirectory(atPath: potentialExistingLocation.path),
|
||||||
files.contains(where: { $0.hasSuffix(".m3u8") })
|
files.contains(where: { $0.hasSuffix(".m3u8") })
|
||||||
{
|
{
|
||||||
// Asset exists and appears complete, send completion event
|
|
||||||
self.sendEvent(
|
self.sendEvent(
|
||||||
"onComplete",
|
"onComplete",
|
||||||
[
|
[
|
||||||
@@ -41,7 +90,6 @@ public class HlsDownloaderModule: Module {
|
|||||||
])
|
])
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
// Asset exists but appears incomplete, clean it up
|
|
||||||
try? fm.removeItem(at: potentialExistingLocation)
|
try? fm.removeItem(at: potentialExistingLocation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +107,6 @@ public class HlsDownloaderModule: Module {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rest of the download logic remains the same
|
|
||||||
let asset = AVURLAsset(
|
let asset = AVURLAsset(
|
||||||
url: assetURL,
|
url: assetURL,
|
||||||
options: [
|
options: [
|
||||||
@@ -87,31 +134,16 @@ public class HlsDownloaderModule: Module {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let configuration = URLSessionConfiguration.background(
|
|
||||||
withIdentifier: "com.streamyfin.hlsdownload") // Add unique identifier
|
|
||||||
configuration.allowsCellularAccess = true
|
|
||||||
configuration.sessionSendsLaunchEvents = true
|
|
||||||
configuration.isDiscretionary = false
|
|
||||||
|
|
||||||
let delegate = HLSDownloadDelegate(module: self)
|
|
||||||
delegate.providedId = providedId
|
|
||||||
delegate.startTime = startTime
|
|
||||||
|
|
||||||
let downloadSession = AVAssetDownloadURLSession(
|
|
||||||
configuration: configuration,
|
|
||||||
assetDownloadDelegate: delegate,
|
|
||||||
delegateQueue: OperationQueue.main
|
|
||||||
)
|
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let task = downloadSession.makeAssetDownloadTask(
|
let task = self.downloadSession.makeAssetDownloadTask(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
assetTitle: providedId,
|
assetTitle: providedId,
|
||||||
assetArtworkData: nil,
|
assetArtworkData: nil,
|
||||||
options: [
|
options: [
|
||||||
AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 265_000,
|
AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 265_000,
|
||||||
AVAssetDownloadTaskMinimumRequiredPresentationSizeKey: NSValue(
|
AVAssetDownloadTaskMinimumRequiredPresentationSizeKey: NSValue(
|
||||||
cgSize: CGSize(width: 480, height: 360)),
|
cgSize: CGSize(width: 480, height: 360)
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
@@ -127,8 +159,13 @@ public class HlsDownloaderModule: Module {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let delegate = HLSDownloadDelegate(module: self)
|
||||||
|
delegate.providedId = providedId
|
||||||
|
delegate.startTime = startTime
|
||||||
delegate.taskIdentifier = task.taskIdentifier
|
delegate.taskIdentifier = task.taskIdentifier
|
||||||
|
|
||||||
self.activeDownloads[task.taskIdentifier] = (task, delegate, metadata ?? [:], startTime)
|
self.activeDownloads[task.taskIdentifier] = (task, delegate, metadata ?? [:], startTime)
|
||||||
|
|
||||||
self.sendEvent(
|
self.sendEvent(
|
||||||
"onProgress",
|
"onProgress",
|
||||||
[
|
[
|
||||||
@@ -185,24 +222,18 @@ public class HlsDownloaderModule: Module {
|
|||||||
try fm.createDirectory(at: downloadsDir, withIntermediateDirectories: true)
|
try fm.createDirectory(at: downloadsDir, withIntermediateDirectories: true)
|
||||||
}
|
}
|
||||||
let newLocation = downloadsDir.appendingPathComponent(folderName, isDirectory: true)
|
let newLocation = downloadsDir.appendingPathComponent(folderName, isDirectory: true)
|
||||||
|
|
||||||
// New atomic move implementation
|
|
||||||
let tempLocation = downloadsDir.appendingPathComponent("\(folderName)_temp", isDirectory: true)
|
let tempLocation = downloadsDir.appendingPathComponent("\(folderName)_temp", isDirectory: true)
|
||||||
|
|
||||||
// Clean up any existing temp folder
|
|
||||||
if fm.fileExists(atPath: tempLocation.path) {
|
if fm.fileExists(atPath: tempLocation.path) {
|
||||||
try fm.removeItem(at: tempLocation)
|
try fm.removeItem(at: tempLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to temp location first
|
|
||||||
try fm.moveItem(at: originalLocation, to: tempLocation)
|
try fm.moveItem(at: originalLocation, to: tempLocation)
|
||||||
|
|
||||||
// If target exists, remove it
|
|
||||||
if fm.fileExists(atPath: newLocation.path) {
|
if fm.fileExists(atPath: newLocation.path) {
|
||||||
try fm.removeItem(at: newLocation)
|
try fm.removeItem(at: newLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final move from temp to target
|
|
||||||
try fm.moveItem(at: tempLocation, to: newLocation)
|
try fm.moveItem(at: tempLocation, to: newLocation)
|
||||||
|
|
||||||
return newLocation
|
return newLocation
|
||||||
@@ -219,48 +250,69 @@ public class HlsDownloaderModule: Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class HlsDownloaderDelegate: NSObject, AVAssetDownloadDelegate {
|
||||||
class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
|
|
||||||
weak var module: HlsDownloaderModule?
|
weak var module: HlsDownloaderModule?
|
||||||
var taskIdentifier: Int = 0
|
var taskIdentifier: Int = 0
|
||||||
var providedId: String = ""
|
var providedId: String = ""
|
||||||
var downloadedSeconds: Double = 0
|
var downloadedSeconds: Double = 0
|
||||||
var totalSeconds: Double = 0
|
var totalSeconds: Double = 0
|
||||||
var startTime: Double = 0
|
var startTime: Double = 0
|
||||||
private var wasCancelled = false
|
|
||||||
|
|
||||||
init(module: HlsDownloaderModule) {
|
init(module: HlsDownloaderModule) {
|
||||||
self.module = module
|
self.module = module
|
||||||
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func urlSession(
|
||||||
|
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange,
|
||||||
|
totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange
|
||||||
|
) {
|
||||||
|
module?.urlSession(
|
||||||
|
session, assetDownloadTask: assetDownloadTask, didLoad: timeRange,
|
||||||
|
totalTimeRangesLoaded: loadedTimeRanges, timeRangeExpectedToLoad: timeRangeExpectedToLoad)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func urlSession(
|
||||||
|
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask,
|
||||||
|
didFinishDownloadingTo location: URL
|
||||||
|
) {
|
||||||
|
module?.urlSession(
|
||||||
|
session, assetDownloadTask: assetDownloadTask, didFinishDownloadingTo: location)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func urlSession(
|
||||||
|
_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?
|
||||||
|
) {
|
||||||
|
module?.urlSession(session, task: task, didCompleteWithError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HlsDownloaderModule {
|
||||||
func urlSession(
|
func urlSession(
|
||||||
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange,
|
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange,
|
||||||
totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange
|
totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange
|
||||||
) {
|
) {
|
||||||
|
guard let downloadInfo = activeDownloads[assetDownloadTask.taskIdentifier] else { return }
|
||||||
|
|
||||||
let downloaded = loadedTimeRanges.reduce(0.0) { total, value in
|
let downloaded = loadedTimeRanges.reduce(0.0) { total, value in
|
||||||
let timeRange = value.timeRangeValue
|
let timeRange = value.timeRangeValue
|
||||||
return total + CMTimeGetSeconds(timeRange.duration)
|
return total + CMTimeGetSeconds(timeRange.duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
let total = CMTimeGetSeconds(timeRangeExpectedToLoad.duration)
|
let total = CMTimeGetSeconds(timeRangeExpectedToLoad.duration)
|
||||||
let metadata = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.metadata ?? [:]
|
|
||||||
let startTime = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.startTime ?? 0
|
|
||||||
|
|
||||||
self.downloadedSeconds = downloaded
|
|
||||||
self.totalSeconds = total
|
|
||||||
|
|
||||||
let progress = total > 0 ? downloaded / total : 0
|
let progress = total > 0 ? downloaded / total : 0
|
||||||
|
|
||||||
module?.sendEvent(
|
sendEvent(
|
||||||
"onProgress",
|
"onProgress",
|
||||||
[
|
[
|
||||||
"id": providedId,
|
"id": downloadInfo.delegate.providedId,
|
||||||
"progress": progress,
|
"progress": progress,
|
||||||
"secondsDownloaded": downloaded,
|
"secondsDownloaded": downloaded,
|
||||||
"secondsTotal": total,
|
"secondsTotal": total,
|
||||||
"state": progress >= 1.0 ? "DONE" : "DOWNLOADING",
|
"state": progress >= 1.0 ? "DONE" : "DOWNLOADING",
|
||||||
"metadata": metadata,
|
"metadata": downloadInfo.metadata,
|
||||||
"startTime": startTime,
|
"startTime": downloadInfo.startTime,
|
||||||
|
"taskId": assetDownloadTask.taskIdentifier,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,110 +320,82 @@ class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
|
|||||||
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask,
|
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask,
|
||||||
didFinishDownloadingTo location: URL
|
didFinishDownloadingTo location: URL
|
||||||
) {
|
) {
|
||||||
if wasCancelled {
|
guard let downloadInfo = activeDownloads[assetDownloadTask.taskIdentifier] else { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.metadata ?? [:]
|
|
||||||
let startTime = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.startTime ?? 0
|
|
||||||
let folderName = providedId
|
|
||||||
|
|
||||||
guard let module = module else { return }
|
|
||||||
|
|
||||||
// Calculate download size
|
|
||||||
// let fileManager = FileManager.default
|
|
||||||
// let enumerator = fileManager.enumerator(
|
|
||||||
// at: newLocation,
|
|
||||||
// includingPropertiesForKeys: [.totalFileAllocatedSizeKey],
|
|
||||||
// options: [.skipsHiddenFiles],
|
|
||||||
// errorHandler: nil)!
|
|
||||||
|
|
||||||
// var totalSize: Int64 = 0
|
|
||||||
// while let filePath = enumerator.nextObject() as? URL {
|
|
||||||
// do {
|
|
||||||
// let resourceValues = try filePath.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
|
||||||
// if let size = resourceValues.totalFileAllocatedSize {
|
|
||||||
// totalSize += Int64(size)
|
|
||||||
// }
|
|
||||||
// } catch {
|
|
||||||
// print("Error calculating size: \(error)")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let newLocation = try module.persistDownloadedFolder(
|
let newLocation = try persistDownloadedFolder(
|
||||||
originalLocation: location, folderName: folderName)
|
originalLocation: location, folderName: downloadInfo.delegate.providedId)
|
||||||
|
|
||||||
// Handle metadata first
|
if !downloadInfo.metadata.isEmpty {
|
||||||
if !metadata.isEmpty {
|
|
||||||
let metadataLocation = newLocation.deletingLastPathComponent().appendingPathComponent(
|
let metadataLocation = newLocation.deletingLastPathComponent().appendingPathComponent(
|
||||||
"\(providedId).json")
|
"\(downloadInfo.delegate.providedId).json")
|
||||||
let jsonData = try JSONSerialization.data(
|
let jsonData = try JSONSerialization.data(
|
||||||
withJSONObject: metadata, options: .prettyPrinted)
|
withJSONObject: downloadInfo.metadata, options: .prettyPrinted)
|
||||||
try jsonData.write(to: metadataLocation)
|
try jsonData.write(to: metadataLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new Task for async operation
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await rewriteM3U8Files(baseDir: newLocation.path)
|
try await rewriteM3U8Files(baseDir: newLocation.path)
|
||||||
|
|
||||||
module.sendEvent(
|
sendEvent(
|
||||||
"onComplete",
|
"onComplete",
|
||||||
[
|
[
|
||||||
"id": providedId,
|
"id": downloadInfo.delegate.providedId,
|
||||||
"location": newLocation.absoluteString,
|
"location": newLocation.absoluteString,
|
||||||
"state": "DONE",
|
"state": "DONE",
|
||||||
"metadata": metadata,
|
"metadata": downloadInfo.metadata,
|
||||||
"startTime": startTime,
|
"startTime": downloadInfo.startTime,
|
||||||
])
|
])
|
||||||
} catch {
|
} catch {
|
||||||
module.sendEvent(
|
sendEvent(
|
||||||
"onError",
|
"onError",
|
||||||
[
|
[
|
||||||
"id": providedId,
|
"id": downloadInfo.delegate.providedId,
|
||||||
"error": error.localizedDescription,
|
"error": error.localizedDescription,
|
||||||
"state": "FAILED",
|
"state": "FAILED",
|
||||||
"metadata": metadata,
|
"metadata": downloadInfo.metadata,
|
||||||
"startTime": startTime,
|
"startTime": downloadInfo.startTime,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
module.sendEvent(
|
sendEvent(
|
||||||
"onError",
|
"onError",
|
||||||
[
|
[
|
||||||
"id": providedId,
|
"id": downloadInfo.delegate.providedId,
|
||||||
"error": error.localizedDescription,
|
"error": error.localizedDescription,
|
||||||
"state": "FAILED",
|
"state": "FAILED",
|
||||||
"metadata": metadata,
|
"metadata": downloadInfo.metadata,
|
||||||
"startTime": startTime,
|
"startTime": downloadInfo.startTime,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
module.removeDownload(with: assetDownloadTask.taskIdentifier)
|
removeDownload(with: assetDownloadTask.taskIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
func urlSession(
|
||||||
if let error = error {
|
_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?
|
||||||
if (error as NSError).code == NSURLErrorCancelled {
|
) {
|
||||||
wasCancelled = true
|
guard let error = error,
|
||||||
module?.removeDownload(with: taskIdentifier)
|
let downloadInfo = activeDownloads[task.taskIdentifier]
|
||||||
return
|
else { return }
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = module?.activeDownloads[task.taskIdentifier]?.metadata ?? [:]
|
if (error as NSError).code == NSURLErrorCancelled {
|
||||||
let startTime = module?.activeDownloads[task.taskIdentifier]?.startTime ?? 0
|
removeDownload(with: task.taskIdentifier)
|
||||||
module?.sendEvent(
|
return
|
||||||
"onError",
|
|
||||||
[
|
|
||||||
"id": providedId,
|
|
||||||
"error": error.localizedDescription,
|
|
||||||
"state": "FAILED",
|
|
||||||
"metadata": metadata,
|
|
||||||
"startTime": startTime,
|
|
||||||
])
|
|
||||||
module?.removeDownload(with: taskIdentifier)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendEvent(
|
||||||
|
"onError",
|
||||||
|
[
|
||||||
|
"id": downloadInfo.delegate.providedId,
|
||||||
|
"error": error.localizedDescription,
|
||||||
|
"state": "FAILED",
|
||||||
|
"metadata": downloadInfo.metadata,
|
||||||
|
"startTime": downloadInfo.startTime,
|
||||||
|
])
|
||||||
|
|
||||||
|
removeDownload(with: task.taskIdentifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type OnProgressEventPayload = BaseEventPayload & {
|
|||||||
progress: number;
|
progress: number;
|
||||||
secondsDownloaded: number;
|
secondsDownloaded: number;
|
||||||
secondsTotal: number;
|
secondsTotal: number;
|
||||||
|
taskId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OnErrorEventPayload = BaseEventPayload & {
|
export type OnErrorEventPayload = BaseEventPayload & {
|
||||||
|
|||||||
@@ -151,7 +151,8 @@ export const NativeDownloadProvider: React.FC<{
|
|||||||
"[HLS] Download progress:",
|
"[HLS] Download progress:",
|
||||||
download.metadata.item.Id,
|
download.metadata.item.Id,
|
||||||
download.progress,
|
download.progress,
|
||||||
download.state
|
download.state,
|
||||||
|
download.taskId
|
||||||
);
|
);
|
||||||
|
|
||||||
setDownloads((prev) => ({
|
setDownloads((prev) => ({
|
||||||
@@ -166,26 +167,15 @@ export const NativeDownloadProvider: React.FC<{
|
|||||||
startTime: download?.startTime,
|
startTime: download?.startTime,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
const completeListener = addCompleteListener(async (payload) => {
|
if (download.state === "DONE") {
|
||||||
try {
|
refetchDownloadedFiles();
|
||||||
// await rewriteM3U8Files(payload.location);
|
|
||||||
// await markFileAsDone(payload.id);
|
|
||||||
console.log("completeListener", payload.id);
|
|
||||||
|
|
||||||
setDownloads((prev) => {
|
setDownloads((prev) => {
|
||||||
const newDownloads = { ...prev };
|
const newDownloads = { ...prev };
|
||||||
delete newDownloads[payload.id];
|
delete newDownloads[download.id];
|
||||||
return newDownloads;
|
return newDownloads;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (payload.state === "DONE") toast.success("Download complete ✅");
|
|
||||||
|
|
||||||
refetchDownloadedFiles();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to download file:", error);
|
|
||||||
toast.error("Failed to download ❌");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,15 +187,16 @@ export const NativeDownloadProvider: React.FC<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error.state === "CANCELLED") toast.info("Download cancelled 🟡");
|
if (error.state === "CANCELLED") toast.info("Download cancelled 🟡");
|
||||||
else {
|
else if (error.state === "FAILED") {
|
||||||
toast.error("Download failed ❌");
|
toast.error("Download failed ❌");
|
||||||
console.error("Download error:", error);
|
console.error("Download error:", error);
|
||||||
|
} else {
|
||||||
|
console.error("errorListener fired with unknown state:", error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
progressListener.remove();
|
progressListener.remove();
|
||||||
completeListener.remove();
|
|
||||||
errorListener.remove();
|
errorListener.remove();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
Reference in New Issue
Block a user