// // LocalLibraryItem.swift // App // // Created by Ron Heft on 8/16/22. // import Foundation import RealmSwift class LocalLibraryItem: Object, Codable { @Persisted(primaryKey: true) var id: String = "local_\(UUID().uuidString)" @Persisted var basePath: String = "" @Persisted var _contentUrl: String? @Persisted var isInvalid: Bool = false @Persisted var mediaType: String = "" @Persisted var media: MediaType? @Persisted var localFiles = List() @Persisted var _coverContentUrl: String? @Persisted var isLocal: Bool = true @Persisted var serverConnectionConfigId: String? @Persisted var serverAddress: String? @Persisted var serverUserId: String? @Persisted(indexed: true) var libraryItemId: String? var contentUrl: String? { if let path = _contentUrl { return AbsDownloader.itemDownloadFolder(path: path)!.absoluteString } else { return nil } } 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 } else { return nil } } var isBook: Bool { self.mediaType == "book" } var isPodcast: Bool { self.mediaType == "podcast" } private enum CodingKeys : String, CodingKey { case id, basePath, contentUrl, isInvalid, mediaType, media, localFiles, coverContentUrl, isLocal, serverConnectionConfigId, serverAddress, serverUserId, libraryItemId } override init() { super.init() } required init(from decoder: Decoder) throws { super.init() let values = try decoder.container(keyedBy: CodingKeys.self) id = try values.decode(String.self, forKey: .id) basePath = try values.decode(String.self, forKey: .basePath) isInvalid = try values.decode(Bool.self, forKey: .isInvalid) mediaType = try values.decode(String.self, forKey: .mediaType) media = try? values.decode(MediaType.self, forKey: .media) if let files = try? values.decode([LocalFile].self, forKey: .localFiles) { localFiles.append(objectsIn: files) } 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(Array(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) } } extension LocalLibraryItem { convenience init(_ item: LibraryItem, localUrl: String, server: ServerConnectionConfig, files: [LocalFile], coverPath: String?) { self.init() self._contentUrl = localUrl self.mediaType = item.mediaType self.localFiles.append(objectsIn: files) 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 linkLocalFiles(self.localFiles, fromMedia: item.media) } func addFiles(_ files: [LocalFile], item: LibraryItem) throws { guard self.isPodcast else { throw LibraryItemDownloadError.podcastOnlySupported } self.localFiles.append(objectsIn: files.filter({ $0.isAudioFile() })) linkLocalFiles(self.localFiles, fromMedia: item.media) } private func linkLocalFiles(_ files: List, fromMedia: MediaType?) { guard let fromMedia = MediaType.detachCopy(of: fromMedia) else { return } let fileMap = files.map { ($0.filename ?? "", $0.id) } let fileIdByFilename = Dictionary(fileMap, uniquingKeysWith: { (_, last) in last }) if ( self.isBook ) { for i in fromMedia.tracks.indices { _ = fromMedia.tracks[i].setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: i) } } else if ( self.isPodcast ) { let episodes = List() for episode in fromMedia.episodes { // Filter out episodes not downloaded let episodeIsDownloaded = episode.audioTrack?.setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: 0) ?? false if episodeIsDownloaded { episodes.append(episode) } } fromMedia.episodes = episodes } self.media = fromMedia } func getDuration() -> Double { var total = 0.0 self.media?.tracks.enumerated().forEach { _, track in total += track.duration } return total } func getPodcastEpisode(episodeId: String?) -> PodcastEpisode? { guard self.isPodcast else { return nil } guard let episodes = self.media?.episodes else { return nil } return episodes.first(where: { $0.id == episodeId }) } func getPlaybackSession(episode: PodcastEpisode?) -> PlaybackSession { let localEpisodeId = episode?.id let sessionId = "play_local_\(UUID().uuidString)" // Get current progress from local media let mediaProgressId = (localEpisodeId != nil) ? "\(self.id)-\(localEpisodeId!)" : self.id let mediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: mediaProgressId) let mediaMetadata = Metadata.detachCopy(of: self.media?.metadata) let chapters = List() self.media?.chapters.forEach { chapter in chapters.append(Chapter.detachCopy(of: chapter)!) } let authorName = mediaMetadata?.authorDisplayName let audioTracks = List() if let episode = episode, let track = episode.audioTrack { audioTracks.append(AudioTrack.detachCopy(of: track)!) } else if let tracks = self.media?.tracks { tracks.forEach { t in audioTracks.append(AudioTrack.detachCopy(of: t)!) } } let dateNow = Date().timeIntervalSince1970 return PlaybackSession( id: sessionId, userId: self.serverUserId, libraryItemId: self.libraryItemId, episodeId: episode?.serverEpisodeId, mediaType: self.mediaType, mediaMetadata: mediaMetadata, chapters: chapters, displayTitle: mediaMetadata?.title, displayAuthor: authorName, coverPath: self.coverContentUrl, duration: self.getDuration(), playMethod: PlayMethod.local.rawValue, startedAt: dateNow, updatedAt: dateNow, timeListening: 0.0, audioTracks: audioTracks, currentTime: mediaProgress?.currentTime ?? 0.0, libraryItem: nil, localLibraryItem: self, serverConnectionConfigId: self.serverConnectionConfigId, serverAddress: self.serverAddress ) } func delete() { try! self.realm?.write { self.realm?.delete(self.localFiles) self.realm?.delete(self) } } }