From 446e54cb91bb6e8f46a34e9388ae7838ffc98b38 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Wed, 10 Aug 2022 22:17:12 -0400 Subject: [PATCH 01/27] Fix data model issues --- ios/App/App/AppDelegate.swift | 1 - ios/App/App/plugins/AbsDownloader.swift | 8 +- ios/App/Shared/models/DataClasses.swift | 10 ++- ios/App/Shared/models/DownloadItem.swift | 10 ++- ios/App/Shared/models/LocalLibrary.swift | 74 ++++++------------- .../models/LocalLibraryExtensions.swift | 31 ++++++-- 6 files changed, 65 insertions(+), 69 deletions(-) diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index e3225579..ee0f4cbe 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -45,7 +45,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Local library Realm.registerRealmables(LocalLibraryItem.self) - Realm.registerRealmables(LocalPodcastEpisode.self) Realm.registerRealmables(LocalFile.self) Realm.registerRealmables(LocalMediaProgress.self) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index a6d5e6a7..96a6d1cf 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -154,10 +154,8 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { let files = downloadItem.downloadItemParts.compactMap { part -> LocalFile? in if part.filename == "cover.jpg" { coverFile = part.destinationUri - return nil - } else { - return LocalFile(libraryItem.id, part.filename!, part.mimeType()!, part.destinationUri!, fileSize: Int(part.destinationURL!.fileSize)) } + return LocalFile(libraryItem.id, part.filename!, part.mimeType()!, part.destinationUri!, fileSize: Int(part.destinationURL!.fileSize)) } let localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files, coverPath: coverFile) @@ -165,8 +163,8 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { statusNotification["localLibraryItem"] = try? localLibraryItem.asDictionary() if let progress = libraryItem.userMediaProgress { - // TODO: Handle podcast - let localMediaProgress = LocalMediaProgress(localLibraryItem: localLibraryItem, episode: nil, progress: progress) + let episode = downloadItem.media?.episodes?.first(where: { $0.id == downloadItem.episodeId }) + let localMediaProgress = LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode, progress: progress) Database.shared.saveLocalMediaProgress(localMediaProgress) statusNotification["localMediaProgress"] = try? localMediaProgress.asDictionary() } diff --git a/ios/App/Shared/models/DataClasses.swift b/ios/App/Shared/models/DataClasses.swift index 426d6522..f61ca385 100644 --- a/ios/App/Shared/models/DataClasses.swift +++ b/ios/App/Shared/models/DataClasses.swift @@ -199,8 +199,7 @@ struct AudioTrack: Realmable, Codable { var contentUrl: String? var mimeType: String var metadata: FileMetadata? - // var isLocal: Bool - // var localFileId: String? + var localFileId: String? // var audioProbeResult: AudioProbeResult? Needed for local playback var serverIndex: Int? @@ -208,6 +207,13 @@ struct AudioTrack: Realmable, Codable { duration = 0 mimeType = "" } + + mutating func setLocalInfo(filenameIdMap: [String: String], serverIndex: Int) { + if let localFileId = filenameIdMap[self.metadata?.filename ?? ""] { + self.localFileId = localFileId + self.serverIndex = serverIndex + } + } } struct FileMetadata: Realmable, Codable { diff --git a/ios/App/Shared/models/DownloadItem.swift b/ios/App/Shared/models/DownloadItem.swift index a9ce6c6e..53695220 100644 --- a/ios/App/Shared/models/DownloadItem.swift +++ b/ios/App/Shared/models/DownloadItem.swift @@ -121,6 +121,14 @@ extension DownloadItemPart { } func mimeType() -> String? { - audioTrack?.mimeType ?? episode?.audioTrack?.mimeType + if let track = audioTrack { + return track.mimeType + } else if let podcastTrack = episode?.audioTrack { + return podcastTrack.mimeType + } else if serverPath?.hasSuffix("/cover") ?? false { + return "image/jpg" + } else { + return nil + } } } diff --git a/ios/App/Shared/models/LocalLibrary.swift b/ios/App/Shared/models/LocalLibrary.swift index c2e5841e..888f95ac 100644 --- a/ios/App/Shared/models/LocalLibrary.swift +++ b/ios/App/Shared/models/LocalLibrary.swift @@ -11,12 +11,12 @@ import Unrealm struct LocalLibraryItem: Realmable, Codable { var id: String = "local_\(UUID().uuidString)" var basePath: String = "" - dynamic var _contentUrl: String? + var _contentUrl: String? var isInvalid: Bool = false var mediaType: String = "" var media: MediaType? var localFiles: [LocalFile] = [] - dynamic var _coverContentUrl: String? + var _coverContentUrl: String? var isLocal: Bool = true var serverConnectionConfigId: String? var serverAddress: String? @@ -24,28 +24,18 @@ struct LocalLibraryItem: Realmable, Codable { var libraryItemId: String? var contentUrl: String? { - set(url) { - _contentUrl = url - } - get { - if let path = _contentUrl { - return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString - } else { - return nil - } + 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 - } + if let path = self._coverContentUrl { + return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString + } else { + return nil } } @@ -63,17 +53,15 @@ struct LocalLibraryItem: Realmable, Codable { 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) + 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 { @@ -94,41 +82,23 @@ struct LocalLibraryItem: Realmable, Codable { } } -struct LocalPodcastEpisode: Realmable, Codable { - var id: String = UUID().uuidString - var index: Int = 0 - var episode: String? - var episodeType: String? - var title: String = "Unknown" - var subtitle: String? - var desc: String? - var audioFile: AudioFile? - var audioTrack: AudioTrack? - var duration: Double = 0 - var size: Int = 0 - var serverEpisodeId: String? - - static func primaryKey() -> String? { - return "id" - } -} - struct LocalFile: Realmable, Codable { var id: String = UUID().uuidString var filename: String? - var contentUrl: String = "" - var absolutePath: String { - return AbsDownloader.downloadsDirectory.appendingPathComponent(self.contentUrl).absoluteString - } + var _contentUrl: String = "" var mimeType: String? var size: Int = 0 + var contentUrl: String { + return AbsDownloader.downloadsDirectory.appendingPathComponent(_contentUrl).absoluteString + } + static func primaryKey() -> String? { return "id" } private enum CodingKeys : String, CodingKey { - case id, filename, contentUrl, absolutePath, mimeType, size + case id, filename, contentUrl, mimeType, size } init() {} @@ -137,8 +107,7 @@ struct LocalFile: Realmable, Codable { 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) + mimeType = try? values.decode(String.self, forKey: .mimeType) size = try values.decode(Int.self, forKey: .size) } @@ -147,7 +116,6 @@ struct LocalFile: Realmable, Codable { 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) } diff --git a/ios/App/Shared/models/LocalLibraryExtensions.swift b/ios/App/Shared/models/LocalLibraryExtensions.swift index 1111498e..c6b38dbc 100644 --- a/ios/App/Shared/models/LocalLibraryExtensions.swift +++ b/ios/App/Shared/models/LocalLibraryExtensions.swift @@ -10,15 +10,32 @@ import Foundation extension LocalLibraryItem { init(_ item: LibraryItem, localUrl: String, server: ServerConnectionConfig, files: [LocalFile], coverPath: String?) { self.init() - self.contentUrl = localUrl + self._contentUrl = localUrl self.mediaType = item.mediaType - self.media = item.media self.localFiles = files - self.coverContentUrl = coverPath + self._coverContentUrl = coverPath self.libraryItemId = item.id self.serverConnectionConfigId = server.id self.serverAddress = server.address self.serverUserId = server.userId + + // Link the audio tracks and files + var media = item.media + let fileIdByFilename = Dictionary(uniqueKeysWithValues: files.map { ($0.filename ?? "", $0.id) } ) + if ( item.mediaType == "book" ) { + if let tracks = media.tracks { + for i in tracks.indices { + media.tracks?[i].setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: i) + } + } + } else if ( item.mediaType == "podcast" ) { + if let episodes = media.episodes { + for i in episodes.indices { + media.episodes?[i].audioTrack?.setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: 0) + } + } + } + self.media = media } func getDuration() -> Double { @@ -27,7 +44,7 @@ extension LocalLibraryItem { return total } - func getPlaybackSession(episode: LocalPodcastEpisode?) -> PlaybackSession { + func getPlaybackSession(episode: PodcastEpisode?) -> PlaybackSession { let localEpisodeId = episode?.id let sessionId = "play_local_\(UUID().uuidString)" @@ -49,7 +66,7 @@ extension LocalLibraryItem { id: sessionId, userId: self.serverUserId, libraryItemId: self.libraryItemId, - episodeId: episode?.serverEpisodeId, + episodeId: episode?.id, mediaType: self.mediaType, chapters: [], displayTitle: mediaMetadata?.title, @@ -75,7 +92,7 @@ extension LocalFile { self.id = "\(libraryItemId)_\(filename.toBase64())" self.filename = filename self.mimeType = mimeType - self.contentUrl = localUrl + self._contentUrl = localUrl self.size = fileSize } @@ -91,7 +108,7 @@ extension LocalFile { } extension LocalMediaProgress { - init(localLibraryItem: LocalLibraryItem, episode: LocalPodcastEpisode?, progress: MediaProgress) { + init(localLibraryItem: LocalLibraryItem, episode: PodcastEpisode?, progress: MediaProgress) { self.id = localLibraryItem.id self.localLibraryItemId = localLibraryItem.id self.libraryItemId = localLibraryItem.libraryItemId From 7d730dcafb0c5fdc48ccffb22bf8fba459178aaf Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Wed, 10 Aug 2022 22:56:42 -0400 Subject: [PATCH 02/27] Fix optionals --- ios/App/Shared/models/DataClasses.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/App/Shared/models/DataClasses.swift b/ios/App/Shared/models/DataClasses.swift index f61ca385..8cd32b2d 100644 --- a/ios/App/Shared/models/DataClasses.swift +++ b/ios/App/Shared/models/DataClasses.swift @@ -120,7 +120,7 @@ struct Metadata: Realmable, Codable { struct PodcastEpisode: Realmable, Codable { var id: String - var index: Int + var index: Int? var episode: String? var episodeType: String? var title: String @@ -128,8 +128,8 @@ struct PodcastEpisode: Realmable, Codable { var desc: String? var audioFile: AudioFile? var audioTrack: AudioTrack? - var duration: Double - var size: Int + var duration: Double? + var size: Int? // var serverEpisodeId: String? init() { @@ -156,7 +156,7 @@ struct PodcastEpisode: Realmable, Codable { } struct AudioFile: Realmable, Codable { - var index: Int + var index: Int? var ino: String var metadata: FileMetadata From 81a4e4ff7bb6d3bf89d9cff83493cd246534a838 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Wed, 10 Aug 2022 23:20:56 -0400 Subject: [PATCH 03/27] Enable podcast downloading on iOS --- components/tables/podcast/EpisodeRow.vue | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/components/tables/podcast/EpisodeRow.vue b/components/tables/podcast/EpisodeRow.vue index 0110b1fd..2c409ae2 100644 --- a/components/tables/podcast/EpisodeRow.vue +++ b/components/tables/podcast/EpisodeRow.vue @@ -26,7 +26,7 @@ -
+
audio_file {{ downloadItem ? 'downloading' : 'download' }} download_done @@ -143,7 +143,12 @@ export default { }, downloadClick() { if (this.downloadItem) return - this.download() + if (this.isIos) { + // no local folders on iOS + this.startDownload() + } else { + this.download() + } }, async download(selectedLocalFolder = null) { var localFolder = selectedLocalFolder @@ -183,7 +188,14 @@ export default { } }, async startDownload(localFolder) { - var downloadRes = await AbsDownloader.downloadLibraryItem({ libraryItemId: this.libraryItemId, localFolderId: localFolder.id, episodeId: this.episode.id }) + var payload = { + libraryItemId: this.libraryItemId, + episodeId: this.episodeId + } + if (localFolder) { + this.localFolderId = localFolder.id + } + var downloadRes = await AbsDownloader.downloadLibraryItem(payload) if (downloadRes && downloadRes.error) { var errorMsg = downloadRes.error || 'Unknown error' console.error('Download error', errorMsg) From 599a8e23cbfe61b41f52a9fc9788eb0f3adad8ae Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 11:20:17 -0400 Subject: [PATCH 04/27] Fix podcast episode downloading --- ios/App/App/plugins/AbsDownloader.swift | 6 +++--- ios/App/Shared/models/DataClasses.swift | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index 96a6d1cf..d5d0581f 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -234,7 +234,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { // Queue up everything for downloading var downloadItem = DownloadItem(libraryItem: item, episodeId: episodeId, server: Store.serverConfig!) downloadItem.downloadItemParts = try tracks.enumerated().map({ i, track in - try startLibraryItemTrackDownload(item: item, position: i, track: track) + try startLibraryItemTrackDownload(item: item, position: i, track: track, episode: episode) }) // Also download the cover @@ -253,7 +253,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } } - private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack) throws -> DownloadItemPart { + private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPart { NSLog("TRACK \(track.contentUrl!)") // If we don't name metadata, then we can't proceed @@ -266,7 +266,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { 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) + var downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode) // Store the id on the task so the download item can be pulled from the database later task.taskDescription = downloadItemPart.id diff --git a/ios/App/Shared/models/DataClasses.swift b/ios/App/Shared/models/DataClasses.swift index 8cd32b2d..70a10055 100644 --- a/ios/App/Shared/models/DataClasses.swift +++ b/ios/App/Shared/models/DataClasses.swift @@ -161,7 +161,6 @@ struct AudioFile: Realmable, Codable { var metadata: FileMetadata init() { - index = 0 ino = "" metadata = FileMetadata() } From e52a5fd585210d7ccdf2afd4eeca168d1e709c3d Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 11:20:29 -0400 Subject: [PATCH 05/27] Fix incorrect episode id --- components/tables/podcast/EpisodeRow.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/tables/podcast/EpisodeRow.vue b/components/tables/podcast/EpisodeRow.vue index 2c409ae2..35d5f3ef 100644 --- a/components/tables/podcast/EpisodeRow.vue +++ b/components/tables/podcast/EpisodeRow.vue @@ -190,7 +190,7 @@ export default { async startDownload(localFolder) { var payload = { libraryItemId: this.libraryItemId, - episodeId: this.episodeId + episodeId: this.episode.id } if (localFolder) { this.localFolderId = localFolder.id From 5f4ff380354dc3f1e51b4832ae1dc0a0cbf0e73c Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 12:30:45 -0400 Subject: [PATCH 06/27] Support downloading new podcast episodes --- ios/App/App/plugins/AbsDownloader.swift | 12 +++++-- ios/App/Shared/models/DataClasses.swift | 4 ++- ios/App/Shared/models/LocalLibrary.swift | 4 ++- .../models/LocalLibraryExtensions.swift | 35 +++++++++++++------ ios/App/Shared/util/Database.swift | 2 +- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index d5d0581f..9220f744 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -157,14 +157,19 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } return LocalFile(libraryItem.id, part.filename!, part.mimeType()!, part.destinationUri!, fileSize: Int(part.destinationURL!.fileSize)) } - let localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files, coverPath: coverFile) + var localLibraryItem = Database.shared.getLocalLibraryItemByLLId(libraryItem: libraryItem.id) + if (localLibraryItem != nil && localLibraryItem!.isPodcast) { + try! localLibraryItem?.addFiles(files, item: libraryItem) + } else { + localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files, coverPath: coverFile) + } - Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem) + Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem!) statusNotification["localLibraryItem"] = try? localLibraryItem.asDictionary() if let progress = libraryItem.userMediaProgress { let episode = downloadItem.media?.episodes?.first(where: { $0.id == downloadItem.episodeId }) - let localMediaProgress = LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode, progress: progress) + let localMediaProgress = LocalMediaProgress(localLibraryItem: localLibraryItem!, episode: episode, progress: progress) Database.shared.saveLocalMediaProgress(localMediaProgress) statusNotification["localMediaProgress"] = try? localMediaProgress.asDictionary() } @@ -319,6 +324,7 @@ enum LibraryItemDownloadError: String, Error { case noMetadata = "No metadata for track, unable to download" case libraryItemNotPodcast = "Library item is not a podcast but episode was requested" case podcastEpisodeNotFound = "Invalid podcast episode not found" + case podcastOnlySupported = "Only podcasts are supported for this function" case unknownMediaType = "Unknown media type" case failedDirectory = "Failed to create directory" case failedDownload = "Failed to download item" diff --git a/ios/App/Shared/models/DataClasses.swift b/ios/App/Shared/models/DataClasses.swift index 70a10055..b525264f 100644 --- a/ios/App/Shared/models/DataClasses.swift +++ b/ios/App/Shared/models/DataClasses.swift @@ -207,11 +207,13 @@ struct AudioTrack: Realmable, Codable { mimeType = "" } - mutating func setLocalInfo(filenameIdMap: [String: String], serverIndex: Int) { + mutating func setLocalInfo(filenameIdMap: [String: String], serverIndex: Int) -> Bool { if let localFileId = filenameIdMap[self.metadata?.filename ?? ""] { self.localFileId = localFileId self.serverIndex = serverIndex + return true } + return false } } diff --git a/ios/App/Shared/models/LocalLibrary.swift b/ios/App/Shared/models/LocalLibrary.swift index 888f95ac..49af3994 100644 --- a/ios/App/Shared/models/LocalLibrary.swift +++ b/ios/App/Shared/models/LocalLibrary.swift @@ -9,7 +9,7 @@ import Foundation import Unrealm struct LocalLibraryItem: Realmable, Codable { - var id: String = "local_\(UUID().uuidString)" + var id: String = "" var basePath: String = "" var _contentUrl: String? var isInvalid: Bool = false @@ -39,6 +39,8 @@ struct LocalLibraryItem: Realmable, Codable { } } + var isPodcast: Bool { self.mediaType == "podcast" } + static func primaryKey() -> String? { return "id" } diff --git a/ios/App/Shared/models/LocalLibraryExtensions.swift b/ios/App/Shared/models/LocalLibraryExtensions.swift index c6b38dbc..eb36275b 100644 --- a/ios/App/Shared/models/LocalLibraryExtensions.swift +++ b/ios/App/Shared/models/LocalLibraryExtensions.swift @@ -10,6 +10,7 @@ import Foundation extension LocalLibraryItem { init(_ item: LibraryItem, localUrl: String, server: ServerConnectionConfig, files: [LocalFile], coverPath: String?) { self.init() + self.id = "local_\(item.id)" self._contentUrl = localUrl self.mediaType = item.mediaType self.localFiles = files @@ -20,22 +21,36 @@ extension LocalLibraryItem { self.serverUserId = server.userId // Link the audio tracks and files - var media = item.media - let fileIdByFilename = Dictionary(uniqueKeysWithValues: files.map { ($0.filename ?? "", $0.id) } ) - if ( item.mediaType == "book" ) { - if let tracks = media.tracks { + linkLocalFiles(files, fromMedia: item.media) + } + + mutating func addFiles(_ files: [LocalFile], item: LibraryItem) throws { + guard self.isPodcast else { throw LibraryItemDownloadError.podcastOnlySupported } + self.localFiles.append(contentsOf: files.filter({ $0.isAudioFile() })) + linkLocalFiles(self.localFiles, fromMedia: item.media) + } + + mutating private func linkLocalFiles(_ files: [LocalFile], fromMedia: MediaType) { + var fromMedia = fromMedia + let fileMap = files.map { ($0.filename ?? "", $0.id) } + let fileIdByFilename = Dictionary(fileMap, uniquingKeysWith: { (_, last) in last }) + if ( self.mediaType == "book" ) { + if let tracks = fromMedia.tracks { for i in tracks.indices { - media.tracks?[i].setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: i) + _ = fromMedia.tracks?[i].setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: i) } } - } else if ( item.mediaType == "podcast" ) { - if let episodes = media.episodes { - for i in episodes.indices { - media.episodes?[i].audioTrack?.setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: 0) + } else if ( self.mediaType == "podcast" ) { + if let episodes = fromMedia.episodes { + fromMedia.episodes = episodes.compactMap { episode in + // Filter out episodes not downloaded + var episode = episode + let episodeIsDownloaded = episode.audioTrack?.setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: 0) ?? false + return episodeIsDownloaded ? episode : nil } } } - self.media = media + self.media = fromMedia } func getDuration() -> Double { diff --git a/ios/App/Shared/util/Database.swift b/ios/App/Shared/util/Database.swift index 64ffc05e..849de8f8 100644 --- a/ios/App/Shared/util/Database.swift +++ b/ios/App/Shared/util/Database.swift @@ -168,7 +168,7 @@ class Database { public func saveLocalMediaProgress(_ mediaProgress: LocalMediaProgress) { let realm = try! Realm() - try! realm.write { realm.add(mediaProgress) } + try! realm.write { realm.add(mediaProgress, update: .modified) } } // For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}" From 2b07c81e546887675f023df45db5f1de75216831 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 12:32:58 -0400 Subject: [PATCH 07/27] Use book/podcast helper --- ios/App/Shared/models/LocalLibrary.swift | 1 + ios/App/Shared/models/LocalLibraryExtensions.swift | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ios/App/Shared/models/LocalLibrary.swift b/ios/App/Shared/models/LocalLibrary.swift index 49af3994..71eda6d2 100644 --- a/ios/App/Shared/models/LocalLibrary.swift +++ b/ios/App/Shared/models/LocalLibrary.swift @@ -39,6 +39,7 @@ struct LocalLibraryItem: Realmable, Codable { } } + var isBook: Bool { self.mediaType == "book" } var isPodcast: Bool { self.mediaType == "podcast" } static func primaryKey() -> String? { diff --git a/ios/App/Shared/models/LocalLibraryExtensions.swift b/ios/App/Shared/models/LocalLibraryExtensions.swift index eb36275b..4e99606d 100644 --- a/ios/App/Shared/models/LocalLibraryExtensions.swift +++ b/ios/App/Shared/models/LocalLibraryExtensions.swift @@ -34,13 +34,13 @@ extension LocalLibraryItem { var fromMedia = fromMedia let fileMap = files.map { ($0.filename ?? "", $0.id) } let fileIdByFilename = Dictionary(fileMap, uniquingKeysWith: { (_, last) in last }) - if ( self.mediaType == "book" ) { + if ( self.isBook ) { if let tracks = fromMedia.tracks { for i in tracks.indices { _ = fromMedia.tracks?[i].setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: i) } } - } else if ( self.mediaType == "podcast" ) { + } else if ( self.isPodcast ) { if let episodes = fromMedia.episodes { fromMedia.episodes = episodes.compactMap { episode in // Filter out episodes not downloaded From 999d434fe909963bad7eb90507662a4608732872 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 12:58:47 -0400 Subject: [PATCH 08/27] Fix download item not clearing for podcasts --- ios/App/App/plugins/AbsDownloader.swift | 2 +- ios/App/Shared/models/DataClasses.swift | 40 +++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index 9220f744..b9e86c00 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -142,7 +142,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { private func handleDownloadTaskCompleteFromDownloadItem(_ downloadItem: DownloadItem) { var statusNotification = [String: Any]() - statusNotification["libraryItemId"] = downloadItem.libraryItemId + statusNotification["libraryItemId"] = downloadItem.id if ( downloadItem.didDownloadSuccessfully() ) { ApiClient.getLibraryItemWithProgress(libraryItemId: downloadItem.libraryItemId!, episodeId: downloadItem.episodeId) { libraryItem in diff --git a/ios/App/Shared/models/DataClasses.swift b/ios/App/Shared/models/DataClasses.swift index b525264f..fb86b667 100644 --- a/ios/App/Shared/models/DataClasses.swift +++ b/ios/App/Shared/models/DataClasses.swift @@ -130,7 +130,7 @@ struct PodcastEpisode: Realmable, Codable { var audioTrack: AudioTrack? var duration: Double? var size: Int? -// var serverEpisodeId: String? + var serverEpisodeId: String { self.id } init() { id = "" @@ -140,6 +140,10 @@ struct PodcastEpisode: Realmable, Codable { size = 0 } + static func ignoredProperties() -> [String] { + ["serverEpisodeId"] + } + private enum CodingKeys : String, CodingKey { case id, index, @@ -151,7 +155,39 @@ struct PodcastEpisode: Realmable, Codable { audioFile, audioTrack, duration, - size + size, + serverEpisodeId + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(String.self, forKey: .id) + index = try? values.decode(Int.self, forKey: .index) + episode = try? values.decode(String.self, forKey: .episode) + episodeType = try? values.decode(String.self, forKey: .episodeType) + title = try values.decode(String.self, forKey: .title) + subtitle = try? values.decode(String.self, forKey: .subtitle) + desc = try? values.decode(String.self, forKey: .desc) + audioFile = try? values.decode(AudioFile.self, forKey: .audioFile) + audioTrack = try? values.decode(AudioTrack.self, forKey: .audioTrack) + duration = try? values.decode(Double.self, forKey: .duration) + 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(index, forKey: .index) + try container.encode(episode, forKey: .episode) + try container.encode(episodeType, forKey: .episodeType) + try container.encode(title, forKey: .title) + try container.encode(subtitle, forKey: .subtitle) + try container.encode(desc, forKey: .desc) + try container.encode(audioFile, forKey: .audioFile) + try container.encode(audioTrack, forKey: .audioTrack) + try container.encode(duration, forKey: .duration) + try container.encode(size, forKey: .size) + try container.encode(serverEpisodeId, forKey: .serverEpisodeId) } } From 02eabb82c1091bbcf83f6b815bedd1836d902219 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 13:16:25 -0400 Subject: [PATCH 09/27] Hide UI options that iOS does not need / support --- ios/App/App/plugins/AbsFileSystem.swift | 14 +++++------ pages/localMedia/item/_id.vue | 31 ++++++++++--------------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/ios/App/App/plugins/AbsFileSystem.swift b/ios/App/App/plugins/AbsFileSystem.swift index c2d59580..83610d75 100644 --- a/ios/App/App/plugins/AbsFileSystem.swift +++ b/ios/App/App/plugins/AbsFileSystem.swift @@ -16,7 +16,7 @@ public class AbsFileSystem: CAPPlugin { // TODO: Implement NSLog("Select Folder for media type \(mediaType ?? "UNSET")") - call.resolve() + call.unavailable("Not available on iOS") } @objc func checkFolderPermission(_ call: CAPPluginCall) { @@ -25,9 +25,7 @@ public class AbsFileSystem: CAPPlugin { // TODO: Is this even necessary on iOS? NSLog("checkFolderPermission for folder \(folderUrl ?? "UNSET")") - call.resolve([ - "value": true - ]) + call.unavailable("Not available on iOS") } @objc func scanFolder(_ call: CAPPluginCall) { @@ -37,7 +35,7 @@ public class AbsFileSystem: CAPPlugin { // TODO: Implement NSLog("scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)") - call.resolve() + call.unavailable("Not available on iOS") } @objc func removeFolder(_ call: CAPPluginCall) { @@ -46,7 +44,7 @@ public class AbsFileSystem: CAPPlugin { // TODO: Implement NSLog("removeFolder \(folderId ?? "UNSET")") - call.resolve() + call.unavailable("Not available on iOS") } @objc func removeLocalLibraryItem(_ call: CAPPluginCall) { @@ -55,7 +53,7 @@ public class AbsFileSystem: CAPPlugin { // TODO: Implement NSLog("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")") - call.resolve() + call.unavailable("Not available on iOS") } @objc func scanLocalLibraryItem(_ call: CAPPluginCall) { @@ -65,7 +63,7 @@ public class AbsFileSystem: CAPPlugin { // TODO: Implement NSLog("scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)") - call.resolve() + call.unavailable("Not available on iOS") } @objc func deleteItem(_ call: CAPPluginCall) { diff --git a/pages/localMedia/item/_id.vue b/pages/localMedia/item/_id.vue index 0d825982..53714ad4 100644 --- a/pages/localMedia/item/_id.vue +++ b/pages/localMedia/item/_id.vue @@ -11,7 +11,7 @@ more_vert
-

Folder: {{ folderName }}

+

Folder: {{ folderName }}

{{ libraryItemId ? 'Linked to item on server ' + liServerAddress : 'Not linked to server item' }}

@@ -138,6 +138,9 @@ export default { } }, computed: { + isIos() { + return this.$platform === 'ios' + }, basePath() { return this.localLibraryItem ? this.localLibraryItem.basePath : null }, @@ -194,24 +197,14 @@ export default { } ] } else { - return [ - { - text: 'Scan', - value: 'scan' - }, - { - text: 'Force Re-Scan', - value: 'rescan' - }, - { - text: 'Remove', - value: 'remove' - }, - { - text: 'Remove & Delete Files', - value: 'delete' - } - ] + var options = [] + if ( !this.isIos ) { + options.push({ text: 'Scan', value: 'scan'}) + options.push({ text: 'Force Re-Scan', value: 'rescan'}) + options.push({ text: 'Remove', value: 'remove'}) + } + options.push({ text: 'Remove & Delete Files', value: 'delete'}) + return options } } }, From 8edeefc1cd29284c5e22a2c5abce393ced8f80d9 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 13:46:30 -0400 Subject: [PATCH 10/27] Prevent iCloud backups of downloads --- ios/App/App/plugins/AbsDownloader.swift | 28 +++++++++++++++++++----- ios/App/Shared/models/DownloadItem.swift | 2 +- ios/App/Shared/models/LocalLibrary.swift | 6 ++--- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index b9e86c00..06b6e131 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -11,7 +11,7 @@ import Capacitor @objc(AbsDownloader) public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { - static let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] typealias DownloadProgressHandler = (_ downloadItem: DownloadItem, _ downloadItemPart: inout DownloadItemPart) throws -> Void @@ -307,16 +307,34 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { let itemDirectory = item.id NSLog("ITEM DIR \(itemDirectory)") - do { - try FileManager.default.createDirectory(at: AbsDownloader.downloadsDirectory.appendingPathComponent(itemDirectory), withIntermediateDirectories: true) - } catch { - NSLog("Failed to CREATE LI DIRECTORY \(error)") + guard AbsDownloader.itemDownloadFolder(path: itemDirectory) != nil else { + NSLog("Failed to CREATE LI DIRECTORY \(itemDirectory)") throw LibraryItemDownloadError.failedDirectory } return itemDirectory } + static func itemDownloadFolder(path: String) -> URL? { + do { + var itemFolder = AbsDownloader.downloadsDirectory.appendingPathComponent(path) + + if !FileManager.default.fileExists(atPath: itemFolder.path) { + try FileManager.default.createDirectory(at: itemFolder, withIntermediateDirectories: true) + } + + // Make sure we don't backup download files to iCloud + var resourceValues = URLResourceValues() + resourceValues.isExcludedFromBackup = true + try itemFolder.setResourceValues(resourceValues) + + return itemFolder + } catch { + NSLog("Failed to CREATE LI DIRECTORY \(error)") + return nil + } + } + } enum LibraryItemDownloadError: String, Error { diff --git a/ios/App/Shared/models/DownloadItem.swift b/ios/App/Shared/models/DownloadItem.swift index 53695220..8be65ae3 100644 --- a/ios/App/Shared/models/DownloadItem.swift +++ b/ios/App/Shared/models/DownloadItem.swift @@ -82,7 +82,7 @@ struct DownloadItemPart: Realmable, Codable { var destinationUri: String? var destinationURL: URL? { if let destinationUri = self.destinationUri { - return AbsDownloader.downloadsDirectory.appendingPathComponent(destinationUri) + return AbsDownloader.itemDownloadFolder(path: destinationUri)! } else { return nil } diff --git a/ios/App/Shared/models/LocalLibrary.swift b/ios/App/Shared/models/LocalLibrary.swift index 71eda6d2..542f4dab 100644 --- a/ios/App/Shared/models/LocalLibrary.swift +++ b/ios/App/Shared/models/LocalLibrary.swift @@ -25,7 +25,7 @@ struct LocalLibraryItem: Realmable, Codable { var contentUrl: String? { if let path = _contentUrl { - return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString + return AbsDownloader.itemDownloadFolder(path: path)!.absoluteString } else { return nil } @@ -33,7 +33,7 @@ struct LocalLibraryItem: Realmable, Codable { var coverContentUrl: String? { if let path = self._coverContentUrl { - return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString + return AbsDownloader.itemDownloadFolder(path: path)!.absoluteString } else { return nil } @@ -93,7 +93,7 @@ struct LocalFile: Realmable, Codable { var size: Int = 0 var contentUrl: String { - return AbsDownloader.downloadsDirectory.appendingPathComponent(_contentUrl).absoluteString + return AbsDownloader.itemDownloadFolder(path: _contentUrl)!.absoluteString } static func primaryKey() -> String? { From 60cb5f398c54f0c64cd84fda5c20f9221147ad48 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 14:12:45 -0400 Subject: [PATCH 11/27] Handle deleting library items --- ios/App/App/plugins/AbsAudioPlayer.swift | 2 +- ios/App/App/plugins/AbsDatabase.swift | 4 ++-- ios/App/App/plugins/AbsDownloader.swift | 2 +- ios/App/App/plugins/AbsFileSystem.swift | 18 ++++++++++++++---- ios/App/Shared/models/LocalLibrary.swift | 8 ++++++++ ios/App/Shared/util/Database.swift | 24 ++++++++++++------------ pages/localMedia/item/_id.vue | 4 ++-- 7 files changed, 40 insertions(+), 22 deletions(-) diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index a66d3b36..d4e74869 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -44,7 +44,7 @@ public class AbsAudioPlayer: CAPPlugin { let isLocalItem = libraryItemId?.starts(with: "local_") ?? false if (isLocalItem) { - let item = Database.shared.getLocalLibraryItem(localLibraryItem: libraryItemId!) + let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!) // TODO: Logic required for podcasts here let playbackSession = item?.getPlaybackSession(episode: nil) PlayerHandler.startPlayback(session: playbackSession!, playWhenReady: playWhenReady, playbackRate: playbackRate) diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index fdc3bab6..1d3f6569 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -81,7 +81,7 @@ public class AbsDatabase: CAPPlugin { @objc func getLocalLibraryItem(_ call: CAPPluginCall) { do { - let item = Database.shared.getLocalLibraryItem(localLibraryItem: call.getString("id") ?? "") + let item = Database.shared.getLocalLibraryItem(localLibraryItemId: call.getString("id") ?? "") switch item { case .some(let foundItem): call.resolve(try foundItem.asDictionary()) @@ -97,7 +97,7 @@ public class AbsDatabase: CAPPlugin { @objc func getLocalLibraryItemByLId(_ call: CAPPluginCall) { do { - let item = Database.shared.getLocalLibraryItemByLLId(libraryItem: call.getString("libraryItemId") ?? "") + let item = Database.shared.getLocalLibraryItem(byServerLibraryItemId: call.getString("libraryItemId") ?? "") switch item { case .some(let foundItem): call.resolve(try foundItem.asDictionary()) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index 06b6e131..bfb2bd60 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -157,7 +157,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } return LocalFile(libraryItem.id, part.filename!, part.mimeType()!, part.destinationUri!, fileSize: Int(part.destinationURL!.fileSize)) } - var localLibraryItem = Database.shared.getLocalLibraryItemByLLId(libraryItem: libraryItem.id) + var localLibraryItem = Database.shared.getLocalLibraryItem(byServerLibraryItemId: libraryItem.id) if (localLibraryItem != nil && localLibraryItem!.isPodcast) { try! localLibraryItem?.addFiles(files, item: libraryItem) } else { diff --git a/ios/App/App/plugins/AbsFileSystem.swift b/ios/App/App/plugins/AbsFileSystem.swift index 83610d75..a1a2763d 100644 --- a/ios/App/App/plugins/AbsFileSystem.swift +++ b/ios/App/App/plugins/AbsFileSystem.swift @@ -67,13 +67,23 @@ public class AbsFileSystem: CAPPlugin { } @objc func deleteItem(_ call: CAPPluginCall) { - let localLibraryItemId = call.getString("localLibraryItemId") + let localLibraryItemId = call.getString("id") let contentUrl = call.getString("contentUrl") - - // TODO: Implement + NSLog("deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")") - call.resolve() + var success = false + do { + if let localLibraryItemId = localLibraryItemId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) { + try FileManager.default.removeItem(at: item.contentDirectory!) + Database.shared.removeLocalLibraryItem(localLibraryItemId: localLibraryItemId) + success = true + } + } catch { + NSLog("Failed to delete \(error)") + } + + call.resolve(["success": success]) } @objc func deleteTrackFromItem(_ call: CAPPluginCall) { diff --git a/ios/App/Shared/models/LocalLibrary.swift b/ios/App/Shared/models/LocalLibrary.swift index 542f4dab..845434c8 100644 --- a/ios/App/Shared/models/LocalLibrary.swift +++ b/ios/App/Shared/models/LocalLibrary.swift @@ -31,6 +31,14 @@ struct LocalLibraryItem: Realmable, Codable { } } + var contentDirectory: URL? { + if let path = _contentUrl { + return AbsDownloader.itemDownloadFolder(path: path) + } else { + return nil + } + } + var coverContentUrl: String? { if let path = self._coverContentUrl { return AbsDownloader.itemDownloadFolder(path: path)!.absoluteString diff --git a/ios/App/Shared/util/Database.swift b/ios/App/Shared/util/Database.swift index 849de8f8..a1b418ae 100644 --- a/ios/App/Shared/util/Database.swift +++ b/ios/App/Shared/util/Database.swift @@ -108,14 +108,14 @@ class Database { return Array(realm.objects(LocalLibraryItem.self)) } - public func getLocalLibraryItemByLLId(libraryItem: String) -> LocalLibraryItem? { + public func getLocalLibraryItem(byServerLibraryItemId: String) -> LocalLibraryItem? { let realm = try! Realm() - return realm.objects(LocalLibraryItem.self).first(where: { $0.libraryItemId == libraryItem }) + return realm.objects(LocalLibraryItem.self).first(where: { $0.libraryItemId == byServerLibraryItemId }) } - public func getLocalLibraryItem(localLibraryItem: String) -> LocalLibraryItem? { + public func getLocalLibraryItem(localLibraryItemId: String) -> LocalLibraryItem? { let realm = try! Realm() - return realm.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItem) + return realm.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItemId) } public func saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) { @@ -123,6 +123,14 @@ class Database { try! realm.write { realm.add(localLibraryItem, update: .modified) } } + public func removeLocalLibraryItem(localLibraryItemId: String) { + let realm = try! Realm() + try! realm.write { + let item = getLocalLibraryItem(localLibraryItemId: localLibraryItemId) + realm.delete(item!) + } + } + public func getDownloadItem(downloadItemId: String) -> DownloadItem? { let realm = try! Realm() return realm.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId) @@ -158,14 +166,6 @@ class Database { return realm.objects(DeviceSettings.self).first ?? getDefaultDeviceSettings() } - public func removeLocalLibraryItem(localLibraryItemId: String) { - let realm = try! Realm() - try! realm.write { - let item = getLocalLibraryItemByLLId(libraryItem: localLibraryItemId) - realm.delete(item!) - } - } - public func saveLocalMediaProgress(_ mediaProgress: LocalMediaProgress) { let realm = try! Realm() try! realm.write { realm.add(mediaProgress, update: .modified) } diff --git a/pages/localMedia/item/_id.vue b/pages/localMedia/item/_id.vue index 53714ad4..94fcec33 100644 --- a/pages/localMedia/item/_id.vue +++ b/pages/localMedia/item/_id.vue @@ -322,13 +322,13 @@ export default { async deleteItem() { const { value } = await Dialog.confirm({ title: 'Confirm', - message: `Warning! This will delete the folder "${this.basePath}" and all contents. Are you sure?` + message: `Warning! This will delete "${this.media.metadata.title}" and all associated local files. Are you sure?` }) if (value) { var res = await AbsFileSystem.deleteItem(this.localLibraryItem) if (res && res.success) { this.$toast.success('Deleted Successfully') - this.$router.replace(`/localMedia/folders/${this.folderId}`) + this.$router.replace(this.isIos ? '/bookshelf' : `/localMedia/folders/${this.folderId}`) } else this.$toast.error('Failed to delete') } }, From 4e63d4367963eb1bc84019782912bb0606d07778 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 15:36:06 -0400 Subject: [PATCH 12/27] Fix progress done percent --- ios/App/App/plugins/AbsDownloader.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index bfb2bd60..2aa34eb0 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -26,7 +26,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in - downloadItemPart.progress = 1 + downloadItemPart.progress = 100 downloadItemPart.completed = true do { From 0a989e7811289cfc8e9a3b696f4b9d11aa351777 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 15:36:25 -0400 Subject: [PATCH 13/27] Handle deleting podcast episodes --- ios/App/App/plugins/AbsFileSystem.swift | 31 ++++++++++++++++++++---- ios/App/Shared/models/LocalLibrary.swift | 9 ++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/ios/App/App/plugins/AbsFileSystem.swift b/ios/App/App/plugins/AbsFileSystem.swift index a1a2763d..7335fb13 100644 --- a/ios/App/App/plugins/AbsFileSystem.swift +++ b/ios/App/App/plugins/AbsFileSystem.swift @@ -81,19 +81,40 @@ public class AbsFileSystem: CAPPlugin { } } catch { NSLog("Failed to delete \(error)") + success = false } call.resolve(["success": success]) } @objc func deleteTrackFromItem(_ call: CAPPluginCall) { - let localLibraryItemId = call.getString("localLibraryItemId") + let localLibraryItemId = call.getString("id") let trackLocalFileId = call.getString("trackLocalFileId") - let contentUrl = call.getString("contentUrl") - // TODO: Implement - NSLog("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET") url \(contentUrl ?? "UNSET")") + NSLog("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET")") - call.resolve() + var success = false + do { + if let localLibraryItemId = localLibraryItemId, let trackLocalFileId = trackLocalFileId, var item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) { + if let fileIndex = item.localFiles.firstIndex(where: { $0.id == trackLocalFileId }) { + try FileManager.default.removeItem(at: item.localFiles[fileIndex].contentPath) + item.localFiles.remove(at: fileIndex) + if item.isPodcast, var media = item.media { + media.episodes = media.episodes?.filter { $0.audioTrack?.localFileId != trackLocalFileId } + item.media = media + } + Database.shared.saveLocalLibraryItem(localLibraryItem: item) + call.resolve(try item.asDictionary()) + success = true + } + } + } catch { + NSLog("Failed to delete \(error)") + success = false + } + + if !success { + call.resolve(["success": success]) + } } } diff --git a/ios/App/Shared/models/LocalLibrary.swift b/ios/App/Shared/models/LocalLibrary.swift index 845434c8..fde755df 100644 --- a/ios/App/Shared/models/LocalLibrary.swift +++ b/ios/App/Shared/models/LocalLibrary.swift @@ -100,16 +100,16 @@ struct LocalFile: Realmable, Codable { var mimeType: String? var size: Int = 0 - var contentUrl: String { - return AbsDownloader.itemDownloadFolder(path: _contentUrl)!.absoluteString - } + var contentUrl: String { AbsDownloader.itemDownloadFolder(path: _contentUrl)!.absoluteString } + var contentPath: URL { AbsDownloader.itemDownloadFolder(path: _contentUrl)! } + var basePath: String? { self.filename } static func primaryKey() -> String? { return "id" } private enum CodingKeys : String, CodingKey { - case id, filename, contentUrl, mimeType, size + case id, filename, contentUrl, mimeType, size, basePath } init() {} @@ -129,6 +129,7 @@ struct LocalFile: Realmable, Codable { try container.encode(contentUrl, forKey: .contentUrl) try container.encode(mimeType, forKey: .mimeType) try container.encode(size, forKey: .size) + try container.encode(basePath, forKey: .basePath) } } From a107a25e4329cda03aefecdaa89a33c58e9e0ab9 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 16:11:53 -0400 Subject: [PATCH 14/27] Fix downloading in progress books --- ios/App/Shared/models/DataClasses.swift | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/ios/App/Shared/models/DataClasses.swift b/ios/App/Shared/models/DataClasses.swift index fb86b667..578ee637 100644 --- a/ios/App/Shared/models/DataClasses.swift +++ b/ios/App/Shared/models/DataClasses.swift @@ -325,6 +325,56 @@ struct MediaProgress: Realmable, Codable { lastUpdate = 0 startedAt = 0 } + + private enum CodingKeys : String, CodingKey { + case id, libraryItemId, episodeId, duration, progress, currentTime, isFinished, lastUpdate, startedAt, finishedAt + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(String.self, forKey: .id) + libraryItemId = try values.decode(String.self, forKey: .libraryItemId) + episodeId = try? values.decode(String.self, forKey: .episodeId) + duration = try MediaProgress.doubleOrStringDecoder(from: decoder, with: values, key: .duration) + progress = try MediaProgress.doubleOrStringDecoder(from: decoder, with: values, key: .progress) + currentTime = try MediaProgress.doubleOrStringDecoder(from: decoder, with: values, key: .currentTime) + isFinished = try values.decode(Bool.self, forKey: .isFinished) + lastUpdate = try MediaProgress.intOrStringDecoder(from: decoder, with: values, key: .lastUpdate) + startedAt = try MediaProgress.intOrStringDecoder(from: decoder, with: values, key: .startedAt) + finishedAt = try? MediaProgress.intOrStringDecoder(from: decoder, with: values, key: .finishedAt) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(libraryItemId, forKey: .libraryItemId) + try container.encode(episodeId, forKey: .episodeId) + try container.encode(duration, forKey: .duration) + try container.encode(progress, forKey: .progress) + try container.encode(currentTime, forKey: .currentTime) + try container.encode(isFinished, forKey: .isFinished) + try container.encode(lastUpdate, forKey: .lastUpdate) + try container.encode(startedAt, forKey: .startedAt) + try container.encode(finishedAt, forKey: .finishedAt) + } + + static private func doubleOrStringDecoder(from decoder: Decoder, with values: KeyedDecodingContainer, key: MediaProgress.CodingKeys) throws -> Double { + do { + return try values.decode(Double.self, forKey: key) + } catch { + let stringDuration = try values.decode(String.self, forKey: key) + return Double(stringDuration) ?? 0.0 + } + } + + static private func intOrStringDecoder(from decoder: Decoder, with values: KeyedDecodingContainer, key: MediaProgress.CodingKeys) throws -> Int { + do { + return try values.decode(Int.self, forKey: key) + } catch { + let stringDuration = try values.decode(String.self, forKey: key) + return Int(stringDuration) ?? 0 + } + } } struct PlaybackMetadata: Realmable, Codable { From 64707b0928ff5b4bcb07d89b7188cecad33f51cb Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 16:26:32 -0400 Subject: [PATCH 15/27] Support local media progress --- ios/App/App/plugins/AbsDatabase.swift | 10 +++++++++- ios/App/Shared/models/LocalLibraryExtensions.swift | 1 + ios/App/Shared/util/Database.swift | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index 1d3f6569..6520c9b2 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -114,9 +114,17 @@ public class AbsDatabase: CAPPlugin { @objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) { call.resolve([ "value": [] ]) } + @objc func getAllLocalMediaProgress(_ call: CAPPluginCall) { - call.resolve([ "value": [] ]) + do { + call.resolve([ "value": try Database.shared.getAllLocalMediaProgress().asDictionaryArray() ]) + } catch { + NSLog("Error while loading local media progress") + debugPrint(error) + call.resolve(["value": []]) + } } + @objc func updateDeviceSettings(_ call: CAPPluginCall) { let disableAutoRewind = call.getBool("disableAutoRewind") ?? false let enableAltView = call.getBool("enableAltView") ?? false diff --git a/ios/App/Shared/models/LocalLibraryExtensions.swift b/ios/App/Shared/models/LocalLibraryExtensions.swift index 4e99606d..7dbc256a 100644 --- a/ios/App/Shared/models/LocalLibraryExtensions.swift +++ b/ios/App/Shared/models/LocalLibraryExtensions.swift @@ -138,6 +138,7 @@ extension LocalMediaProgress { self.serverConnectionConfigId = localLibraryItem.serverConnectionConfigId self.duration = progress.duration + self.progress = progress.progress self.currentTime = progress.currentTime self.isFinished = false self.lastUpdate = progress.lastUpdate diff --git a/ios/App/Shared/util/Database.swift b/ios/App/Shared/util/Database.swift index a1b418ae..d8a30851 100644 --- a/ios/App/Shared/util/Database.swift +++ b/ios/App/Shared/util/Database.swift @@ -166,6 +166,11 @@ class Database { return realm.objects(DeviceSettings.self).first ?? getDefaultDeviceSettings() } + public func getAllLocalMediaProgress() -> [LocalMediaProgress] { + let realm = try! Realm() + return Array(realm.objects(LocalMediaProgress.self)) + } + public func saveLocalMediaProgress(_ mediaProgress: LocalMediaProgress) { let realm = try! Realm() try! realm.write { realm.add(mediaProgress, update: .modified) } From 2e6946e1f70da5dd927fa8c547e63ceab0ec6e89 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 16:39:59 -0400 Subject: [PATCH 16/27] Disable track reordering on iOS --- pages/localMedia/item/_id.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/localMedia/item/_id.vue b/pages/localMedia/item/_id.vue index 94fcec33..4b342c13 100644 --- a/pages/localMedia/item/_id.vue +++ b/pages/localMedia/item/_id.vue @@ -22,11 +22,11 @@

Audio Tracks ({{ audioTracks.length }})

- +