diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 3234ca6a..61ef079a 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -35,11 +35,28 @@ 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; }; - C4B265F5285A5A6600E1B5C3 /* LocalLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */; }; - 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 */; }; + E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504528AC1A3900C746DD /* LibraryItem.swift */; }; + E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504728AC1A7A00C746DD /* MediaType.swift */; }; + E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504928AC1AA600C746DD /* Metadata.swift */; }; + E9D5504C28AC1AE000C746DD /* PodcastEpisode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504B28AC1AE000C746DD /* PodcastEpisode.swift */; }; + E9D5504E28AC1B0700C746DD /* AudioFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504D28AC1B0700C746DD /* AudioFile.swift */; }; + E9D5505028AC1B3E00C746DD /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504F28AC1B3E00C746DD /* Author.swift */; }; + E9D5505228AC1B5D00C746DD /* Chapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5505128AC1B5D00C746DD /* Chapter.swift */; }; + E9D5505428AC1B7900C746DD /* AudioTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5505328AC1B7900C746DD /* AudioTrack.swift */; }; + E9D5505628AC1BFA00C746DD /* FileMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5505528AC1BFA00C746DD /* FileMetadata.swift */; }; + E9D5505828AC1C1A00C746DD /* Library.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5505728AC1C1A00C746DD /* Library.swift */; }; + E9D5505A28AC1C4500C746DD /* Folder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5505928AC1C4500C746DD /* Folder.swift */; }; + E9D5505C28AC1C6200C746DD /* LibraryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5505B28AC1C6200C746DD /* LibraryFile.swift */; }; + E9D5505E28AC1C8500C746DD /* MediaProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5505D28AC1C8500C746DD /* MediaProgress.swift */; }; + E9D5506028AC1CA900C746DD /* PlaybackMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5505F28AC1CA900C746DD /* PlaybackMetadata.swift */; }; + E9D5506228AC1CC900C746DD /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5506128AC1CC900C746DD /* PlayerState.swift */; }; + E9D5506628AC1D7300C746DD /* LocalLibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5506528AC1D7300C746DD /* LocalLibraryItem.swift */; }; + E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5506728AC1DC300C746DD /* LocalPodcastEpisode.swift */; }; + E9D5506A28AC1DF100C746DD /* LocalFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5506928AC1DF100C746DD /* LocalFile.swift */; }; + E9D5506C28AC1E2100C746DD /* LocalMediaProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5506B28AC1E2100C746DD /* LocalMediaProgress.swift */; }; + E9D5506F28AC1E8E00C746DD /* DownloadItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5506E28AC1E8E00C746DD /* DownloadItem.swift */; }; + E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507028AC1EC700C746DD /* DownloadItemPart.swift */; }; + E9D5507328AC218300C746DD /* DaoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507228AC218300C746DD /* DaoExtensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -75,11 +92,28 @@ 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; - C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalLibrary.swift; sourceTree = ""; }; - 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 = ""; }; + E9D5504528AC1A3900C746DD /* LibraryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryItem.swift; sourceTree = ""; }; + E9D5504728AC1A7A00C746DD /* MediaType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaType.swift; sourceTree = ""; }; + E9D5504928AC1AA600C746DD /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = ""; }; + E9D5504B28AC1AE000C746DD /* PodcastEpisode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PodcastEpisode.swift; sourceTree = ""; }; + E9D5504D28AC1B0700C746DD /* AudioFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFile.swift; sourceTree = ""; }; + E9D5504F28AC1B3E00C746DD /* Author.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = ""; }; + E9D5505128AC1B5D00C746DD /* Chapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chapter.swift; sourceTree = ""; }; + E9D5505328AC1B7900C746DD /* AudioTrack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioTrack.swift; sourceTree = ""; }; + E9D5505528AC1BFA00C746DD /* FileMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMetadata.swift; sourceTree = ""; }; + E9D5505728AC1C1A00C746DD /* Library.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Library.swift; sourceTree = ""; }; + E9D5505928AC1C4500C746DD /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = ""; }; + E9D5505B28AC1C6200C746DD /* LibraryFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFile.swift; sourceTree = ""; }; + E9D5505D28AC1C8500C746DD /* MediaProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProgress.swift; sourceTree = ""; }; + E9D5505F28AC1CA900C746DD /* PlaybackMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackMetadata.swift; sourceTree = ""; }; + E9D5506128AC1CC900C746DD /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = ""; }; + E9D5506528AC1D7300C746DD /* LocalLibraryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalLibraryItem.swift; sourceTree = ""; }; + E9D5506728AC1DC300C746DD /* LocalPodcastEpisode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPodcastEpisode.swift; sourceTree = ""; }; + E9D5506928AC1DF100C746DD /* LocalFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFile.swift; sourceTree = ""; }; + E9D5506B28AC1E2100C746DD /* LocalMediaProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMediaProgress.swift; sourceTree = ""; }; + E9D5506E28AC1E8E00C746DD /* DownloadItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadItem.swift; sourceTree = ""; }; + E9D5507028AC1EC700C746DD /* DownloadItemPart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadItemPart.swift; sourceTree = ""; }; + E9D5507228AC218300C746DD /* DaoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaoExtensions.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 */ @@ -142,13 +176,13 @@ children = ( 3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */, 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */, - C4D0677428106D0C00B8F875 /* DataClasses.swift */, - C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */, - E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */, - E9D3815B289E0C9B0019EEED /* DownloadItem.swift */, - E9D3815D28A2F00A0019EEED /* DownloadItemExtensions.swift */, 3A90295E280968E700E1D427 /* PlaybackReport.swift */, + E9D5505F28AC1CA900C746DD /* PlaybackMetadata.swift */, + E9D5506128AC1CC900C746DD /* PlayerState.swift */, 4DF74911287105C600AC7814 /* DeviceSettings.swift */, + E9D5506328AC1D3F00C746DD /* server */, + E9D5506428AC1D5800C746DD /* local */, + E9D5506D28AC1E7400C746DD /* download */, ); path = models; sourceTree = ""; @@ -162,6 +196,7 @@ 3AF1970B2806E2590096F747 /* ApiClient.swift */, 3AB34052280829BF0039308B /* Extensions.swift */, 3AB34054280832720039308B /* PlayerEvents.swift */, + E9D5507228AC218300C746DD /* DaoExtensions.swift */, ); path = util; sourceTree = ""; @@ -211,6 +246,46 @@ name = Pods; sourceTree = ""; }; + E9D5506328AC1D3F00C746DD /* server */ = { + isa = PBXGroup; + children = ( + E9D5504528AC1A3900C746DD /* LibraryItem.swift */, + E9D5504728AC1A7A00C746DD /* MediaType.swift */, + E9D5504928AC1AA600C746DD /* Metadata.swift */, + E9D5504B28AC1AE000C746DD /* PodcastEpisode.swift */, + E9D5504D28AC1B0700C746DD /* AudioFile.swift */, + E9D5504F28AC1B3E00C746DD /* Author.swift */, + E9D5505128AC1B5D00C746DD /* Chapter.swift */, + E9D5505328AC1B7900C746DD /* AudioTrack.swift */, + E9D5505528AC1BFA00C746DD /* FileMetadata.swift */, + E9D5505728AC1C1A00C746DD /* Library.swift */, + E9D5505928AC1C4500C746DD /* Folder.swift */, + E9D5505B28AC1C6200C746DD /* LibraryFile.swift */, + E9D5505D28AC1C8500C746DD /* MediaProgress.swift */, + ); + path = server; + sourceTree = ""; + }; + E9D5506428AC1D5800C746DD /* local */ = { + isa = PBXGroup; + children = ( + E9D5506528AC1D7300C746DD /* LocalLibraryItem.swift */, + E9D5506728AC1DC300C746DD /* LocalPodcastEpisode.swift */, + E9D5506928AC1DF100C746DD /* LocalFile.swift */, + E9D5506B28AC1E2100C746DD /* LocalMediaProgress.swift */, + ); + path = local; + sourceTree = ""; + }; + E9D5506D28AC1E7400C746DD /* download */ = { + isa = PBXGroup; + children = ( + E9D5506E28AC1E8E00C746DD /* DownloadItem.swift */, + E9D5507028AC1EC700C746DD /* DownloadItemPart.swift */, + ); + path = download; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -324,32 +399,49 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E9D3815E28A2F00A0019EEED /* DownloadItemExtensions.swift in Sources */, + E9D5507328AC218300C746DD /* DaoExtensions.swift in Sources */, + E9D5506228AC1CC900C746DD /* PlayerState.swift in Sources */, 3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */, 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, 3A90295F280968E700E1D427 /* PlaybackReport.swift in Sources */, - C4B265F5285A5A6600E1B5C3 /* LocalLibrary.swift in Sources */, + E9D5505A28AC1C4500C746DD /* Folder.swift in Sources */, 3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */, + E9D5506628AC1D7300C746DD /* LocalLibraryItem.swift in Sources */, + E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */, 3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */, 3AD4FCED28044E6C006DB301 /* Store.swift in Sources */, 4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */, + E9D5504C28AC1AE000C746DD /* PodcastEpisode.swift in Sources */, + E9D5506A28AC1DF100C746DD /* LocalFile.swift in Sources */, 3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */, + E9D5506F28AC1E8E00C746DD /* DownloadItem.swift in Sources */, 3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */, + E9D5505E28AC1C8500C746DD /* MediaProgress.swift in Sources */, 3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */, + E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */, 4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */, 3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */, 3AB34053280829BF0039308B /* Extensions.swift in Sources */, - E9D3815C289E0C9B0019EEED /* DownloadItem.swift in Sources */, + E9D5505828AC1C1A00C746DD /* Library.swift in Sources */, 3AD4FCEB280443DD006DB301 /* Database.swift in Sources */, 3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */, 4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */, + E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */, + E9D5505228AC1B5D00C746DD /* Chapter.swift in Sources */, + E9D5506028AC1CA900C746DD /* PlaybackMetadata.swift in Sources */, + E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */, + E9D5504E28AC1B0700C746DD /* AudioFile.swift in Sources */, + E9D5505428AC1B7900C746DD /* AudioTrack.swift in Sources */, + E9D5505C28AC1C6200C746DD /* LibraryFile.swift in Sources */, 4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */, + E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */, 3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */, + E9D5505028AC1B3E00C746DD /* Author.swift in Sources */, 3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */, - C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */, 4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */, + E9D5505628AC1BFA00C746DD /* FileMetadata.swift in Sources */, 3AB34055280832720039308B /* PlayerEvents.swift in Sources */, - E99C8C932883A00F00E3279A /* LocalLibraryExtensions.swift in Sources */, + E9D5506C28AC1E2100C746DD /* LocalMediaProgress.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index fdc2ea46..99d44fe4 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. let configuration = Realm.Configuration( - schemaVersion: 1, + schemaVersion: 2, migrationBlock: { migration, oldSchemaVersion in if (oldSchemaVersion < 1) { NSLog("Realm schema version was \(oldSchemaVersion)") diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index edf9884c..a5dbffa2 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -137,10 +137,10 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { self.progressStatusQueue.async(flags: .barrier) { self.downloadItemProgress.removeValue(forKey: item.id!) } - if let item = Database.shared.getDownloadItem(downloadItemId: item.id!) { - Database.shared.removeDownloadItem(item) - } self.handleDownloadTaskCompleteFromDownloadItem(item) + if let item = Database.shared.getDownloadItem(downloadItemId: item.id!) { + item.delete() + } } // Emit status for active downloads @@ -241,19 +241,19 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } private func startLibraryItemDownload(_ item: LibraryItem, episode: PodcastEpisode?) throws { - var tracks = List() + let tracks = List() var episodeId: String? // Handle the different media type downloads switch item.mediaType { case "book": guard item.media?.tracks.count ?? 0 > 0 else { throw LibraryItemDownloadError.noTracks } - tracks = item.media?.tracks ?? tracks + item.media?.tracks.forEach { t in tracks.append(AudioTrack.detachCopy(of: t)!) } case "podcast": guard let episode = episode else { throw LibraryItemDownloadError.podcastEpisodeNotFound } guard let podcastTrack = episode.audioTrack else { throw LibraryItemDownloadError.noTracks } episodeId = episode.id - tracks.append(podcastTrack) + tracks.append(AudioTrack.detachCopy(of: podcastTrack)!) default: throw LibraryItemDownloadError.unknownMediaType } diff --git a/ios/App/App/plugins/AbsFileSystem.swift b/ios/App/App/plugins/AbsFileSystem.swift index d5b4e4f6..928eed64 100644 --- a/ios/App/App/plugins/AbsFileSystem.swift +++ b/ios/App/App/plugins/AbsFileSystem.swift @@ -70,7 +70,7 @@ public class AbsFileSystem: CAPPlugin { do { if let localLibraryItemId = localLibraryItemId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) { try FileManager.default.removeItem(at: item.contentDirectory!) - Database.shared.removeLocalLibraryItem(localLibraryItemId: localLibraryItemId) + item.delete() success = true } } catch { @@ -88,25 +88,26 @@ public class AbsFileSystem: CAPPlugin { NSLog("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET")") var success = false - do { - if let localLibraryItemId = localLibraryItemId, let trackLocalFileId = trackLocalFileId, let 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, let media = item.media { - if let episodeIndex = media.episodes.firstIndex(where: { $0.audioTrack?.localFileId == trackLocalFileId }) { - media.episodes.remove(at: episodeIndex) + if let localLibraryItemId = localLibraryItemId, let trackLocalFileId = trackLocalFileId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) { + item.update { + do { + if let fileIndex = item.localFiles.firstIndex(where: { $0.id == trackLocalFileId }) { + try FileManager.default.removeItem(at: item.localFiles[fileIndex].contentPath) + item.realm?.delete(item.localFiles[fileIndex]) + if item.isPodcast, let media = item.media { + if let episodeIndex = media.episodes.firstIndex(where: { $0.audioTrack?.localFileId == trackLocalFileId }) { + media.episodes.remove(at: episodeIndex) + } + item.media = media } - item.media = media + call.resolve(try item.asDictionary()) + success = true } - Database.shared.saveLocalLibraryItem(localLibraryItem: item) - call.resolve(try item.asDictionary()) - success = true + } catch { + NSLog("Failed to delete \(error)") + success = false } } - } catch { - NSLog("Failed to delete \(error)") - success = false } if !success { diff --git a/ios/App/Shared/models/DataClasses.swift b/ios/App/Shared/models/DataClasses.swift deleted file mode 100644 index 64eea432..00000000 --- a/ios/App/Shared/models/DataClasses.swift +++ /dev/null @@ -1,651 +0,0 @@ -// -// DataClasses.swift -// App -// -// Created by benonymity on 4/20/22. -// - -import Foundation -import CoreMedia -import RealmSwift - -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? - - 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) - } -} - -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? - - 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) - } -} - -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? - - var authorDisplayName: String { self.authorName ?? "Unknown" } - - private enum CodingKeys : String, CodingKey { - case title, - subtitle, - authors, - narrators, - genres, - publishedYear, - publishedDate, - publisher, - desc = "description", // Fixes a collision with the base Swift object's field "description" - isbn, - asin, - language, - explicit, - authorName, - authorNameLF, - narratorName, - 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) - } -} - -class PodcastEpisode: Object, Codable { - @Persisted var id: String = "" - @Persisted var index: Int? - @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? - @Persisted var size: Int? - var serverEpisodeId: String { self.id } - - private enum CodingKeys : String, CodingKey { - case id, - index, - episode, - episodeType, - title, - subtitle, - desc = "description", // Fixes a collision with the base Swift object's field "description" - audioFile, - audioTrack, - duration, - size, - serverEpisodeId - } - - override init() {} - - required 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) - } -} - -class AudioFile: Object, Codable { - @Persisted var index: Int? - @Persisted var ino: String = "" - @Persisted var 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) - } -} - -class Author: Object, Codable { - @Persisted var id: String = "" - @Persisted var name: String = "Unknown" - @Persisted var coverPath: String? - - 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) - } -} - -class Chapter: Object, Codable { - @Persisted var id: Int = 0 - @Persisted var start: Double = 0 - @Persisted var end: Double = 0 - @Persisted var title: String? - - 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) - } -} - -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 localFileId: String? - // var audioProbeResult: AudioProbeResult? Needed for local playback - @Persisted var serverIndex: Int? - - 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) - } - - 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 - } - - func getLocalFile() -> LocalFile? { - guard let localFileId = self.localFileId else { return nil } - return Database.shared.getLocalFile(localFileId: localFileId) - } -} - -class FileMetadata: Object, Codable { - @Persisted var filename: String = "" - @Persisted var ext: String = "" - @Persisted var path: String = "" - @Persisted var relPath: String = "" - - 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) - } -} - -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 = "" - - 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) - } -} - -class Folder: Object, Codable { - @Persisted var id: String = "" - @Persisted var fullPath: String = "" - - 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) - } -} - -class LibraryFile: Object, Codable { - @Persisted var ino: String = "" - @Persisted var 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) - } -} - -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? - - 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 { - 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.doubleOrStringDecoder(key: .duration) - progress = try values.doubleOrStringDecoder(key: .progress) - currentTime = try values.doubleOrStringDecoder(key: .currentTime) - isFinished = try values.decode(Bool.self, forKey: .isFinished) - lastUpdate = try values.intOrStringDecoder(key: .lastUpdate) - startedAt = try values.intOrStringDecoder(key: .startedAt) - finishedAt = try? values.intOrStringDecoder(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) - } -} - -class PlaybackMetadata: Codable { - var duration: Double = 0 - var currentTime: Double = 0 - var playerState: PlayerState = PlayerState.IDLE -} - -enum PlayerState: Codable { - case IDLE - case BUFFERING - case READY - case ENDED -} diff --git a/ios/App/Shared/models/DownloadItemExtensions.swift b/ios/App/Shared/models/DownloadItemExtensions.swift deleted file mode 100644 index fa71bbd7..00000000 --- a/ios/App/Shared/models/DownloadItemExtensions.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// 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.itemDownloadFolder(path: 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 deleted file mode 100644 index cea39dd0..00000000 --- a/ios/App/Shared/models/LocalLibrary.swift +++ /dev/null @@ -1,238 +0,0 @@ -// -// LocalLibrary.swift -// App -// -// Created by benonymity on 6/15/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) - } -} - -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? - - 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) - } -} - -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 - - var contentUrl: String { AbsDownloader.itemDownloadFolder(path: _contentUrl)!.absoluteString } - var contentPath: URL { AbsDownloader.itemDownloadFolder(path: _contentUrl)! } - var basePath: String? { self.filename } - - private enum CodingKeys : String, CodingKey { - case id, filename, contentUrl, mimeType, size, basePath - } - - 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) - mimeType = try? values.decode(String.self, forKey: .mimeType) - size = try values.decode(Int.self, forKey: .size) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(filename, forKey: .filename) - try container.encode(contentUrl, forKey: .contentUrl) - try container.encode(mimeType, forKey: .mimeType) - try container.encode(size, forKey: .size) - try container.encode(basePath, forKey: .basePath) - } -} - -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 - @Persisted var serverConnectionConfigId: String? - @Persisted var serverAddress: String? - @Persisted var serverUserId: String? - @Persisted(indexed: true) var libraryItemId: String? - @Persisted(indexed: true) var episodeId: String? - - var progressPercent: Int { Int(self.progress * 100) } - - 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 deleted file mode 100644 index 71b49f44..00000000 --- a/ios/App/Shared/models/LocalLibraryExtensions.swift +++ /dev/null @@ -1,226 +0,0 @@ -// -// LocalLibraryExtensions.swift -// App -// -// Created by Ron Heft on 7/16/22. -// - -import Foundation -import RealmSwift - -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 = 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 = self.media?.metadata - let chapters = self.media?.chapters ?? List() - let authorName = mediaMetadata?.authorDisplayName - - let audioTracks = List() - if let episode = episode, let track = episode.audioTrack { - audioTracks.append(track) - } else if let tracks = self.media?.tracks { - audioTracks.append(objectsIn: tracks) - } - - let dateNow = Date().timeIntervalSince1970 - return PlaybackSession( - id: sessionId, - userId: self.serverUserId, - libraryItemId: self.libraryItemId, - episodeId: episode?.serverEpisodeId, - mediaType: self.mediaType, - chapters: chapters, - displayTitle: mediaMetadata?.title, - displayAuthor: authorName, - coverPath: self.coverContentUrl, - duration: self.getDuration(), - playMethod: PlayMethod.local.rawValue, - startedAt: dateNow, - updatedAt: 0, - timeListening: 0.0, - audioTracks: audioTracks, - currentTime: mediaProgress?.currentTime ?? 0.0, - libraryItem: nil, - localLibraryItem: self, - serverConnectionConfigId: self.serverConnectionConfigId, - serverAddress: self.serverAddress - ) - } -} - -extension LocalFile { - 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 - self._contentUrl = localUrl - self.size = fileSize - } - - var absolutePath: String { - return AbsDownloader.itemDownloadFolder(path: self._contentUrl)?.absoluteString ?? "" - } - - func isAudioFile() -> Bool { - switch self.mimeType { - case "application/octet-stream", - "video/mp4": - return true - default: - return self.mimeType?.starts(with: "audio") ?? false - } - } -} - -extension LocalMediaProgress { - convenience init(localLibraryItem: LocalLibraryItem, episode: PodcastEpisode?) { - self.init() - - self.id = localLibraryItem.id - self.localLibraryItemId = localLibraryItem.id - self.libraryItemId = localLibraryItem.libraryItemId - - self.serverAddress = localLibraryItem.serverAddress - self.serverUserId = localLibraryItem.serverUserId - self.serverConnectionConfigId = localLibraryItem.serverConnectionConfigId - - self.duration = localLibraryItem.getDuration() - self.progress = 0.0 - self.currentTime = 0.0 - self.isFinished = false - self.lastUpdate = Int(Date().timeIntervalSince1970) - self.startedAt = 0 - self.finishedAt = nil - - if let episode = episode { - self.id += "-\(episode.id)" - self.episodeId = episode.id - self.duration = episode.duration ?? 0.0 - } - } - - convenience init(localLibraryItem: LocalLibraryItem, episode: PodcastEpisode?, progress: MediaProgress) { - self.init(localLibraryItem: localLibraryItem, episode: episode) - self.duration = progress.duration - self.progress = progress.progress - self.currentTime = progress.currentTime - self.isFinished = progress.isFinished - self.lastUpdate = progress.lastUpdate - self.startedAt = progress.startedAt - self.finishedAt = progress.finishedAt - } - - func updateIsFinished(_ finished: Bool) { - try! Realm().write { - if self.isFinished != finished { - self.progress = finished ? 1.0 : 0.0 - } - - if self.startedAt == 0 && finished { - self.startedAt = Int(Date().timeIntervalSince1970) - } - - self.isFinished = finished - self.lastUpdate = Int(Date().timeIntervalSince1970) - self.finishedAt = finished ? lastUpdate : nil - } - } - - func updateFromPlaybackSession(_ playbackSession: PlaybackSession) { - try! Realm().write { - self.currentTime = playbackSession.currentTime - self.progress = playbackSession.progress - self.lastUpdate = Int(Date().timeIntervalSince1970) - self.isFinished = playbackSession.progress >= 100.0 - self.finishedAt = self.isFinished ? self.lastUpdate : nil - } - } - - func updateFromServerMediaProgress(_ serverMediaProgress: MediaProgress) { - try! Realm().write { - self.isFinished = serverMediaProgress.isFinished - self.progress = serverMediaProgress.progress - self.currentTime = serverMediaProgress.currentTime - self.duration = serverMediaProgress.duration - self.lastUpdate = serverMediaProgress.lastUpdate - self.finishedAt = serverMediaProgress.finishedAt - self.startedAt = serverMediaProgress.startedAt - } - } - - static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) -> LocalMediaProgress? { - if let localMediaProgressId = localMediaProgressId { - return Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) - } else if let localLibraryItemId = localLibraryItemId { - guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil } - let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId) - return LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode) - } else { - return nil - } - } -} diff --git a/ios/App/Shared/models/PlaybackMetadata.swift b/ios/App/Shared/models/PlaybackMetadata.swift new file mode 100644 index 00000000..65c41560 --- /dev/null +++ b/ios/App/Shared/models/PlaybackMetadata.swift @@ -0,0 +1,14 @@ +// +// PlaybackMetadata.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation + +class PlaybackMetadata: Codable { + var duration: Double = 0 + var currentTime: Double = 0 + var playerState: PlayerState = PlayerState.IDLE +} diff --git a/ios/App/Shared/models/PlaybackSession.swift b/ios/App/Shared/models/PlaybackSession.swift index eadee179..9fadc1d3 100644 --- a/ios/App/Shared/models/PlaybackSession.swift +++ b/ios/App/Shared/models/PlaybackSession.swift @@ -33,11 +33,13 @@ class PlaybackSession: Object, Codable { var isLocal: Bool { self.localLibraryItem != nil } - var localMediaProgressId: String { - if let episodeId = episodeId { - return "\(localLibraryItem!.id)-\(episodeId)" + var localMediaProgressId: String? { + if let localLibraryItem = localLibraryItem, let episodeId = episodeId { + return "\(localLibraryItem.id)-\(episodeId)" + } else if let localLibraryItem = localLibraryItem { + return localLibraryItem.id } else { - return localLibraryItem!.id + return nil } } diff --git a/ios/App/Shared/models/PlayerState.swift b/ios/App/Shared/models/PlayerState.swift new file mode 100644 index 00000000..02599f17 --- /dev/null +++ b/ios/App/Shared/models/PlayerState.swift @@ -0,0 +1,15 @@ +// +// PlayerState.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation + +enum PlayerState: Codable { + case IDLE + case BUFFERING + case READY + case ENDED +} diff --git a/ios/App/Shared/models/DownloadItem.swift b/ios/App/Shared/models/download/DownloadItem.swift similarity index 58% rename from ios/App/Shared/models/DownloadItem.swift rename to ios/App/Shared/models/download/DownloadItem.swift index cea63f3f..446cadcc 100644 --- a/ios/App/Shared/models/DownloadItem.swift +++ b/ios/App/Shared/models/download/DownloadItem.swift @@ -2,7 +2,7 @@ // DownloadItem.swift // App // -// Created by Ron Heft on 8/5/22. +// Created by Ron Heft on 8/16/22. // import Foundation @@ -60,47 +60,38 @@ class DownloadItem: Object, Codable { } } -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 - - private enum CodingKeys : String, CodingKey { - case id, filename, itemTitle, completed, moved, failed, progress +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 + } } - override init() { - super.init() + func isDoneDownloading() -> Bool { + self.downloadItemParts.allSatisfy({ $0.completed }) } - 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 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(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) + func delete() { + try! self.realm?.write { + self.realm?.delete(self.downloadItemParts) + self.realm?.delete(self) + } } } diff --git a/ios/App/Shared/models/download/DownloadItemPart.swift b/ios/App/Shared/models/download/DownloadItemPart.swift new file mode 100644 index 00000000..4b9acc68 --- /dev/null +++ b/ios/App/Shared/models/download/DownloadItemPart.swift @@ -0,0 +1,95 @@ +// +// DownloadItemPart.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class DownloadItemPart: Object, Codable { + @Persisted(primaryKey: true) var id = "" + @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 + + private enum CodingKeys : String, CodingKey { + case id, filename, itemTitle, completed, moved, failed, progress + } + + 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) + 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) + } +} + +extension DownloadItemPart { + convenience init(filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) { + self.init() + + self.id = destination.toBase64() + self.filename = filename + self.itemTitle = itemTitle + self.serverPath = serverPath + self.audioTrack = AudioTrack.detachCopy(of: audioTrack) + self.episode = PodcastEpisode.detachCopy(of: 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.itemDownloadFolder(path: destinationUri) + } else { + return nil + } + } + + func mimeType() -> String? { + audioTrack?.mimeType ?? episode?.audioTrack?.mimeType + } +} diff --git a/ios/App/Shared/models/local/LocalFile.swift b/ios/App/Shared/models/local/LocalFile.swift new file mode 100644 index 00000000..eb077339 --- /dev/null +++ b/ios/App/Shared/models/local/LocalFile.swift @@ -0,0 +1,73 @@ +// +// LocalFile.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +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 + + var contentUrl: String { AbsDownloader.itemDownloadFolder(path: _contentUrl)!.absoluteString } + var contentPath: URL { AbsDownloader.itemDownloadFolder(path: _contentUrl)! } + var basePath: String? { self.filename } + + private enum CodingKeys : String, CodingKey { + case id, filename, contentUrl, mimeType, size, basePath + } + + 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) + mimeType = try? values.decode(String.self, forKey: .mimeType) + size = try values.decode(Int.self, forKey: .size) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(filename, forKey: .filename) + try container.encode(contentUrl, forKey: .contentUrl) + try container.encode(mimeType, forKey: .mimeType) + try container.encode(size, forKey: .size) + try container.encode(basePath, forKey: .basePath) + } +} + +extension LocalFile { + 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 + self._contentUrl = localUrl + self.size = fileSize + } + + var absolutePath: String { + return AbsDownloader.itemDownloadFolder(path: self._contentUrl)?.absoluteString ?? "" + } + + func isAudioFile() -> Bool { + switch self.mimeType { + case "application/octet-stream", + "video/mp4": + return true + default: + return self.mimeType?.starts(with: "audio") ?? false + } + } +} diff --git a/ios/App/Shared/models/local/LocalLibraryItem.swift b/ios/App/Shared/models/local/LocalLibraryItem.swift new file mode 100644 index 00000000..d74ebd5d --- /dev/null +++ b/ios/App/Shared/models/local/LocalLibraryItem.swift @@ -0,0 +1,206 @@ +// +// 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, + chapters: chapters, + displayTitle: mediaMetadata?.title, + displayAuthor: authorName, + coverPath: self.coverContentUrl, + duration: self.getDuration(), + playMethod: PlayMethod.local.rawValue, + startedAt: dateNow, + updatedAt: 0, + 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) + } + } +} diff --git a/ios/App/Shared/models/local/LocalMediaProgress.swift b/ios/App/Shared/models/local/LocalMediaProgress.swift new file mode 100644 index 00000000..add69842 --- /dev/null +++ b/ios/App/Shared/models/local/LocalMediaProgress.swift @@ -0,0 +1,168 @@ +// +// LocalMediaProgress.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +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 + @Persisted var serverConnectionConfigId: String? + @Persisted var serverAddress: String? + @Persisted var serverUserId: String? + @Persisted(indexed: true) var libraryItemId: String? + @Persisted(indexed: true) var episodeId: String? + + var progressPercent: Int { Int(self.progress * 100) } + + 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) + } +} + +extension LocalMediaProgress { + convenience init(localLibraryItem: LocalLibraryItem, episode: PodcastEpisode?) { + self.init() + + self.id = localLibraryItem.id + self.localLibraryItemId = localLibraryItem.id + self.libraryItemId = localLibraryItem.libraryItemId + + self.serverAddress = localLibraryItem.serverAddress + self.serverUserId = localLibraryItem.serverUserId + self.serverConnectionConfigId = localLibraryItem.serverConnectionConfigId + + self.duration = localLibraryItem.getDuration() + self.progress = 0.0 + self.currentTime = 0.0 + self.isFinished = false + self.lastUpdate = Int(Date().timeIntervalSince1970) + self.startedAt = 0 + self.finishedAt = nil + + if let episode = episode { + self.id += "-\(episode.id)" + self.episodeId = episode.id + self.duration = episode.duration ?? 0.0 + } + } + + convenience init(localLibraryItem: LocalLibraryItem, episode: PodcastEpisode?, progress: MediaProgress) { + self.init(localLibraryItem: localLibraryItem, episode: episode) + self.duration = progress.duration + self.progress = progress.progress + self.currentTime = progress.currentTime + self.isFinished = progress.isFinished + self.lastUpdate = progress.lastUpdate + self.startedAt = progress.startedAt + self.finishedAt = progress.finishedAt + } + + func updateIsFinished(_ finished: Bool) { + try! Realm().write { + if self.isFinished != finished { + self.progress = finished ? 1.0 : 0.0 + } + + if self.startedAt == 0 && finished { + self.startedAt = Int(Date().timeIntervalSince1970) + } + + self.isFinished = finished + self.lastUpdate = Int(Date().timeIntervalSince1970) + self.finishedAt = finished ? lastUpdate : nil + } + } + + func updateFromPlaybackSession(_ playbackSession: PlaybackSession) { + try! Realm().write { + self.currentTime = playbackSession.currentTime + self.progress = playbackSession.progress + self.lastUpdate = Int(Date().timeIntervalSince1970) + self.isFinished = playbackSession.progress >= 100.0 + self.finishedAt = self.isFinished ? self.lastUpdate : nil + } + } + + func updateFromServerMediaProgress(_ serverMediaProgress: MediaProgress) { + try! Realm().write { + self.isFinished = serverMediaProgress.isFinished + self.progress = serverMediaProgress.progress + self.currentTime = serverMediaProgress.currentTime + self.duration = serverMediaProgress.duration + self.lastUpdate = serverMediaProgress.lastUpdate + self.finishedAt = serverMediaProgress.finishedAt + self.startedAt = serverMediaProgress.startedAt + } + } + + static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) -> LocalMediaProgress? { + if let localMediaProgressId = localMediaProgressId { + return Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) + } else if let localLibraryItemId = localLibraryItemId { + guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil } + let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId) + return LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode) + } else { + return nil + } + } +} + diff --git a/ios/App/Shared/models/local/LocalPodcastEpisode.swift b/ios/App/Shared/models/local/LocalPodcastEpisode.swift new file mode 100644 index 00000000..28fec0a4 --- /dev/null +++ b/ios/App/Shared/models/local/LocalPodcastEpisode.swift @@ -0,0 +1,46 @@ +// +// LocalPodcastEpisode.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +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? + + 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) + } +} + + diff --git a/ios/App/Shared/models/server/AudioFile.swift b/ios/App/Shared/models/server/AudioFile.swift new file mode 100644 index 00000000..8911bfb4 --- /dev/null +++ b/ios/App/Shared/models/server/AudioFile.swift @@ -0,0 +1,38 @@ +// +// AudioFile.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class AudioFile: EmbeddedObject, Codable { + @Persisted var index: Int? + @Persisted var ino: String = "" + @Persisted var 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) + } +} diff --git a/ios/App/Shared/models/server/AudioTrack.swift b/ios/App/Shared/models/server/AudioTrack.swift new file mode 100644 index 00000000..c0d0ab88 --- /dev/null +++ b/ios/App/Shared/models/server/AudioTrack.swift @@ -0,0 +1,72 @@ +// +// AudioTrack.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class AudioTrack: EmbeddedObject, 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? + @Persisted var localFileId: String? + @Persisted var serverIndex: Int? + + private enum CodingKeys : String, CodingKey { + case index, startOffset, duration, title, contentUrl, mimeType, metadata, localFileId, 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) + localFileId = try! values.decodeIfPresent(String.self, forKey: .localFileId) + 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(localFileId, forKey: .localFileId) + try container.encode(serverIndex, forKey: .serverIndex) + } +} + +extension AudioTrack { + 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 + } + + func getLocalFile() -> LocalFile? { + guard let localFileId = self.localFileId else { return nil } + return Database.shared.getLocalFile(localFileId: localFileId) + } +} diff --git a/ios/App/Shared/models/server/Author.swift b/ios/App/Shared/models/server/Author.swift new file mode 100644 index 00000000..ba7d5ebf --- /dev/null +++ b/ios/App/Shared/models/server/Author.swift @@ -0,0 +1,38 @@ +// +// Author.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class Author: EmbeddedObject, Codable { + @Persisted var id: String = "" + @Persisted var name: String = "Unknown" + @Persisted var coverPath: String? + + 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) + } +} diff --git a/ios/App/Shared/models/server/Chapter.swift b/ios/App/Shared/models/server/Chapter.swift new file mode 100644 index 00000000..2e4d1e02 --- /dev/null +++ b/ios/App/Shared/models/server/Chapter.swift @@ -0,0 +1,41 @@ +// +// Chapter.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class Chapter: EmbeddedObject, Codable { + @Persisted var id: Int = 0 + @Persisted var start: Double = 0 + @Persisted var end: Double = 0 + @Persisted var title: String? + + 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) + } +} diff --git a/ios/App/Shared/models/server/FileMetadata.swift b/ios/App/Shared/models/server/FileMetadata.swift new file mode 100644 index 00000000..a3c62170 --- /dev/null +++ b/ios/App/Shared/models/server/FileMetadata.swift @@ -0,0 +1,41 @@ +// +// FileMetadata.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class FileMetadata: EmbeddedObject, Codable { + @Persisted var filename: String = "" + @Persisted var ext: String = "" + @Persisted var path: String = "" + @Persisted var relPath: String = "" + + 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) + } +} diff --git a/ios/App/Shared/models/server/Folder.swift b/ios/App/Shared/models/server/Folder.swift new file mode 100644 index 00000000..a1440c40 --- /dev/null +++ b/ios/App/Shared/models/server/Folder.swift @@ -0,0 +1,35 @@ +// +// Folder.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class Folder: EmbeddedObject, Codable { + @Persisted var id: String = "" + @Persisted var fullPath: String = "" + + 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) + } +} diff --git a/ios/App/Shared/models/server/Library.swift b/ios/App/Shared/models/server/Library.swift new file mode 100644 index 00000000..f569de8b --- /dev/null +++ b/ios/App/Shared/models/server/Library.swift @@ -0,0 +1,46 @@ +// +// Library.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class Library: EmbeddedObject, Codable { + @Persisted var id: String = "" + @Persisted var name: String = "Unknown" + @Persisted var folders = List() + @Persisted var icon: String = "" + @Persisted var mediaType: String = "" + + 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) + } +} diff --git a/ios/App/Shared/models/server/LibraryFile.swift b/ios/App/Shared/models/server/LibraryFile.swift new file mode 100644 index 00000000..9373765b --- /dev/null +++ b/ios/App/Shared/models/server/LibraryFile.swift @@ -0,0 +1,35 @@ +// +// LibraryFile.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class LibraryFile: EmbeddedObject, Codable { + @Persisted var ino: String = "" + @Persisted var 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) + } +} diff --git a/ios/App/Shared/models/server/LibraryItem.swift b/ios/App/Shared/models/server/LibraryItem.swift new file mode 100644 index 00000000..b9213f01 --- /dev/null +++ b/ios/App/Shared/models/server/LibraryItem.swift @@ -0,0 +1,92 @@ +// +// LibraryItem.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +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? + + 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) + } +} diff --git a/ios/App/Shared/models/server/MediaProgress.swift b/ios/App/Shared/models/server/MediaProgress.swift new file mode 100644 index 00000000..b481a5b4 --- /dev/null +++ b/ios/App/Shared/models/server/MediaProgress.swift @@ -0,0 +1,58 @@ +// +// MediaProgress.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class MediaProgress: EmbeddedObject, 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? + + 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 { + 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.doubleOrStringDecoder(key: .duration) + progress = try values.doubleOrStringDecoder(key: .progress) + currentTime = try values.doubleOrStringDecoder(key: .currentTime) + isFinished = try values.decode(Bool.self, forKey: .isFinished) + lastUpdate = try values.intOrStringDecoder(key: .lastUpdate) + startedAt = try values.intOrStringDecoder(key: .startedAt) + finishedAt = try? values.intOrStringDecoder(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) + } +} diff --git a/ios/App/Shared/models/server/MediaType.swift b/ios/App/Shared/models/server/MediaType.swift new file mode 100644 index 00000000..db7e04d7 --- /dev/null +++ b/ios/App/Shared/models/server/MediaType.swift @@ -0,0 +1,72 @@ +// +// MediaType.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class MediaType: EmbeddedObject, 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? + + 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) + } +} diff --git a/ios/App/Shared/models/server/Metadata.swift b/ios/App/Shared/models/server/Metadata.swift new file mode 100644 index 00000000..9b33cad9 --- /dev/null +++ b/ios/App/Shared/models/server/Metadata.swift @@ -0,0 +1,108 @@ +// +// Metadata.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class Metadata: EmbeddedObject, 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? + + var authorDisplayName: String { self.authorName ?? "Unknown" } + + private enum CodingKeys : String, CodingKey { + case title, + subtitle, + authors, + narrators, + genres, + publishedYear, + publishedDate, + publisher, + desc = "description", // Fixes a collision with the base Swift object's field "description" + isbn, + asin, + language, + explicit, + authorName, + authorNameLF, + narratorName, + 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) + } +} diff --git a/ios/App/Shared/models/server/PodcastEpisode.swift b/ios/App/Shared/models/server/PodcastEpisode.swift new file mode 100644 index 00000000..c480c07d --- /dev/null +++ b/ios/App/Shared/models/server/PodcastEpisode.swift @@ -0,0 +1,72 @@ +// +// PodcastEpisode.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +class PodcastEpisode: EmbeddedObject, Codable { + @Persisted var id: String = "" + @Persisted var index: Int? + @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? + @Persisted var size: Int? + var serverEpisodeId: String { self.id } + + private enum CodingKeys : String, CodingKey { + case id, + index, + episode, + episodeType, + title, + subtitle, + desc = "description", // Fixes a collision with the base Swift object's field "description" + audioFile, + audioTrack, + duration, + size, + serverEpisodeId + } + + override init() {} + + required 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) + } +} diff --git a/ios/App/Shared/util/DaoExtensions.swift b/ios/App/Shared/util/DaoExtensions.swift new file mode 100644 index 00000000..f1d61c3a --- /dev/null +++ b/ios/App/Shared/util/DaoExtensions.swift @@ -0,0 +1,45 @@ +// +// DaoExtensions.swift +// App +// +// Created by Ron Heft on 8/16/22. +// + +import Foundation +import RealmSwift + +extension Object { + func save() { + let realm = try! Realm() + try! realm.write { + realm.add(self, update: .modified) + } + } + + func update(handler: () -> Void) { + try! self.realm?.write { + handler() + } + } +} + +extension EmbeddedObject { + // Required to disassociate from Realm when copying into local objects + static func detachCopy(of object: T?) -> T? { + guard let object = object else { return nil } + let json = try! JSONEncoder().encode(object) + return try! JSONDecoder().decode(T.self, from: json) + } +} + +protocol Deletable { + func delete() +} + +extension Deletable where Self: Object { + func delete() { + try! self.realm?.write { + self.realm?.delete(self) + } + } +} diff --git a/ios/App/Shared/util/Database.swift b/ios/App/Shared/util/Database.swift index 12716c22..396682ae 100644 --- a/ios/App/Shared/util/Database.swift +++ b/ios/App/Shared/util/Database.swift @@ -131,14 +131,6 @@ 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 getLocalFile(localFileId: String) -> LocalFile? { let realm = try! Realm() return realm.object(ofType: LocalFile.self, forPrimaryKey: localFileId) @@ -164,11 +156,6 @@ class Database { return try! realm.write { realm.add(downloadItem, update: .modified) } } - public func removeDownloadItem(_ downloadItem: DownloadItem) { - let realm = try! Realm() - return try! realm.write { realm.delete(downloadItem) } - } - public func getDeviceSettings() -> DeviceSettings { let realm = try! Realm() return realm.objects(DeviceSettings.self).first ?? getDefaultDeviceSettings() diff --git a/ios/App/Shared/util/Extensions.swift b/ios/App/Shared/util/Extensions.swift index a3924c2e..c76af20c 100644 --- a/ios/App/Shared/util/Extensions.swift +++ b/ios/App/Shared/util/Extensions.swift @@ -94,19 +94,3 @@ extension URL { return attributes?[.creationDate] as? Date } } - -// MARK: - DAO Methods -extension Object { - func save() { - let realm = try! Realm() - try! realm.write { - realm.add(self, update: .modified) - } - } - - func update(handler: () -> Void?) { - try! self.realm?.write { - handler() - } - } -}