From 4eeb7f24d866bb5c2bd67d84c11759ee7a154a81 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 17 Feb 2023 13:07:35 -0600 Subject: [PATCH 1/8] Fix:iOS downloads #537 --- .../widgets/DownloadProgressIndicator.vue | 1 - ios/App/App/AppDelegate.swift | 2 +- ios/App/App/plugins/AbsDownloader.swift | 27 +++++++++++++------ .../Shared/models/download/DownloadItem.swift | 2 +- .../models/download/DownloadItemPart.swift | 15 +++++++++-- .../Shared/models/server/FileMetadata.swift | 5 +++- store/globals.js | 4 +-- 7 files changed, 40 insertions(+), 16 deletions(-) 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 }) From 05d7d0d0d8409cdd3ad052fa127600983c651408 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 17 Feb 2023 15:49:53 -0600 Subject: [PATCH 2/8] Fix:Android re-scan folder removing/unlinking downloads --- .../app/device/FolderScanner.kt | 21 +++++++++++-------- store/globals.js | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt index 7cdb49a0..fc6998fa 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -51,7 +51,7 @@ class FolderScanner(var ctx: Context) { var mediaItemsUpToDate = 0 // Search for files in media item folder - val foldersFound = df.search(false, DocumentFileType.FOLDER) + val foldersFound = df.search(true, DocumentFileType.FOLDER) // Match folders found with local library items already saved in db var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) @@ -72,11 +72,16 @@ class FolderScanner(var ctx: Context) { Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}") val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) } - when (scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)) { - ItemScanResult.REMOVED -> mediaItemsRemoved++ - ItemScanResult.UPDATED -> mediaItemsUpdated++ - ItemScanResult.ADDED -> mediaItemsAdded++ - else -> mediaItemsUpToDate++ + val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream")) + + // Do not scan folders that have no media items and not an existing item already + if (existingItem != null || filesInFolder.isNotEmpty()) { + when (scanLibraryItemFolder(itemFolder, filesInFolder, localFolder, existingItem, forceAudioProbe)) { + ItemScanResult.REMOVED -> mediaItemsRemoved++ + ItemScanResult.UPDATED -> mediaItemsUpdated++ + ItemScanResult.ADDED -> mediaItemsAdded++ + else -> mediaItemsUpToDate++ + } } } @@ -91,7 +96,7 @@ class FolderScanner(var ctx: Context) { } } - private fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult { + private fun scanLibraryItemFolder(itemFolder:DocumentFile, filesInFolder:List, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult { val itemFolderName = itemFolder.name ?: "" val itemId = getLocalLibraryItemId(itemFolder.id) @@ -106,8 +111,6 @@ class FolderScanner(var ctx: Context) { var coverContentUrl:String? = null var coverAbsolutePath:String? = null - val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream")) - val existingLocalFilesRemoved = existingLocalFiles.filter { elf -> filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder } diff --git a/store/globals.js b/store/globals.js index a4a26b28..a436ef25 100644 --- a/store/globals.js +++ b/store/globals.js @@ -130,7 +130,7 @@ export const mutations = { downloadItem.downloadItemParts = downloadItem.downloadItemParts.map(dip => { let newDip = dip.id == downloadItemPart.id ? downloadItemPart : dip - totalBytes += Number(newDip.fileSize) + totalBytes += newDip.completed ? Number(newDip.bytesDownloaded) : Number(newDip.fileSize) totalBytesDownloaded += Number(newDip.bytesDownloaded) return newDip From 47ccc547491e4787cdd45ba4849f9a8aacd3029f Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 17 Feb 2023 16:07:37 -0600 Subject: [PATCH 3/8] Fix:Reorder local audio files overlapping save button --- pages/localMedia/item/_id.vue | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pages/localMedia/item/_id.vue b/pages/localMedia/item/_id.vue index 9b4b9a5f..bd2313e8 100644 --- a/pages/localMedia/item/_id.vue +++ b/pages/localMedia/item/_id.vue @@ -1,6 +1,6 @@