diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 2c008a44..3234ca6a 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D0677428106D0C00B8F875 /* DataClasses.swift */; }; E99C8C932883A00F00E3279A /* LocalLibraryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */; }; E9D3815C289E0C9B0019EEED /* DownloadItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D3815B289E0C9B0019EEED /* DownloadItem.swift */; }; + E9D3815E28A2F00A0019EEED /* DownloadItemExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D3815D28A2F00A0019EEED /* DownloadItemExtensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -78,6 +79,7 @@ C4D0677428106D0C00B8F875 /* DataClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataClasses.swift; sourceTree = ""; }; E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalLibraryExtensions.swift; sourceTree = ""; }; E9D3815B289E0C9B0019EEED /* DownloadItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadItem.swift; sourceTree = ""; }; + E9D3815D28A2F00A0019EEED /* DownloadItemExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadItemExtensions.swift; sourceTree = ""; }; FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -144,6 +146,7 @@ C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */, E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */, E9D3815B289E0C9B0019EEED /* DownloadItem.swift */, + E9D3815D28A2F00A0019EEED /* DownloadItemExtensions.swift */, 3A90295E280968E700E1D427 /* PlaybackReport.swift */, 4DF74911287105C600AC7814 /* DeviceSettings.swift */, ); @@ -321,6 +324,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E9D3815E28A2F00A0019EEED /* DownloadItemExtensions.swift in Sources */, 3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */, 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, 3A90295F280968E700E1D427 /* PlaybackReport.swift in Sources */, diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index e3225579..105aefab 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -15,7 +15,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { migrationBlock: { migration, oldSchemaVersion in if (oldSchemaVersion < 1) { NSLog("Realm schema version was \(oldSchemaVersion)") - migration.enumerateObjects(ofType: DeviceSettings.rlmClassName()) { oldObject, newObject in + migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in newObject?["enableAltView"] = false } } @@ -23,36 +23,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) Realm.Configuration.defaultConfiguration = configuration - Realm.registerRealmables(DeviceSettings.self) - Realm.registerRealmables(ServerConnectionConfig.self) - Realm.registerRealmables(ServerConnectionConfigActiveIndex.self) - - // Data classes - Realm.registerRealmables(LibraryItem.self) - Realm.registerRealmables(MediaType.self) - Realm.registerRealmables(Metadata.self) - Realm.registerRealmables(PodcastEpisode.self) - Realm.registerRealmables(AudioFile.self) - Realm.registerRealmables(Author.self) - Realm.registerRealmables(Chapter.self) - Realm.registerRealmables(AudioTrack.self) - Realm.registerRealmables(FileMetadata.self) - Realm.registerRealmables(Library.self) - Realm.registerRealmables(Folder.self) - Realm.registerRealmables(LibraryFile.self) - Realm.registerRealmables(MediaProgress.self) - Realm.registerRealmables(PlaybackMetadata.self) - - // Local library - Realm.registerRealmables(LocalLibraryItem.self) - Realm.registerRealmables(LocalPodcastEpisode.self) - Realm.registerRealmables(LocalFile.self) - Realm.registerRealmables(LocalMediaProgress.self) - - // Download item - Realm.registerRealmables(DownloadItem.self) - Realm.registerRealmables(DownloadItemPart.self) - return true } diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index fdc3bab6..cc940804 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -40,7 +40,14 @@ public class AbsDatabase: CAPPlugin { id = "\(address)@\(username)".toBase64() } - let config = ServerConnectionConfig(id: id!, index: 1, name: name, address: address, userId: userId, username: username, token: token) + let config = ServerConnectionConfig() + config.id = id ?? "" + config.index = 1 + config.name = name + config.address = address + config.userId = userId + config.username = username + config.token = token Store.serverConfig = config call.resolve(convertServerConnectionConfigToJSON(config: config)) @@ -122,7 +129,11 @@ public class AbsDatabase: CAPPlugin { let enableAltView = call.getBool("enableAltView") ?? false let jumpBackwardsTime = call.getInt("jumpBackwardsTime") ?? 10 let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10 - let settings = DeviceSettings(disableAutoRewind: disableAutoRewind, enableAltView: enableAltView, jumpBackwardsTime: jumpBackwardsTime, jumpForwardTime: jumpForwardTime) + let settings = DeviceSettings() + settings.disableAutoRewind = disableAutoRewind + settings.enableAltView = enableAltView + settings.jumpBackwardsTime = jumpBackwardsTime + settings.jumpForwardTime = jumpForwardTime Database.shared.setDeviceSettings(deviceSettings: settings) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index a6d5e6a7..0b085de1 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -7,6 +7,7 @@ import Foundation import Capacitor +import RealmSwift @objc(AbsDownloader) public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { @@ -151,7 +152,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { var coverFile: String? // Assemble the local library item - let files = downloadItem.downloadItemParts.compactMap { part -> LocalFile? in + let files = downloadItem.downloadItemParts.enumerated().compactMap { _, part -> LocalFile? in if part.filename == "cover.jpg" { coverFile = part.destinationUri return nil @@ -193,7 +194,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { if let episodeId = episodeId { // Download a podcast episode guard libraryItem.mediaType == "podcast" else { throw LibraryItemDownloadError.libraryItemNotPodcast } - let episode = libraryItem.media.episodes?.first(where: { $0.id == episodeId }) + let episode = libraryItem.media?.episodes.enumerated().first(where: { $1.id == episodeId })?.element guard let episode = episode else { throw LibraryItemDownloadError.podcastEpisodeNotFound } try self.startLibraryItemDownload(libraryItem, episode: episode) } else { @@ -216,31 +217,31 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } private func startLibraryItemDownload(_ item: LibraryItem, episode: PodcastEpisode?) throws { - var tracks: [AudioTrack] + var tracks = List() var episodeId: String? // Handle the different media type downloads switch item.mediaType { case "book": - guard let bookTracks = item.media.tracks else { throw LibraryItemDownloadError.noTracks } - tracks = bookTracks + guard item.media?.tracks.count ?? 0 > 0 else { throw LibraryItemDownloadError.noTracks } + tracks = item.media?.tracks ?? tracks case "podcast": guard let episode = episode else { throw LibraryItemDownloadError.podcastEpisodeNotFound } guard let podcastTrack = episode.audioTrack else { throw LibraryItemDownloadError.noTracks } episodeId = episode.id - tracks = [podcastTrack] + tracks.append(podcastTrack) default: throw LibraryItemDownloadError.unknownMediaType } // 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) - }) + let downloadItem = DownloadItem(libraryItem: item, episodeId: episodeId, server: Store.serverConfig!) + for (i, track) in tracks.enumerated() { + downloadItem.downloadItemParts.append(try startLibraryItemTrackDownload(item: item, position: i, track: track)) + } // Also download the cover - if item.media.coverPath != nil && !item.media.coverPath!.isEmpty { + if item.media?.coverPath != nil && !(item.media?.coverPath!.isEmpty ?? true) { if let coverDownload = try? startLibraryItemCoverDownload(item: item) { downloadItem.downloadItemParts.append(coverDownload) } @@ -251,7 +252,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { // Start all the downloads for downloadItemPart in downloadItem.downloadItemParts { - downloadItemPart.task.resume() + downloadItemPart.task?.resume() } } @@ -268,7 +269,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) + let downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: nil) // Store the id on the task so the download item can be pulled from the database later task.taskDescription = downloadItemPart.id @@ -283,7 +284,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { let itemDirectory = try createLibraryItemFileDirectory(item: item) let localUrl = "\(itemDirectory)/\(filename)" - var downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil) + let downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil) let task = session.downloadTask(with: downloadItemPart.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/DataClasses.swift b/ios/App/Shared/models/DataClasses.swift index 426d6522..ff5d1211 100644 --- a/ios/App/Shared/models/DataClasses.swift +++ b/ios/App/Shared/models/DataClasses.swift @@ -7,94 +7,173 @@ import Foundation import CoreMedia -import Unrealm +import RealmSwift -struct LibraryItem: Realmable, Codable { - var id: String - var ino: String - var libraryId: String - var folderId: String - var path: String - var relPath: String - var isFile: Bool - var mtimeMs: Int - var ctimeMs: Int - var birthtimeMs: Int - var addedAt: Int - var updatedAt: Int - var lastScan: Int? - var scanVersion: String? - var isMissing: Bool - var isInvalid: Bool - var mediaType: String - var media: MediaType - var libraryFiles: [LibraryFile] - var userMediaProgress: MediaProgress? +class LibraryItem: Object, Codable { + @Persisted var id: String = "" + @Persisted var ino: String = "" + @Persisted var libraryId: String = "" + @Persisted var folderId: String = "" + @Persisted var path: String = "" + @Persisted var relPath: String = "" + @Persisted var isFile: Bool = true + @Persisted var mtimeMs: Int = 0 + @Persisted var ctimeMs: Int = 0 + @Persisted var birthtimeMs: Int = 0 + @Persisted var addedAt: Int = 0 + @Persisted var updatedAt: Int = 0 + @Persisted var lastScan: Int? + @Persisted var scanVersion: String? + @Persisted var isMissing: Bool = false + @Persisted var isInvalid: Bool = false + @Persisted var mediaType: String = "" + @Persisted var media: MediaType? + @Persisted var libraryFiles = List() + @Persisted var userMediaProgress: MediaProgress? - init() { - id = "" - ino = "" - libraryId = "" - folderId = "" - path = "" - relPath = "" - isFile = true - mtimeMs = 0 - ctimeMs = 0 - birthtimeMs = 0 - addedAt = 0 - updatedAt = 0 - isMissing = false - isInvalid = false - mediaType = "" - media = MediaType() - libraryFiles = [] + private enum CodingKeys : String, CodingKey { + case id, ino, libraryId, folderId, path, relPath, isFile, mtimeMs, ctimeMs, birthtimeMs, addedAt, updatedAt, lastScan, scanVersion, isMissing, isInvalid, mediaType, media, libraryFiles, userMediaProgress + } + + 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) + ino = try values.decode(String.self, forKey: .ino) + libraryId = try values.decode(String.self, forKey: .libraryId) + folderId = try values.decode(String.self, forKey: .folderId) + path = try values.decode(String.self, forKey: .path) + relPath = try values.decode(String.self, forKey: .relPath) + isFile = try values.decode(Bool.self, forKey: .isFile) + mtimeMs = try values.decode(Int.self, forKey: .mtimeMs) + ctimeMs = try values.decode(Int.self, forKey: .ctimeMs) + birthtimeMs = try values.decode(Int.self, forKey: .birthtimeMs) + addedAt = try values.decode(Int.self, forKey: .addedAt) + updatedAt = try values.decode(Int.self, forKey: .updatedAt) + lastScan = try? values.decode(Int.self, forKey: .lastScan) + scanVersion = try? values.decode(String.self, forKey: .scanVersion) + isMissing = try values.decode(Bool.self, forKey: .isMissing) + 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([LibraryFile].self, forKey: .libraryFiles) { + libraryFiles.append(objectsIn: files) + } + userMediaProgress = try? values.decode(MediaProgress.self, forKey: .userMediaProgress) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(ino, forKey: .ino) + try container.encode(libraryId, forKey: .libraryId) + try container.encode(folderId, forKey: .folderId) + try container.encode(path, forKey: .path) + try container.encode(relPath, forKey: .relPath) + try container.encode(isFile, forKey: .isFile) + try container.encode(mtimeMs, forKey: .mtimeMs) + try container.encode(ctimeMs, forKey: .ctimeMs) + try container.encode(birthtimeMs, forKey: .birthtimeMs) + try container.encode(addedAt, forKey: .addedAt) + try container.encode(updatedAt, forKey: .updatedAt) + try container.encode(lastScan, forKey: .lastScan) + try container.encode(scanVersion, forKey: .scanVersion) + try container.encode(isMissing, forKey: .isMissing) + try container.encode(isInvalid, forKey: .isInvalid) + try container.encode(mediaType, forKey: .mediaType) + try container.encode(media, forKey: .media) + try container.encode(Array(libraryFiles), forKey: .libraryFiles) + try container.encode(userMediaProgress, forKey: .userMediaProgress) } } -struct MediaType: Realmable, Codable { - var libraryItemId: String? - var metadata: Metadata - var coverPath: String? - var tags: [String]? - var audioFiles: [AudioFile]? - var chapters: [Chapter]? - var tracks: [AudioTrack]? - var size: Int? - var duration: Double? - var episodes: [PodcastEpisode]? - var autoDownloadEpisodes: Bool? +class MediaType: Object, Codable { + @Persisted var libraryItemId: String? + @Persisted var metadata: Metadata? + @Persisted var coverPath: String? + @Persisted var tags = List() + @Persisted var audioFiles = List() + @Persisted var chapters = List() + @Persisted var tracks = List() + @Persisted var size: Int? + @Persisted var duration: Double? + @Persisted var episodes = List() + @Persisted var autoDownloadEpisodes: Bool? - init() { - metadata = Metadata() + private enum CodingKeys : String, CodingKey { + case libraryItemId, metadata, coverPath, tags, audioFiles, chapters, tracks, size, duration, episodes, autoDownloadEpisodes + } + + override init() { + super.init() + } + + required init(from decoder: Decoder) throws { + super.init() + let values = try decoder.container(keyedBy: CodingKeys.self) + libraryItemId = try? values.decode(String.self, forKey: .libraryItemId) + metadata = try? values.decode(Metadata.self, forKey: .metadata) + coverPath = try? values.decode(String.self, forKey: .coverPath) + if let tagList = try? values.decode([String].self, forKey: .tags) { + tags.append(objectsIn: tagList) + } + if let fileList = try? values.decode([AudioFile].self, forKey: .audioFiles) { + audioFiles.append(objectsIn: fileList) + } + if let chapterList = try? values.decode([Chapter].self, forKey: .chapters) { + chapters.append(objectsIn: chapterList) + } + if let trackList = try? values.decode([AudioTrack].self, forKey: .tracks) { + tracks.append(objectsIn: trackList) + } + size = try? values.decode(Int.self, forKey: .size) + duration = try? values.decode(Double.self, forKey: .duration) + if let episodeList = try? values.decode([PodcastEpisode].self, forKey: .episodes) { + episodes.append(objectsIn: episodeList) + } + autoDownloadEpisodes = try? values.decode(Bool.self, forKey: .autoDownloadEpisodes) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(libraryItemId, forKey: .libraryItemId) + try container.encode(metadata, forKey: .metadata) + try container.encode(coverPath, forKey: .coverPath) + try container.encode(Array(tags), forKey: .tags) + try container.encode(Array(audioFiles), forKey: .audioFiles) + try container.encode(Array(chapters), forKey: .chapters) + try container.encode(Array(tracks), forKey: .tracks) + try container.encode(size, forKey: .size) + try container.encode(duration, forKey: .duration) + try container.encode(Array(episodes), forKey: .episodes) + try container.encode(autoDownloadEpisodes, forKey: .autoDownloadEpisodes) } } -struct Metadata: Realmable, Codable { - var title: String - var subtitle: String? - var authors: [Author]? - var narrators: [String]? - var genres: [String] - var publishedYear: String? - var publishedDate: String? - var publisher: String? - var desc: String? - var isbn: String? - var asin: String? - var language: String? - var explicit: Bool - var authorName: String? - var authorNameLF: String? - var narratorName: String? - var seriesName: String? - var feedUrl: String? - - init() { - title = "Unknown" - genres = [] - explicit = false - } +class Metadata: Object, Codable { + @Persisted var title: String = "Unknown" + @Persisted var subtitle: String? + @Persisted var authors = List() + @Persisted var narrators = List() + @Persisted var genres = List() + @Persisted var publishedYear: String? + @Persisted var publishedDate: String? + @Persisted var publisher: String? + @Persisted var desc: String? + @Persisted var isbn: String? + @Persisted var asin: String? + @Persisted var language: String? + @Persisted var explicit: Bool = false + @Persisted var authorName: String? + @Persisted var authorNameLF: String? + @Persisted var narratorName: String? + @Persisted var seriesName: String? + @Persisted var feedUrl: String? private enum CodingKeys : String, CodingKey { case title, @@ -116,30 +195,77 @@ struct Metadata: Realmable, Codable { seriesName, feedUrl } + + override init() { + super.init() + } + + required init(from decoder: Decoder) throws { + super.init() + let values = try decoder.container(keyedBy: CodingKeys.self) + title = try values.decode(String.self, forKey: .title) + subtitle = try? values.decode(String.self, forKey: .subtitle) + if let authorList = try? values.decode([Author].self, forKey: .authors) { + authors.append(objectsIn: authorList) + } + if let narratorList = try? values.decode([String].self, forKey: .narrators) { + narrators.append(objectsIn: narratorList) + } + if let genreList = try? values.decode([String].self, forKey: .genres) { + genres.append(objectsIn: genreList) + } + publishedYear = try? values.decode(String.self, forKey: .publishedYear) + publishedDate = try? values.decode(String.self, forKey: .publishedDate) + publisher = try? values.decode(String.self, forKey: .publisher) + desc = try? values.decode(String.self, forKey: .desc) + isbn = try? values.decode(String.self, forKey: .isbn) + asin = try? values.decode(String.self, forKey: .asin) + language = try? values.decode(String.self, forKey: .language) + explicit = try values.decode(Bool.self, forKey: .explicit) + authorName = try? values.decode(String.self, forKey: .authorName) + authorNameLF = try? values.decode(String.self, forKey: .authorNameLF) + narratorName = try? values.decode(String.self, forKey: .narratorName) + seriesName = try? values.decode(String.self, forKey: .seriesName) + feedUrl = try? values.decode(String.self, forKey: .feedUrl) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(title, forKey: .title) + try container.encode(subtitle, forKey: .subtitle) + try container.encode(Array(authors), forKey: .authors) + try container.encode(Array(narrators), forKey: .narrators) + try container.encode(Array(genres), forKey: .genres) + try container.encode(publishedYear, forKey: .publishedYear) + try container.encode(publishedDate, forKey: .publishedDate) + try container.encode(publisher, forKey: .publisher) + try container.encode(desc, forKey: .desc) + try container.encode(isbn, forKey: .isbn) + try container.encode(asin, forKey: .asin) + try container.encode(language, forKey: .language) + try container.encode(explicit, forKey: .explicit) + try container.encode(authorName, forKey: .authorName) + try container.encode(authorNameLF, forKey: .authorNameLF) + try container.encode(narratorName, forKey: .narratorName) + try container.encode(seriesName, forKey: .seriesName) + try container.encode(feedUrl, forKey: .feedUrl) + } } -struct PodcastEpisode: Realmable, Codable { - var id: String - var index: Int - var episode: String? - var episodeType: String? - var title: String - var subtitle: String? - var desc: String? - var audioFile: AudioFile? - var audioTrack: AudioTrack? - var duration: Double - var size: Int +class PodcastEpisode: Object, Codable { + @Persisted var id: String = "" + @Persisted var index: Int = 0 + @Persisted var episode: String? + @Persisted var episodeType: String? + @Persisted var title: String = "Unknown" + @Persisted var subtitle: String? + @Persisted var desc: String? + @Persisted var audioFile: AudioFile? + @Persisted var audioTrack: AudioTrack? + @Persisted var duration: Double = 0 + @Persisted var size: Int = 0 // var serverEpisodeId: String? - init() { - id = "" - index = 0 - title = "Unknown" - duration = 0 - size = 0 - } - private enum CodingKeys : String, CodingKey { case id, index, @@ -153,151 +279,322 @@ struct PodcastEpisode: Realmable, Codable { duration, size } + + // TODO: Encoding } -struct AudioFile: Realmable, Codable { - var index: Int - var ino: String - var metadata: FileMetadata +class AudioFile: Object, Codable { + @Persisted var index: Int = 0 + @Persisted var ino: String = "" + @Persisted var metadata: FileMetadata? - init() { - index = 0 - ino = "" - metadata = FileMetadata() + private enum CodingKeys : String, CodingKey { + case index, ino, metadata + } + + override init() { + super.init() + } + + required init(from decoder: Decoder) throws { + super.init() + let values = try decoder.container(keyedBy: CodingKeys.self) + index = try values.decode(Int.self, forKey: .index) + ino = try values.decode(String.self, forKey: .ino) + metadata = try? values.decode(FileMetadata.self, forKey: .metadata) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(index, forKey: .index) + try container.encode(ino, forKey: .ino) + try container.encode(metadata, forKey: .metadata) } } -struct Author: Realmable, Codable { - var id: String - var name: String - var coverPath: String? +class Author: Object, Codable { + @Persisted var id: String = "" + @Persisted var name: String = "Unknown" + @Persisted var coverPath: String? - init() { - id = "" - name = "Unknown" + private enum CodingKeys : String, CodingKey { + case id, name, coverPath + } + + 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) + name = try values.decode(String.self, forKey: .name) + coverPath = try? values.decode(String.self, forKey: .coverPath) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(coverPath, forKey: .coverPath) } } -struct Chapter: Realmable, Codable { - var id: Int - var start: Double - var end: Double - var title: String? +class Chapter: Object, Codable { + @Persisted var id: Int = 0 + @Persisted var start: Double = 0 + @Persisted var end: Double = 0 + @Persisted var title: String? - init() { - id = 0 - start = 0 - end = 0 + private enum CodingKeys : String, CodingKey { + case id, start, end, title + } + + override init() { + super.init() + } + + required init(from decoder: Decoder) throws { + super.init() + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(Int.self, forKey: .id) + start = try values.decode(Double.self, forKey: .start) + end = try values.decode(Double.self, forKey: .end) + title = try? values.decode(String.self, forKey: .title) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(start, forKey: .start) + try container.encode(end, forKey: .end) + try container.encode(title, forKey: .title) } } -struct AudioTrack: Realmable, Codable { - var index: Int? - var startOffset: Double? - var duration: Double - var title: String? - var contentUrl: String? - var mimeType: String - var metadata: FileMetadata? +class AudioTrack: Object, Codable { + @Persisted var index: Int? + @Persisted var startOffset: Double? + @Persisted var duration: Double = 0 + @Persisted var title: String? + @Persisted var contentUrl: String? + @Persisted var mimeType: String = "" + @Persisted var metadata: FileMetadata? // var isLocal: Bool // var localFileId: String? // var audioProbeResult: AudioProbeResult? Needed for local playback - var serverIndex: Int? + @Persisted var serverIndex: Int? - init() { - duration = 0 - mimeType = "" + private enum CodingKeys : String, CodingKey { + case index, startOffset, duration, title, contentUrl, mimeType, metadata, serverIndex + } + + override init() { + super.init() + } + + required init(from decoder: Decoder) throws { + super.init() + let values = try decoder.container(keyedBy: CodingKeys.self) + index = try? values.decode(Int.self, forKey: .index) + startOffset = try? values.decode(Double.self, forKey: .startOffset) + duration = try values.decode(Double.self, forKey: .duration) + title = try? values.decode(String.self, forKey: .title) + contentUrl = try? values.decode(String.self, forKey: .contentUrl) + mimeType = try values.decode(String.self, forKey: .mimeType) + metadata = try? values.decode(FileMetadata.self, forKey: .metadata) + serverIndex = try? values.decode(Int.self, forKey: .serverIndex) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(index, forKey: .index) + try container.encode(startOffset, forKey: .startOffset) + try container.encode(duration, forKey: .duration) + try container.encode(title, forKey: .title) + try container.encode(contentUrl, forKey: .contentUrl) + try container.encode(mimeType, forKey: .mimeType) + try container.encode(metadata, forKey: .metadata) + try container.encode(serverIndex, forKey: .serverIndex) } } -struct FileMetadata: Realmable, Codable { - var filename: String - var ext: String - var path: String - var relPath: String +class FileMetadata: Object, Codable { + @Persisted var filename: String = "" + @Persisted var ext: String = "" + @Persisted var path: String = "" + @Persisted var relPath: String = "" - init() { - filename = "" - ext = "" - path = "" - relPath = "" + private enum CodingKeys : String, CodingKey { + case filename, ext, path, relPath + } + + override init() { + super.init() + } + + required init(from decoder: Decoder) throws { + super.init() + let values = try decoder.container(keyedBy: CodingKeys.self) + filename = try values.decode(String.self, forKey: .filename) + ext = try values.decode(String.self, forKey: .ext) + path = try values.decode(String.self, forKey: .path) + relPath = try values.decode(String.self, forKey: .relPath) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(filename, forKey: .filename) + try container.encode(ext, forKey: .ext) + try container.encode(path, forKey: .path) + try container.encode(relPath, forKey: .relPath) } } -struct Library: Realmable, Codable { - var id: String - var name: String - var folders: [Folder] - var icon: String - var mediaType: String +class Library: Object, Codable { + @Persisted var id: String = "" + @Persisted var name: String = "Unknown" + @Persisted var folders = List() + @Persisted var icon: String = "" + @Persisted var mediaType: String = "" - init() { - id = "" - name = "Unknown" - folders = [] - icon = "" - mediaType = "" + private enum CodingKeys : String, CodingKey { + case id, name, folders, icon, mediaType + } + + 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) + name = try values.decode(String.self, forKey: .name) + if let folderList = try? values.decode([Folder].self, forKey: .folders) { + folders.append(objectsIn: folderList) + } + icon = try values.decode(String.self, forKey: .icon) + mediaType = try values.decode(String.self, forKey: .mediaType) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(folders, forKey: .folders) + try container.encode(icon, forKey: .icon) + try container.encode(mediaType, forKey: .mediaType) } } -struct Folder: Realmable, Codable { - var id: String - var fullPath: String +class Folder: Object, Codable { + @Persisted var id: String = "" + @Persisted var fullPath: String = "" - init() { - id = "" - fullPath = "" + private enum CodingKeys : String, CodingKey { + case id, fullPath + } + + 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) + fullPath = try values.decode(String.self, forKey: .fullPath) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(fullPath, forKey: .fullPath) } } -struct LibraryFile: Realmable, Codable { - var ino: String - var metadata: FileMetadata +class LibraryFile: Object, Codable { + @Persisted var ino: String = "" + @Persisted var metadata: FileMetadata? - init() { - ino = "" - metadata = FileMetadata() + private enum CodingKeys : String, CodingKey { + case ino, metadata + } + + override init() { + super.init() + } + + required init(from decoder: Decoder) throws { + super.init() + let values = try decoder.container(keyedBy: CodingKeys.self) + ino = try values.decode(String.self, forKey: .ino) + metadata = try values.decode(FileMetadata.self, forKey: .metadata) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(ino, forKey: .ino) + try container.encode(metadata, forKey: .metadata) } } -struct MediaProgress: Realmable, Codable { - var id: String - var libraryItemId: String - var episodeId: String? - var duration: Double - var progress: Double - var currentTime: Double - var isFinished: Bool - var lastUpdate: Int - var startedAt: Int - var finishedAt: Int? +class MediaProgress: Object, Codable { + @Persisted var id: String = "" + @Persisted var libraryItemId: String = "" + @Persisted var episodeId: String? + @Persisted var duration: Double = 0 + @Persisted var progress: Double = 0 + @Persisted var currentTime: Double = 0 + @Persisted var isFinished: Bool = false + @Persisted var lastUpdate: Int = 0 + @Persisted var startedAt: Int = 0 + @Persisted var finishedAt: Int? - init() { - id = "" - libraryItemId = "" - duration = 0 - progress = 0 - currentTime = 0 - isFinished = false - lastUpdate = 0 - startedAt = 0 + private enum CodingKeys : String, CodingKey { + case id, libraryItemId, episodeId, duration, progress, currentTime, isFinished, lastUpdate, startedAt, finishedAt + } + + 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) + libraryItemId = try values.decode(String.self, forKey: .libraryItemId) + episodeId = try? values.decode(String.self, forKey: .episodeId) + duration = try values.decode(Double.self, forKey: .duration) + progress = try values.decode(Double.self, forKey: .progress) + currentTime = try values.decode(Double.self, forKey: .currentTime) + isFinished = try values.decode(Bool.self, forKey: .isFinished) + lastUpdate = try values.decode(Int.self, forKey: .lastUpdate) + startedAt = try values.decode(Int.self, forKey: .startedAt) + finishedAt = try? values.decode(Int.self, forKey: .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) } } -struct PlaybackMetadata: Realmable, Codable { - var duration: Double - var currentTime: Double - var playerState: PlayerState - - init() { - duration = 0 - currentTime = 0 - playerState = PlayerState.IDLE - } - - static func ignoredProperties() -> [String] { - return ["playerState"] - } +class PlaybackMetadata: Codable { + var duration: Double = 0 + var currentTime: Double = 0 + var playerState: PlayerState = PlayerState.IDLE } enum PlayerState: Codable { diff --git a/ios/App/Shared/models/DeviceSettings.swift b/ios/App/Shared/models/DeviceSettings.swift index 16e1fd54..df6a7ab1 100644 --- a/ios/App/Shared/models/DeviceSettings.swift +++ b/ios/App/Shared/models/DeviceSettings.swift @@ -7,13 +7,12 @@ import Foundation import RealmSwift -import Unrealm -struct DeviceSettings: Realmable { - var disableAutoRewind: Bool = false - var enableAltView: Bool = false - var jumpBackwardsTime: Int = 10 - var jumpForwardTime: Int = 10 +class DeviceSettings: Object { + @Persisted var disableAutoRewind: Bool = false + @Persisted var enableAltView: Bool = false + @Persisted var jumpBackwardsTime: Int = 10 + @Persisted var jumpForwardTime: Int = 10 } func getDefaultDeviceSettings() -> DeviceSettings { diff --git a/ios/App/Shared/models/DownloadItem.swift b/ios/App/Shared/models/DownloadItem.swift index a9ce6c6e..9e4eb221 100644 --- a/ios/App/Shared/models/DownloadItem.swift +++ b/ios/App/Shared/models/DownloadItem.swift @@ -6,121 +6,102 @@ // import Foundation -import Unrealm +import RealmSwift -struct DownloadItem: Realmable, Codable { - var id: String? - var libraryItemId: String? - var episodeId: String? - var userMediaProgress: MediaProgress? - var serverConnectionConfigId: String? - var serverAddress: String? - var serverUserId: String? - var mediaType: String? - var itemTitle: String? - var media: MediaType? - var downloadItemParts: [DownloadItemPart] = [] - - static func primaryKey() -> String? { - return "id" - } - - static func indexedProperties() -> [String] { - ["libraryItemId"] - } +class DownloadItem: Object, Codable { + @Persisted(primaryKey: true) var id: String? + @Persisted(indexed: true) var libraryItemId: String? + @Persisted var episodeId: String? + @Persisted var userMediaProgress: MediaProgress? + @Persisted var serverConnectionConfigId: String? + @Persisted var serverAddress: String? + @Persisted var serverUserId: String? + @Persisted var mediaType: String? + @Persisted var itemTitle: String? + @Persisted var media: MediaType? + @Persisted var downloadItemParts = List() private enum CodingKeys : String, CodingKey { case id, libraryItemId, episodeId, serverConnectionConfigId, serverAddress, serverUserId, mediaType, itemTitle, downloadItemParts } -} - -extension DownloadItem { - init(libraryItem: LibraryItem, episodeId: String?, server: ServerConnectionConfig) { - self.id = libraryItem.id - self.libraryItemId = libraryItem.id - self.userMediaProgress = libraryItem.userMediaProgress - self.serverConnectionConfigId = server.id - self.serverAddress = server.address - self.serverUserId = server.userId - self.mediaType = libraryItem.mediaType - self.itemTitle = libraryItem.media.metadata.title - self.media = libraryItem.media + + override init() { + super.init() + } + + required init(from decoder: Decoder) throws { + super.init() - if let episodeId = episodeId { - self.id! += "-\(episodeId)" - self.episodeId = episodeId + 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) + serverConnectionConfigId = try? values.decode(String.self, forKey: .serverConnectionConfigId) + serverAddress = try? values.decode(String.self, forKey: .serverAddress) + serverUserId = try? values.decode(String.self, forKey: .serverUserId) + mediaType = try? values.decode(String.self, forKey: .mediaType) + itemTitle = try? values.decode(String.self, forKey: .itemTitle) + if let parts = try? values.decode([DownloadItemPart].self, forKey: .downloadItemParts) { + downloadItemParts.append(objectsIn: parts) } } - func isDoneDownloading() -> Bool { - self.downloadItemParts.allSatisfy({ $0.completed }) - } - - func didDownloadSuccessfully() -> Bool { - self.downloadItemParts.allSatisfy({ $0.failed == false }) + 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(serverConnectionConfigId, forKey: .serverConnectionConfigId) + try container.encode(serverAddress, forKey: .serverAddress) + try container.encode(serverUserId, forKey: .serverUserId) + try container.encode(mediaType, forKey: .mediaType) + try container.encode(itemTitle, forKey: .itemTitle) + try container.encode(Array(downloadItemParts), forKey: .downloadItemParts) } } -struct DownloadItemPart: Realmable, Codable { - var id: String = UUID().uuidString - var filename: String? - var itemTitle: String? - var serverPath: String? - var audioTrack: AudioTrack? - var episode: PodcastEpisode? - var completed: Bool = false - 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 +class DownloadItemPart: Object, Codable { + @Persisted(primaryKey: true) var id: String = UUID().uuidString + @Persisted var filename: String? + @Persisted var itemTitle: String? + @Persisted var serverPath: String? + @Persisted var audioTrack: AudioTrack? + @Persisted var episode: PodcastEpisode? + @Persisted var completed: Bool = false + @Persisted var moved: Bool = false + @Persisted var failed: Bool = false + @Persisted var uri: String? + @Persisted var destinationUri: String? + @Persisted var progress: Double = 0 var task: URLSessionDownloadTask! - static func primaryKey() -> String? { - return "id" - } - - static func ignoredProperties() -> [String] { - ["task"] - } - private enum CodingKeys : String, CodingKey { case id, filename, itemTitle, completed, moved, failed, progress } -} - -extension DownloadItemPart { - init(filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) { - self.filename = filename - self.itemTitle = itemTitle - self.serverPath = serverPath - self.audioTrack = audioTrack - self.episode = episode - - let config = Store.serverConfig! - var downloadUrl = "\(config.address)\(serverPath)?token=\(config.token)" - if (serverPath.hasSuffix("/cover")) { - downloadUrl += "&format=jpeg" // For cover images force to jpeg - } - self.uri = downloadUrl - self.destinationUri = destination + + override init() { + super.init() } - func mimeType() -> String? { - audioTrack?.mimeType ?? episode?.audioTrack?.mimeType + required 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) + 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) + } + + 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(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) } } diff --git a/ios/App/Shared/models/DownloadItemExtensions.swift b/ios/App/Shared/models/DownloadItemExtensions.swift new file mode 100644 index 00000000..d875bb72 --- /dev/null +++ b/ios/App/Shared/models/DownloadItemExtensions.swift @@ -0,0 +1,77 @@ +// +// DownloadItemExtensions.swift +// App +// +// Created by Ron Heft on 8/9/22. +// + +import Foundation + +extension DownloadItem { + convenience init(libraryItem: LibraryItem, episodeId: String?, server: ServerConnectionConfig) { + self.init() + + self.id = libraryItem.id + self.libraryItemId = libraryItem.id + self.userMediaProgress = libraryItem.userMediaProgress + self.serverConnectionConfigId = server.id + self.serverAddress = server.address + self.serverUserId = server.userId + self.mediaType = libraryItem.mediaType + self.itemTitle = libraryItem.media?.metadata?.title + self.media = libraryItem.media + + if let episodeId = episodeId { + self.id! += "-\(episodeId)" + self.episodeId = episodeId + } + } + + func isDoneDownloading() -> Bool { + self.downloadItemParts.allSatisfy({ $0.completed }) + } + + func didDownloadSuccessfully() -> Bool { + self.downloadItemParts.allSatisfy({ $0.failed == false }) + } +} + +extension DownloadItemPart { + convenience init(filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) { + self.init() + + self.filename = filename + self.itemTitle = itemTitle + self.serverPath = serverPath + self.audioTrack = audioTrack + self.episode = episode + + let config = Store.serverConfig! + var downloadUrl = "\(config.address)\(serverPath)?token=\(config.token)" + if (serverPath.hasSuffix("/cover")) { + downloadUrl += "&format=jpeg" // For cover images force to jpeg + } + self.uri = downloadUrl + self.destinationUri = destination + } + + var downloadURL: URL? { + if let uri = self.uri { + return URL(string: uri) + } else { + return nil + } + } + + var destinationURL: URL? { + if let destinationUri = self.destinationUri { + return AbsDownloader.downloadsDirectory.appendingPathComponent(destinationUri) + } else { + return nil + } + } + + func mimeType() -> String? { + audioTrack?.mimeType ?? episode?.audioTrack?.mimeType + } +} diff --git a/ios/App/Shared/models/LocalLibrary.swift b/ios/App/Shared/models/LocalLibrary.swift index c2e5841e..8ffd6a12 100644 --- a/ios/App/Shared/models/LocalLibrary.swift +++ b/ios/App/Shared/models/LocalLibrary.swift @@ -6,76 +6,52 @@ // import Foundation -import Unrealm +import RealmSwift -struct LocalLibraryItem: Realmable, Codable { - var id: String = "local_\(UUID().uuidString)" - var basePath: String = "" - dynamic var _contentUrl: String? - var isInvalid: Bool = false - var mediaType: String = "" - var media: MediaType? - var localFiles: [LocalFile] = [] - 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" - } +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? private enum CodingKeys : String, CodingKey { case id, basePath, contentUrl, isInvalid, mediaType, media, localFiles, coverContentUrl, isLocal, serverConnectionConfigId, serverAddress, serverUserId, libraryItemId } - init() {} + override init() { + super.init() + } - init(from decoder: Decoder) throws { + 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) 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) + media = try? values.decode(MediaType.self, forKey: .media) + if let files = try? values.decode([LocalFile].self, forKey: .localFiles) { + localFiles.append(objectsIn: files) + } + _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 { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) @@ -84,7 +60,7 @@ struct LocalLibraryItem: Realmable, Codable { 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(Array(localFiles), forKey: .localFiles) try container.encode(coverContentUrl, forKey: .coverContentUrl) try container.encode(isLocal, forKey: .isLocal) try container.encode(serverConnectionConfigId, forKey: .serverConnectionConfigId) @@ -94,54 +70,65 @@ 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? +class LocalPodcastEpisode: Object, Codable { + @Persisted(primaryKey: true) var id: String = UUID().uuidString + @Persisted var index: Int = 0 + @Persisted var episode: String? + @Persisted var episodeType: String? + @Persisted var title: String = "Unknown" + @Persisted var subtitle: String? + @Persisted var desc: String? + @Persisted var audioFile: AudioFile? + @Persisted var audioTrack: AudioTrack? + @Persisted var duration: Double = 0 + @Persisted var size: Int = 0 + @Persisted(indexed: true) var serverEpisodeId: String? - static func primaryKey() -> String? { - return "id" + private enum CodingKeys : String, CodingKey { + case id + } + + 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) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .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 mimeType: String? - var size: Int = 0 - - static func primaryKey() -> String? { - return "id" - } +class LocalFile: Object, Codable { + @Persisted(primaryKey: true) var id: String = UUID().uuidString + @Persisted var filename: String? + @Persisted var contentUrl: String = "" + @Persisted var mimeType: String? + @Persisted var size: Int = 0 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) + override init() { + super.init() } + required 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) @@ -153,25 +140,69 @@ struct LocalFile: Realmable, Codable { } } -struct LocalMediaProgress: Realmable, Codable { - var id: String = "" - var localLibraryItemId: String = "" - var localEpisodeId: String? - var duration: Double = 0 - var progress: Double = 0 - var currentTime: Double = 0 - var isFinished: Bool = false - var lastUpdate: Int = 0 - var startedAt: Int = 0 - var finishedAt: Int? +class LocalMediaProgress: Object, Codable { + @Persisted(primaryKey: true) var id: String = "" + @Persisted(indexed: true) var localLibraryItemId: String = "" + @Persisted(indexed: true) var localEpisodeId: String? + @Persisted var duration: Double = 0 + @Persisted var progress: Double = 0 + @Persisted var currentTime: Double = 0 + @Persisted var isFinished: Bool = false + @Persisted var lastUpdate: Int = 0 + @Persisted var startedAt: Int = 0 + @Persisted var finishedAt: Int? // For local lib items from server to support server sync - var serverConnectionConfigId: String? - var serverAddress: String? - var serverUserId: String? - var libraryItemId: String? - var episodeId: String? + @Persisted var serverConnectionConfigId: String? + @Persisted var serverAddress: String? + @Persisted var serverUserId: String? + @Persisted(indexed: true) var libraryItemId: String? + @Persisted(indexed: true) var episodeId: String? - static func primaryKey() -> String? { - return "id" + private enum CodingKeys : String, CodingKey { + case id, localLibraryItemId, localEpisodeId, duration, progress, currentTime, isFinished, lastUpdate, startedAt, finishedAt, serverConnectionConfigId, serverAddress, serverUserId, libraryItemId, episodeId + } + + 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) + localLibraryItemId = try values.decode(String.self, forKey: .localLibraryItemId) + localEpisodeId = try? values.decode(String.self, forKey: .localEpisodeId) + duration = try values.decode(Double.self, forKey: .duration) + progress = try values.decode(Double.self, forKey: .progress) + currentTime = try values.decode(Double.self, forKey: .currentTime) + isFinished = try values.decode(Bool.self, forKey: .isFinished) + lastUpdate = try values.decode(Int.self, forKey: .lastUpdate) + startedAt = try values.decode(Int.self, forKey: .startedAt) + finishedAt = try? values.decode(Int.self, forKey: .finishedAt) + 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) + episodeId = try? values.decode(String.self, forKey: .episodeId) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(localLibraryItemId, forKey: .localLibraryItemId) + try container.encode(localEpisodeId, forKey: .localEpisodeId) + 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) + try container.encode(serverConnectionConfigId, forKey: .serverConnectionConfigId) + try container.encode(serverAddress, forKey: .serverAddress) + try container.encode(serverUserId, forKey: .serverUserId) + try container.encode(libraryItemId, forKey: .libraryItemId) + try container.encode(episodeId, forKey: .episodeId) } } diff --git a/ios/App/Shared/models/LocalLibraryExtensions.swift b/ios/App/Shared/models/LocalLibraryExtensions.swift index 1111498e..68dfdf76 100644 --- a/ios/App/Shared/models/LocalLibraryExtensions.swift +++ b/ios/App/Shared/models/LocalLibraryExtensions.swift @@ -8,12 +8,13 @@ import Foundation extension LocalLibraryItem { - init(_ item: LibraryItem, localUrl: String, server: ServerConnectionConfig, files: [LocalFile], coverPath: String?) { + convenience init(_ item: LibraryItem, localUrl: String, server: ServerConnectionConfig, files: [LocalFile], coverPath: String?) { self.init() + self.contentUrl = localUrl self.mediaType = item.mediaType self.media = item.media - self.localFiles = files + self.localFiles.append(objectsIn: files) self.coverContentUrl = coverPath self.libraryItemId = item.id self.serverConnectionConfigId = server.id @@ -21,9 +22,35 @@ extension LocalLibraryItem { self.serverUserId = server.userId } + 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 + } + } + } + func getDuration() -> Double { var total = 0.0 - self.media?.tracks?.forEach { track in total += track.duration } + self.media?.tracks.enumerated().forEach { _, track in total += track.duration } return total } @@ -70,8 +97,9 @@ extension LocalLibraryItem { } extension LocalFile { - init(_ libraryItemId: String, _ filename: String, _ mimeType: String, _ localUrl: String, fileSize: Int) { + convenience init(_ libraryItemId: String, _ filename: String, _ mimeType: String, _ localUrl: String, fileSize: Int) { self.init() + self.id = "\(libraryItemId)_\(filename.toBase64())" self.filename = filename self.mimeType = mimeType @@ -79,6 +107,10 @@ extension LocalFile { self.size = fileSize } + var absolutePath: String { + return AbsDownloader.downloadsDirectory.appendingPathComponent(self.contentUrl).absoluteString + } + func isAudioFile() -> Bool { switch self.mimeType { case "application/octet-stream", @@ -91,7 +123,9 @@ extension LocalFile { } extension LocalMediaProgress { - init(localLibraryItem: LocalLibraryItem, episode: LocalPodcastEpisode?, progress: MediaProgress) { + convenience init(localLibraryItem: LocalLibraryItem, episode: LocalPodcastEpisode?, progress: MediaProgress) { + self.init() + self.id = localLibraryItem.id self.localLibraryItemId = localLibraryItem.id self.libraryItemId = localLibraryItem.libraryItemId diff --git a/ios/App/Shared/models/ServerConnectionConfig.swift b/ios/App/Shared/models/ServerConnectionConfig.swift index 3f579fed..be841da1 100644 --- a/ios/App/Shared/models/ServerConnectionConfig.swift +++ b/ios/App/Shared/models/ServerConnectionConfig.swift @@ -7,33 +7,20 @@ import Foundation import RealmSwift -import Unrealm -struct ServerConnectionConfig: Realmable { - var id: String = UUID().uuidString - var index: Int = 1 - var name: String = "" - var address: String = "" - var userId: String = "" - var username: String = "" - var token: String = "" - - static func primaryKey() -> String? { - return "id" - } - - static func indexedProperties() -> [String] { - return ["index"] - } +class ServerConnectionConfig: Object { + @Persisted(primaryKey: true) var id: String = UUID().uuidString + @Persisted(indexed: true) var index: Int = 1 + @Persisted var name: String = "" + @Persisted var address: String = "" + @Persisted var userId: String = "" + @Persisted var username: String = "" + @Persisted var token: String = "" } -struct ServerConnectionConfigActiveIndex: Realmable { +class ServerConnectionConfigActiveIndex: Object { // This could overflow, but you really would have to try - var index: Int? - - static func primaryKey() -> String? { - return "index" - } + @Persisted(primaryKey: true) var index: Int? } func convertServerConnectionConfigToJSON(config: ServerConnectionConfig) -> Dictionary { diff --git a/ios/App/Shared/util/Database.swift b/ios/App/Shared/util/Database.swift index 64ffc05e..52533ef4 100644 --- a/ios/App/Shared/util/Database.swift +++ b/ios/App/Shared/util/Database.swift @@ -16,7 +16,7 @@ class Database { private init() {} public func setServerConnectionConfig(config: ServerConnectionConfig) { - var config = config + let config = config let realm = try! Realm() let existing: ServerConnectionConfig? = realm.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id) @@ -74,9 +74,17 @@ class Database { let realm = try! Realm() do { try realm.write { - var existing = realm.objects(ServerConnectionConfigActiveIndex.self).last ?? ServerConnectionConfigActiveIndex(index: index) - existing.index = index - realm.add(existing, update: .modified) + let existing = realm.objects(ServerConnectionConfigActiveIndex.self).last + + if ( existing?.index != index ) { + if let existing = existing { + realm.delete(existing) + } + + let activeConfig = ServerConnectionConfigActiveIndex() + activeConfig.index = index + realm.add(activeConfig) + } } } catch(let exception) { NSLog("failed to save server config active index")