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