From e9961f64a9844552fc6bc901eb3407dbf4529ed8 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 8 Aug 2022 19:25:59 -0400 Subject: [PATCH] Handle a documents directory that can change Thanks iOS --- ios/App/App/plugins/AbsDownloader.swift | 41 ++++--- ios/App/Shared/models/DownloadItem.swift | 34 +++--- ios/App/Shared/models/LocalLibrary.swift | 101 +++++++++++++++++- .../models/LocalLibraryExtensions.swift | 14 ++- 4 files changed, 137 insertions(+), 53 deletions(-) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index 06af972e..11248ad8 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -11,9 +11,10 @@ import Capacitor @objc(AbsDownloader) public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { + static let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + typealias DownloadProgressHandler = (_ downloadItem: DownloadItem, _ downloadItemPart: inout DownloadItemPart) throws -> Void - private let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] private lazy var session: URLSession = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 5 @@ -30,7 +31,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { do { // Move the downloaded file into place - guard let destinationUrl = downloadItemPart.destinationURL() else { + guard let destinationUrl = downloadItemPart.destinationURL else { throw LibraryItemDownloadError.downloadItemPartDestinationUrlNotDefined } try? FileManager.default.removeItem(at: destinationUrl) @@ -142,31 +143,25 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } private func handleDownloadTaskCompleteFromDownloadItem(_ downloadItem: DownloadItem) { + var statusNotification = [String: Any]() + statusNotification["libraryItemId"] = downloadItem.libraryItemId + if ( downloadItem.didDownloadSuccessfully() ) { ApiClient.getLibraryItemWithProgress(libraryItemId: downloadItem.libraryItemId!, episodeId: downloadItem.episodeId) { libraryItem in - var statusNotification = [String: Any]() - statusNotification["libraryItemId"] = libraryItem?.id - guard let libraryItem = libraryItem else { NSLog("LibraryItem not found"); return } - let localDirectory = self.documentsDirectory.appendingPathComponent("\(libraryItem.id)") - var coverFile: URL? + let localDirectory = libraryItem.id + var coverFile: String? // Assemble the local library item let files = downloadItem.downloadItemParts.compactMap { part -> LocalFile? in if part.filename == "cover.jpg" { - coverFile = part.destinationURL() + coverFile = part.destinationUri return nil } else { - return LocalFile(libraryItem.id, part.filename!, part.mimeType()!, part.destinationURL()!) + return LocalFile(libraryItem.id, part.filename!, part.mimeType()!, part.destinationUri!, fileSize: Int(part.destinationURL!.fileSize)) } } - var localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files) - - // Store the cover file - if let coverFile = coverFile { - localLibraryItem.coverContentUrl = coverFile.absoluteString - localLibraryItem.coverAbsolutePath = coverFile.path - } + let localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files, coverPath: coverFile) Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem) statusNotification["localLibraryItem"] = try? localLibraryItem.asDictionary() @@ -180,6 +175,8 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { self.notifyListeners("onItemDownloadComplete", data: statusNotification) } + } else { + self.notifyListeners("onItemDownloadComplete", data: statusNotification) } } @@ -270,7 +267,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { let serverUrl = urlForTrack(item: item, track: track) let itemDirectory = try createLibraryItemFileDirectory(item: item) - let localUrl = itemDirectory.appendingPathComponent("\(filename)") + let localUrl = "\(itemDirectory)/\(filename)" let task = session.downloadTask(with: serverUrl) var downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: nil) @@ -286,10 +283,10 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { let filename = "cover.jpg" let serverPath = "/api/items/\(item.id)/cover" let itemDirectory = try createLibraryItemFileDirectory(item: item) - let localUrl = itemDirectory.appendingPathComponent("\(filename)") + let localUrl = "\(itemDirectory)/\(filename)" var downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil) - let task = session.downloadTask(with: downloadItemPart.downloadURL()!) + let task = session.downloadTask(with: downloadItemPart.downloadURL!) // Store the id on the task so the download item can be pulled from the database later task.taskDescription = downloadItemPart.id @@ -305,12 +302,12 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { return URL(string: urlstr)! } - private func createLibraryItemFileDirectory(item: LibraryItem) throws -> URL { - let itemDirectory = documentsDirectory.appendingPathComponent("\(item.id)") + private func createLibraryItemFileDirectory(item: LibraryItem) throws -> String { + let itemDirectory = item.id NSLog("ITEM DIR \(itemDirectory)") do { - try FileManager.default.createDirectory(at: itemDirectory, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: AbsDownloader.downloadsDirectory.appendingPathComponent(itemDirectory), withIntermediateDirectories: true) } catch { NSLog("Failed to CREATE LI DIRECTORY \(error)") throw LibraryItemDownloadError.failedDirectory diff --git a/ios/App/Shared/models/DownloadItem.swift b/ios/App/Shared/models/DownloadItem.swift index 5fa8e33f..a9ce6c6e 100644 --- a/ios/App/Shared/models/DownloadItem.swift +++ b/ios/App/Shared/models/DownloadItem.swift @@ -72,7 +72,21 @@ struct DownloadItemPart: Realmable, Codable { var moved: Bool = false var failed: Bool = false var uri: String? + var downloadURL: URL? { + if let uri = self.uri { + return URL(string: uri) + } else { + return nil + } + } var destinationUri: String? + var destinationURL: URL? { + if let destinationUri = self.destinationUri { + return AbsDownloader.downloadsDirectory.appendingPathComponent(destinationUri) + } else { + return nil + } + } var progress: Double = 0 var task: URLSessionDownloadTask! @@ -90,7 +104,7 @@ struct DownloadItemPart: Realmable, Codable { } extension DownloadItemPart { - init(filename: String, destination: URL, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) { + init(filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) { self.filename = filename self.itemTitle = itemTitle self.serverPath = serverPath @@ -103,26 +117,10 @@ extension DownloadItemPart { downloadUrl += "&format=jpeg" // For cover images force to jpeg } self.uri = downloadUrl - self.destinationUri = destination.path + self.destinationUri = destination } func mimeType() -> String? { audioTrack?.mimeType ?? episode?.audioTrack?.mimeType } - - func downloadURL() -> URL? { - if let uri = self.uri { - return URL(string: uri) - } else { - return nil - } - } - - func destinationURL() -> URL? { - if let destinationUri = self.destinationUri { - return URL(fileURLWithPath: destinationUri) - } else { - return nil - } - } } diff --git a/ios/App/Shared/models/LocalLibrary.swift b/ios/App/Shared/models/LocalLibrary.swift index 5f36ff99..c2e5841e 100644 --- a/ios/App/Shared/models/LocalLibrary.swift +++ b/ios/App/Shared/models/LocalLibrary.swift @@ -11,23 +11,87 @@ import Unrealm struct LocalLibraryItem: Realmable, Codable { var id: String = "local_\(UUID().uuidString)" var basePath: String = "" - var absolutePath: String = "" - var contentUrl: String = "" + dynamic var _contentUrl: String? var isInvalid: Bool = false var mediaType: String = "" var media: MediaType? var localFiles: [LocalFile] = [] - var coverContentUrl: String? - var coverAbsolutePath: String? + dynamic var _coverContentUrl: String? var isLocal: Bool = true var serverConnectionConfigId: String? var serverAddress: String? var serverUserId: String? var libraryItemId: String? + var contentUrl: String? { + set(url) { + _contentUrl = url + } + get { + if let path = _contentUrl { + return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString + } else { + return nil + } + } + } + + var coverContentUrl: String? { + set(url) { + _coverContentUrl = url + } + get { + if let path = self._coverContentUrl { + return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString + } else { + return nil + } + } + } + static func primaryKey() -> String? { return "id" } + + private enum CodingKeys : String, CodingKey { + case id, basePath, contentUrl, isInvalid, mediaType, media, localFiles, coverContentUrl, isLocal, serverConnectionConfigId, serverAddress, serverUserId, libraryItemId + } + + init() {} + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(String.self, forKey: .id) + basePath = try values.decode(String.self, forKey: .basePath) + contentUrl = try values.decode(String.self, forKey: .contentUrl) + isInvalid = try values.decode(Bool.self, forKey: .isInvalid) + mediaType = try values.decode(String.self, forKey: .mediaType) + media = try values.decode(MediaType.self, forKey: .media) + localFiles = try values.decode([LocalFile].self, forKey: .localFiles) + coverContentUrl = try values.decode(String.self, forKey: .coverContentUrl) + isLocal = try values.decode(Bool.self, forKey: .isLocal) + serverConnectionConfigId = try values.decode(String.self, forKey: .serverConnectionConfigId) + serverAddress = try values.decode(String.self, forKey: .serverAddress) + serverUserId = try values.decode(String.self, forKey: .serverUserId) + libraryItemId = try values.decode(String.self, forKey: .libraryItemId) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(basePath, forKey: .basePath) + try container.encode(contentUrl, forKey: .contentUrl) + try container.encode(isInvalid, forKey: .isInvalid) + try container.encode(mediaType, forKey: .mediaType) + try container.encode(media, forKey: .media) + try container.encode(localFiles, forKey: .localFiles) + try container.encode(coverContentUrl, forKey: .coverContentUrl) + try container.encode(isLocal, forKey: .isLocal) + try container.encode(serverConnectionConfigId, forKey: .serverConnectionConfigId) + try container.encode(serverAddress, forKey: .serverAddress) + try container.encode(serverUserId, forKey: .serverUserId) + try container.encode(libraryItemId, forKey: .libraryItemId) + } } struct LocalPodcastEpisode: Realmable, Codable { @@ -53,13 +117,40 @@ struct LocalFile: Realmable, Codable { var id: String = UUID().uuidString var filename: String? var contentUrl: String = "" - var absolutePath: String = "" + var absolutePath: String { + return AbsDownloader.downloadsDirectory.appendingPathComponent(self.contentUrl).absoluteString + } var mimeType: String? var size: Int = 0 static func primaryKey() -> String? { return "id" } + + private enum CodingKeys : String, CodingKey { + case id, filename, contentUrl, absolutePath, mimeType, size + } + + init() {} + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(String.self, forKey: .id) + filename = try values.decode(String.self, forKey: .filename) + contentUrl = try values.decode(String.self, forKey: .contentUrl) + mimeType = try values.decode(String.self, forKey: .mimeType) + size = try values.decode(Int.self, forKey: .size) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(filename, forKey: .filename) + try container.encode(contentUrl, forKey: .contentUrl) + try container.encode(absolutePath, forKey: .absolutePath) + try container.encode(mimeType, forKey: .mimeType) + try container.encode(size, forKey: .size) + } } struct LocalMediaProgress: Realmable, Codable { diff --git a/ios/App/Shared/models/LocalLibraryExtensions.swift b/ios/App/Shared/models/LocalLibraryExtensions.swift index 93ca000b..1111498e 100644 --- a/ios/App/Shared/models/LocalLibraryExtensions.swift +++ b/ios/App/Shared/models/LocalLibraryExtensions.swift @@ -8,14 +8,13 @@ import Foundation extension LocalLibraryItem { - init(_ item: LibraryItem, localUrl: URL, server: ServerConnectionConfig, files: [LocalFile]) { + init(_ item: LibraryItem, localUrl: String, server: ServerConnectionConfig, files: [LocalFile], coverPath: String?) { self.init() - self.contentUrl = localUrl.absoluteString + self.contentUrl = localUrl self.mediaType = item.mediaType self.media = item.media self.localFiles = files - // TODO: self.coverContentURL - // TODO: self.converAbsolutePath + self.coverContentUrl = coverPath self.libraryItemId = item.id self.serverConnectionConfigId = server.id self.serverAddress = server.address @@ -71,14 +70,13 @@ extension LocalLibraryItem { } extension LocalFile { - init(_ libraryItemId: String, _ filename: String, _ mimeType: String, _ localUrl: URL) { + init(_ libraryItemId: String, _ filename: String, _ mimeType: String, _ localUrl: String, fileSize: Int) { self.init() self.id = "\(libraryItemId)_\(filename.toBase64())" self.filename = filename self.mimeType = mimeType - self.contentUrl = localUrl.absoluteString - self.absolutePath = localUrl.path - self.size = Int(localUrl.fileSize) + self.contentUrl = localUrl + self.size = fileSize } func isAudioFile() -> Bool {