diff --git a/components/widgets/DownloadProgressIndicator.vue b/components/widgets/DownloadProgressIndicator.vue index 6c57ed74..357aee7d 100644 --- a/components/widgets/DownloadProgressIndicator.vue +++ b/components/widgets/DownloadProgressIndicator.vue @@ -44,7 +44,6 @@ export default { }, methods: { clickedIt() { - if (this.isIos) return // TODO: Implement on iOS this.$router.push('/downloading') }, onItemDownloadComplete(data) { diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index d286f505..7f822415 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. let configuration = Realm.Configuration( - schemaVersion: 6, + schemaVersion: 7, migrationBlock: { [weak self] migration, oldSchemaVersion in if (oldSchemaVersion < 1) { self?.logger.log("Realm schema version was \(oldSchemaVersion)") diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index d9dee76d..b208f569 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -33,6 +33,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in let realm = try Realm() try realm.write { + downloadItemPart.bytesDownloaded = downloadItemPart.fileSize downloadItemPart.progress = 100 downloadItemPart.completed = true } @@ -72,9 +73,11 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in // Calculate the download percentage let percentDownloaded = (Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100 + // Only update the progress if we received accurate progress data if percentDownloaded >= 0.0 && percentDownloaded <= 100.0 { try Realm().write { + downloadItemPart.bytesDownloaded = Double(totalBytesWritten) downloadItemPart.progress = percentDownloaded } } @@ -109,6 +112,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { // Call the progress handler do { try progressHandler(downloadItem, part) + try? self.notifyListeners("onDownloadItemPartUpdate", data: part.asDictionary()) } catch { logger.error("Error while processing progress") debugPrint(error) @@ -158,10 +162,9 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } } - // Emit status for active downloads + // Check for items done downloading if let activeDownloads = fetchActiveDownloads() { for item in activeDownloads.values { - try? self.notifyListeners("onItemDownloadUpdate", data: item.asDictionary()) if item.isDoneDownloading() { handleDoneDownloadItem(item) } } } @@ -277,19 +280,22 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { let downloadItem = DownloadItem(libraryItem: item, episodeId: episodeId, server: Store.serverConfig!) var tasks = [DownloadItemPartTask]() for (i, track) in tracks.enumerated() { - let task = try startLibraryItemTrackDownload(item: item, position: i, track: track, episode: episode) + let task = try startLibraryItemTrackDownload(downloadItemId: downloadItem.id!, item: item, position: i, track: track, episode: episode) downloadItem.downloadItemParts.append(task.part) tasks.append(task) } // Also download the cover if item.media?.coverPath != nil && !(item.media?.coverPath!.isEmpty ?? true) { - if let task = try? startLibraryItemCoverDownload(item: item) { + if let task = try? startLibraryItemCoverDownload(downloadItemId: downloadItem.id!, item: item) { downloadItem.downloadItemParts.append(task.part) tasks.append(task) } } + // Notify client of download item + try? self.notifyListeners("onDownloadItem", data: downloadItem.asDictionary()) + // Persist in the database before status start coming in try Database.shared.saveDownloadItem(downloadItem) @@ -299,7 +305,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } } - private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask { + private func startLibraryItemTrackDownload(downloadItemId: String, item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask { logger.log("TRACK \(track.contentUrl!)") // If we don't name metadata, then we can't proceed @@ -312,7 +318,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { let localUrl = "\(itemDirectory)/\(filename)" let task = session.downloadTask(with: serverUrl) - let part = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode) + let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode, size: track.metadata?.size ?? 0) // Store the id on the task so the download item can be pulled from the database later task.taskDescription = part.id @@ -320,13 +326,18 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { return DownloadItemPartTask(part: part, task: task) } - private func startLibraryItemCoverDownload(item: LibraryItem) throws -> DownloadItemPartTask { + private func startLibraryItemCoverDownload(downloadItemId: String, item: LibraryItem) throws -> DownloadItemPartTask { let filename = "cover.jpg" let serverPath = "/api/items/\(item.id)/cover" let itemDirectory = try createLibraryItemFileDirectory(item: item) let localUrl = "\(itemDirectory)/\(filename)" - let part = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil) + // Find library file to get cover size + let coverLibraryFile = item.libraryFiles.first(where: { + $0.metadata?.path == item.media?.coverPath + }) + + let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil, size: coverLibraryFile?.metadata?.size ?? 0) let task = session.downloadTask(with: part.downloadURL!) // Store the id on the task so the download item can be pulled from the database later diff --git a/ios/App/Shared/models/download/DownloadItem.swift b/ios/App/Shared/models/download/DownloadItem.swift index ee30213a..88411be3 100644 --- a/ios/App/Shared/models/download/DownloadItem.swift +++ b/ios/App/Shared/models/download/DownloadItem.swift @@ -9,7 +9,7 @@ import Foundation import RealmSwift class DownloadItem: Object, Codable { - @Persisted(primaryKey: true) var id: String? + @Persisted(primaryKey: true) var id:String? @Persisted(indexed: true) var libraryItemId: String? @Persisted var episodeId: String? @Persisted var userMediaProgress: MediaProgress? diff --git a/ios/App/Shared/models/download/DownloadItemPart.swift b/ios/App/Shared/models/download/DownloadItemPart.swift index 4b9acc68..eafda2eb 100644 --- a/ios/App/Shared/models/download/DownloadItemPart.swift +++ b/ios/App/Shared/models/download/DownloadItemPart.swift @@ -10,7 +10,9 @@ import RealmSwift class DownloadItemPart: Object, Codable { @Persisted(primaryKey: true) var id = "" + @Persisted var downloadItemId: String? @Persisted var filename: String? + @Persisted var fileSize: Double = 0 @Persisted var itemTitle: String? @Persisted var serverPath: String? @Persisted var audioTrack: AudioTrack? @@ -21,9 +23,10 @@ class DownloadItemPart: Object, Codable { @Persisted var uri: String? @Persisted var destinationUri: String? @Persisted var progress: Double = 0 + @Persisted var bytesDownloaded: Double = 0 private enum CodingKeys : String, CodingKey { - case id, filename, itemTitle, completed, moved, failed, progress + case id, downloadItemId, filename, fileSize, itemTitle, completed, moved, failed, progress, bytesDownloaded } override init() { @@ -33,32 +36,40 @@ class DownloadItemPart: Object, Codable { required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) id = try values.decode(String.self, forKey: .id) + downloadItemId = try? values.decode(String.self, forKey: .downloadItemId) filename = try? values.decode(String.self, forKey: .filename) + fileSize = try values.decode(Double.self, forKey: .fileSize) itemTitle = try? values.decode(String.self, forKey: .itemTitle) completed = try values.decode(Bool.self, forKey: .completed) moved = try values.decode(Bool.self, forKey: .moved) failed = try values.decode(Bool.self, forKey: .failed) progress = try values.decode(Double.self, forKey: .progress) + bytesDownloaded = try values.decode(Double.self, forKey: .bytesDownloaded) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) + try container.encode(downloadItemId, forKey: .downloadItemId) try container.encode(filename, forKey: .filename) + try container.encode(fileSize, forKey: .fileSize) try container.encode(itemTitle, forKey: .itemTitle) try container.encode(completed, forKey: .completed) try container.encode(moved, forKey: .moved) try container.encode(failed, forKey: .failed) try container.encode(progress, forKey: .progress) + try container.encode(bytesDownloaded, forKey: .bytesDownloaded) } } extension DownloadItemPart { - convenience init(filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) { + convenience init(downloadItemId: String, filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?, size: Double) { self.init() self.id = destination.toBase64() + self.downloadItemId = downloadItemId self.filename = filename + self.fileSize = size self.itemTitle = itemTitle self.serverPath = serverPath self.audioTrack = AudioTrack.detachCopy(of: audioTrack) diff --git a/ios/App/Shared/models/server/FileMetadata.swift b/ios/App/Shared/models/server/FileMetadata.swift index a3c62170..6108112a 100644 --- a/ios/App/Shared/models/server/FileMetadata.swift +++ b/ios/App/Shared/models/server/FileMetadata.swift @@ -13,9 +13,10 @@ class FileMetadata: EmbeddedObject, Codable { @Persisted var ext: String = "" @Persisted var path: String = "" @Persisted var relPath: String = "" + @Persisted var size:Double = 0 private enum CodingKeys : String, CodingKey { - case filename, ext, path, relPath + case filename, ext, path, relPath, size } override init() { @@ -29,6 +30,7 @@ class FileMetadata: EmbeddedObject, Codable { ext = try values.decode(String.self, forKey: .ext) path = try values.decode(String.self, forKey: .path) relPath = try values.decode(String.self, forKey: .relPath) + size = try values.decode(Double.self, forKey: .size) } func encode(to encoder: Encoder) throws { @@ -37,5 +39,6 @@ class FileMetadata: EmbeddedObject, Codable { try container.encode(ext, forKey: .ext) try container.encode(path, forKey: .path) try container.encode(relPath, forKey: .relPath) + try container.encode(size, forKey: .size) } } diff --git a/store/globals.js b/store/globals.js index de42cc99..a4a26b28 100644 --- a/store/globals.js +++ b/store/globals.js @@ -130,8 +130,8 @@ export const mutations = { downloadItem.downloadItemParts = downloadItem.downloadItemParts.map(dip => { let newDip = dip.id == downloadItemPart.id ? downloadItemPart : dip - totalBytes += newDip.fileSize - totalBytesDownloaded += newDip.bytesDownloaded + totalBytes += Number(newDip.fileSize) + totalBytesDownloaded += Number(newDip.bytesDownloaded) return newDip })