Rewrite of object model to use Unrealm

This addresses issues with JSON serialization
This commit is contained in:
ronaldheft 2022-08-01 09:40:28 -04:00
parent 0b46a9c9b1
commit f6c43e479d
12 changed files with 297 additions and 662 deletions

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -15,7 +15,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
migrationBlock: { migration, oldSchemaVersion in migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) { if (oldSchemaVersion < 1) {
NSLog("Realm schema version was \(oldSchemaVersion)") NSLog("Realm schema version was \(oldSchemaVersion)")
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in migration.enumerateObjects(ofType: DeviceSettings.rlmClassName()) { oldObject, newObject in
newObject?["enableAltView"] = false newObject?["enableAltView"] = false
} }
} }
@ -23,6 +23,32 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
) )
Realm.Configuration.defaultConfiguration = configuration Realm.Configuration.defaultConfiguration = configuration
Realm.registerRealmables(DeviceSettings.self)
Realm.registerRealmables(ServerConnectionConfig.self)
Realm.registerRealmables(ServerConnectionConfigActiveIndex.self)
// Data classes
Realm.registerRealmables(LibraryItem.self)
Realm.registerRealmables(MediaType.self)
Realm.registerRealmables(Metadata.self)
Realm.registerRealmables(PodcastEpisode.self)
Realm.registerRealmables(AudioFile.self)
Realm.registerRealmables(Author.self)
Realm.registerRealmables(Chapter.self)
Realm.registerRealmables(AudioTrack.self)
Realm.registerRealmables(FileMetadata.self)
Realm.registerRealmables(Library.self)
Realm.registerRealmables(Folder.self)
Realm.registerRealmables(LibraryFile.self)
Realm.registerRealmables(MediaProgress.self)
Realm.registerRealmables(PlaybackMetadata.self)
// Local library
Realm.registerRealmables(LocalLibraryItem.self)
Realm.registerRealmables(LocalPodcastEpisode.self)
Realm.registerRealmables(LocalFile.self)
Realm.registerRealmables(LocalMediaProgress.self)
return true return true
} }

View file

@ -35,18 +35,12 @@ public class AbsDatabase: CAPPlugin {
let token = call.getString("token", "") let token = call.getString("token", "")
let name = "\(address) (\(username))" let name = "\(address) (\(username))"
let config = ServerConnectionConfig()
if id == nil { if id == nil {
id = "\(address)@\(username)".toBase64() id = "\(address)@\(username)".toBase64()
} }
config.id = id! let config = ServerConnectionConfig(id: id!, index: 0, name: name, address: address, userId: userId, username: username, token: token)
config.name = name
config.address = address
config.userId = userId
config.username = username
config.token = token
Store.serverConfig = config Store.serverConfig = config
call.resolve(convertServerConnectionConfigToJSON(config: config)) call.resolve(convertServerConnectionConfigToJSON(config: config))
@ -77,7 +71,7 @@ public class AbsDatabase: CAPPlugin {
@objc func getLocalLibraryItems(_ call: CAPPluginCall) { @objc func getLocalLibraryItems(_ call: CAPPluginCall) {
do { do {
let items = Database.shared.getLocalLibraryItems() let items = Database.shared.getLocalLibraryItems()
call.resolve([ "value": try items.asDictionaryArray() ]) call.resolve([ "value": try items.asDictionaryArray()])
} catch(let exception) { } catch(let exception) {
NSLog("error while readling local library items") NSLog("error while readling local library items")
debugPrint(exception) debugPrint(exception)
@ -128,11 +122,7 @@ public class AbsDatabase: CAPPlugin {
let enableAltView = call.getBool("enableAltView") ?? false let enableAltView = call.getBool("enableAltView") ?? false
let jumpBackwardsTime = call.getInt("jumpBackwardsTime") ?? 10 let jumpBackwardsTime = call.getInt("jumpBackwardsTime") ?? 10
let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10 let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10
let settings = DeviceSettings() let settings = DeviceSettings(disableAutoRewind: disableAutoRewind, enableAltView: enableAltView, jumpBackwardsTime: jumpBackwardsTime, jumpForwardTime: jumpForwardTime)
settings.disableAutoRewind = disableAutoRewind
settings.enableAltView = enableAltView
settings.jumpBackwardsTime = jumpBackwardsTime
settings.jumpForwardTime = jumpForwardTime
Database.shared.setDeviceSettings(deviceSettings: settings) Database.shared.setDeviceSettings(deviceSettings: settings)

View file

@ -7,22 +7,22 @@
import Foundation import Foundation
import CoreMedia import CoreMedia
import RealmSwift import Unrealm
struct LibraryItem: Codable { struct LibraryItem: Realmable, Codable {
var id: String var id: String
var ino:String var ino: String
var libraryId: String var libraryId: String
var folderId: String var folderId: String
var path: String var path: String
var relPath: String var relPath: String
var isFile: Bool var isFile: Bool
var mtimeMs: Int64 var mtimeMs: Int
var ctimeMs: Int64 var ctimeMs: Int
var birthtimeMs: Int64 var birthtimeMs: Int
var addedAt: Int64 var addedAt: Int
var updatedAt: Int64 var updatedAt: Int
var lastScan: Int64? var lastScan: Int?
var scanVersion: String? var scanVersion: String?
var isMissing: Bool var isMissing: Bool
var isInvalid: Bool var isInvalid: Bool
@ -30,9 +30,29 @@ struct LibraryItem: Codable {
var media: MediaType var media: MediaType
var libraryFiles: [LibraryFile] var libraryFiles: [LibraryFile]
var userMediaProgress: MediaProgress? var userMediaProgress: MediaProgress?
init() {
id = ""
ino = ""
libraryId = ""
folderId = ""
path = ""
relPath = ""
isFile = true
mtimeMs = 0
ctimeMs = 0
birthtimeMs = 0
addedAt = 0
updatedAt = 0
isMissing = false
isInvalid = false
mediaType = ""
media = MediaType()
libraryFiles = []
}
} }
struct MediaType: Codable { struct MediaType: Realmable, Codable {
var libraryItemId: String? var libraryItemId: String?
var metadata: Metadata var metadata: Metadata
var coverPath: String? var coverPath: String?
@ -40,13 +60,17 @@ struct MediaType: Codable {
var audioFiles: [AudioFile]? var audioFiles: [AudioFile]?
var chapters: [Chapter]? var chapters: [Chapter]?
var tracks: [AudioTrack]? var tracks: [AudioTrack]?
var size: Int64? var size: Int?
var duration: Double? var duration: Double?
var episodes: [PodcastEpisode]? var episodes: [PodcastEpisode]?
var autoDownloadEpisodes: Bool? var autoDownloadEpisodes: Bool?
init() {
metadata = Metadata()
}
} }
struct Metadata: Codable { struct Metadata: Realmable, Codable {
var title: String var title: String
var subtitle: String? var subtitle: String?
var authors: [Author]? var authors: [Author]?
@ -65,9 +89,15 @@ struct Metadata: Codable {
var narratorName: String? var narratorName: String?
var seriesName: String? var seriesName: String?
var feedUrl: String? var feedUrl: String?
init() {
title = "Unknown"
genres = []
explicit = false
}
} }
struct PodcastEpisode: Codable { struct PodcastEpisode: Realmable, Codable {
var id: String var id: String
var index: Int var index: Int
var episode: String? var episode: String?
@ -78,30 +108,55 @@ struct PodcastEpisode: Codable {
var audioFile: AudioFile? var audioFile: AudioFile?
var audioTrack: AudioTrack? var audioTrack: AudioTrack?
var duration: Double var duration: Double
var size: Int64 var size: Int
// var serverEpisodeId: String? // var serverEpisodeId: String?
init() {
id = ""
index = 0
title = "Unknown"
duration = 0
size = 0
}
} }
struct AudioFile: Codable { struct AudioFile: Realmable, Codable {
var index: Int var index: Int
var ino: String var ino: String
var metadata: FileMetadata var metadata: FileMetadata
init() {
index = 0
ino = ""
metadata = FileMetadata()
}
} }
struct Author: Codable { struct Author: Realmable, Codable {
var id: String var id: String
var name: String var name: String
var coverPath: String? var coverPath: String?
init() {
id = ""
name = "Unknown"
}
} }
struct Chapter: Codable { struct Chapter: Realmable, Codable {
var id: Int var id: Int
var start: Double var start: Double
var end: Double var end: Double
var title: String? var title: String?
init() {
id = 0
start = 0
end = 0
}
} }
struct AudioTrack: Codable { struct AudioTrack: Realmable, Codable {
var index: Int? var index: Int?
var startOffset: Double? var startOffset: Double?
var duration: Double var duration: Double
@ -113,50 +168,101 @@ struct AudioTrack: Codable {
// var localFileId: String? // var localFileId: String?
// var audioProbeResult: AudioProbeResult? Needed for local playback // var audioProbeResult: AudioProbeResult? Needed for local playback
var serverIndex: Int? var serverIndex: Int?
init() {
duration = 0
mimeType = ""
}
} }
struct FileMetadata: Codable { struct FileMetadata: Realmable, Codable {
var filename: String var filename: String
var ext: String var ext: String
var path: String var path: String
var relPath: String var relPath: String
init() {
filename = ""
ext = ""
path = ""
relPath = ""
}
} }
struct Library: Codable { struct Library: Realmable, Codable {
var id: String var id: String
var name: String var name: String
var folders: [Folder] var folders: [Folder]
var icon: String var icon: String
var mediaType: String var mediaType: String
init() {
id = ""
name = "Unknown"
folders = []
icon = ""
mediaType = ""
}
} }
struct Folder: Codable { struct Folder: Realmable, Codable {
var id: String var id: String
var fullPath: String var fullPath: String
init() {
id = ""
fullPath = ""
}
} }
struct LibraryFile: Codable { struct LibraryFile: Realmable, Codable {
var ino: String var ino: String
var metadata: FileMetadata var metadata: FileMetadata
init() {
ino = ""
metadata = FileMetadata()
}
} }
struct MediaProgress:Codable { struct MediaProgress: Realmable, Codable {
var id:String var id: String
var libraryItemId:String var libraryItemId: String
var episodeId:String? var episodeId: String?
var duration:Double var duration: Double
var progress:Double var progress: Double
var currentTime:Double var currentTime: Double
var isFinished:Bool var isFinished: Bool
var lastUpdate:Int64 var lastUpdate: Int
var startedAt:Int64 var startedAt: Int
var finishedAt:Int64? var finishedAt: Int?
init() {
id = ""
libraryItemId = ""
duration = 0
progress = 0
currentTime = 0
isFinished = false
lastUpdate = 0
startedAt = 0
}
} }
struct PlaybackMetadata: Codable { struct PlaybackMetadata: Realmable, Codable {
var duration: Double var duration: Double
var currentTime: Double var currentTime: Double
var playerState: PlayerState var playerState: PlayerState
init() {
duration = 0
currentTime = 0
playerState = PlayerState.IDLE
}
static func ignoredProperties() -> [String] {
return ["playerState"]
}
} }
enum PlayerState: Codable { enum PlayerState: Codable {

View file

@ -7,21 +7,17 @@
import Foundation import Foundation
import RealmSwift import RealmSwift
import Unrealm
class DeviceSettings: Object { struct DeviceSettings: Realmable {
@Persisted var disableAutoRewind: Bool var disableAutoRewind: Bool = false
@Persisted var enableAltView: Bool var enableAltView: Bool = false
@Persisted var jumpBackwardsTime: Int var jumpBackwardsTime: Int = 10
@Persisted var jumpForwardTime: Int var jumpForwardTime: Int = 10
} }
func getDefaultDeviceSettings() -> DeviceSettings { func getDefaultDeviceSettings() -> DeviceSettings {
let settings = DeviceSettings() return DeviceSettings()
settings.disableAutoRewind = false
settings.enableAltView = false
settings.jumpForwardTime = 10
settings.jumpBackwardsTime = 10
return settings
} }
func deviceSettingsToJSON(settings: DeviceSettings) -> Dictionary<String, Any> { func deviceSettingsToJSON(settings: DeviceSettings) -> Dictionary<String, Any> {

View file

@ -6,139 +6,81 @@
// //
import Foundation import Foundation
import RealmSwift import Unrealm
class LocalLibraryItem: Object, Encodable { struct LocalLibraryItem: Realmable, Codable {
@Persisted(primaryKey: true) var id: String = "local_\(UUID().uuidString)" var id: String = "local_\(UUID().uuidString)"
@Persisted var basePath: String = "" var basePath: String = ""
@Persisted var absolutePath: String = "" var absolutePath: String = ""
@Persisted var contentUrl: String var contentUrl: String = ""
@Persisted var isInvalid: Bool = false var isInvalid: Bool = false
@Persisted var mediaType: String var mediaType: String = ""
@Persisted var media: LocalMediaType? var media: MediaType?
@Persisted var localFiles: List<LocalFile> var localFiles: [LocalFile] = []
@Persisted var coverContentUrl: String? = nil var coverContentUrl: String?
@Persisted var coverAbsolutePath: String? = nil var coverAbsolutePath: String?
@Persisted var isLocal: Bool = true var isLocal: Bool = true
@Persisted var serverConnectionConfigId: String? = nil var serverConnectionConfigId: String?
@Persisted var serverAddress: String? = nil var serverAddress: String?
@Persisted var serverUserId: String? = nil var serverUserId: String?
@Persisted var libraryItemId: String? = nil var libraryItemId: String?
static func primaryKey() -> String? {
return "id"
}
} }
class LocalMediaType: Object, Encodable { struct LocalPodcastEpisode: Realmable, Codable {
@Persisted var libraryItemId: String? = "" var id: String = UUID().uuidString
@Persisted var metadata: LocalMetadata? var index: Int = 0
@Persisted var coverPath: String? = "" var episode: String?
@Persisted var tags: List<String?> var episodeType: String?
@Persisted var audioFiles: List<LocalAudioFile> var title: String = "Unknown"
@Persisted var chapters: List<LocalChapter> var subtitle: String?
@Persisted var tracks: List<LocalAudioTrack> var desc: String?
@Persisted var size: Int64? = nil var audioFile: AudioFile?
@Persisted var duration: Double? = nil var audioTrack: AudioTrack?
@Persisted var episodes: List<LocalPodcastEpisode> var duration: Double = 0
@Persisted var autoDownloadEpisodes: Bool? = nil var size: Int = 0
var serverEpisodeId: String?
static func primaryKey() -> String? {
return "id"
}
} }
class LocalMetadata: Object, Encodable { struct LocalFile: Realmable, Codable {
@Persisted var title: String var id: String = UUID().uuidString
@Persisted var subtitle: String? = "" var filename: String?
@Persisted var authors: List<LocalAuthor> var contentUrl: String = ""
@Persisted var narrators: List<String?> var absolutePath: String = ""
@Persisted var genres: List<String?> var mimeType: String?
@Persisted var publishedYear: String? = "" var size: Int = 0
@Persisted var publishedDate: String? = ""
@Persisted var publisher: String? = "" static func primaryKey() -> String? {
@Persisted var desc: String? = "" return "id"
@Persisted var isbn: String? = "" }
@Persisted var asin: String? = ""
@Persisted var language: String? = ""
@Persisted var explicit: Bool
@Persisted var authorName: String? = ""
@Persisted var authorNameLF: String? = ""
@Persisted var narratorName: String? = ""
@Persisted var seriesName: String? = ""
@Persisted var feedUrl: String? = ""
} }
class LocalPodcastEpisode: Object, Encodable { struct LocalMediaProgress: Realmable, Codable {
@Persisted var id: String = UUID().uuidString var id: String = UUID().uuidString
@Persisted var index: Int var localLibraryItemId: String = ""
@Persisted var episode: String? = "" var localEpisodeId: String?
@Persisted var episodeType: String? = "" var duration: Double = 0
@Persisted var title: String var progress: Double = 0
@Persisted var subtitle: String? = "" var currentTime: Double = 0
@Persisted var desc: String? = "" var isFinished: Bool = false
@Persisted var audioFile: LocalAudioFile? = nil var lastUpdate: Int = 0
@Persisted var audioTrack: LocalAudioTrack? = nil var startedAt: Int = 0
@Persisted var duration: Double var finishedAt: Int?
@Persisted var size: Int64
@Persisted var serverEpisodeId: String?
}
class LocalAudioFile: Object, Encodable {
@Persisted var index: Int
@Persisted var ino: String
@Persisted var metadata: LocalFileMetadata?
}
class LocalAuthor: Object, Encodable {
@Persisted var id: String = UUID().uuidString
@Persisted var name: String
@Persisted var coverPath: String? = ""
}
class LocalChapter: Object, Encodable {
@Persisted var id: Int
@Persisted var start: Double
@Persisted var end: Double
@Persisted var title: String? = nil
}
class LocalAudioTrack: Object, Encodable {
@Persisted var index: Int? = nil
@Persisted var startOffset: Double? = nil
@Persisted var duration: Double
@Persisted var title: String? = ""
@Persisted var contentUrl: String? = ""
@Persisted var mimeType: String
@Persisted var metadata: LocalFileMetadata? = nil
@Persisted var isLocal: Bool = true
@Persisted var localFileId: String? = ""
@Persisted var serverIndex: Int? = nil
}
class LocalFileMetadata: Object, Encodable {
@Persisted var filename: String
@Persisted var ext: String
@Persisted var path: String
@Persisted var relPath: String
}
class LocalFile: Object, Encodable {
@Persisted var id: String = UUID().uuidString
@Persisted var filename: String? = ""
@Persisted var contentUrl: String
@Persisted var absolutePath: String
@Persisted var mimeType: String? = ""
@Persisted var size: Int64
}
class LocalMediaProgress: Object, Encodable {
@Persisted(primaryKey: true) var id: String = UUID().uuidString
@Persisted var localLibraryItemId: String
@Persisted var localEpisodeId: String? = ""
@Persisted var duration: Double
@Persisted var progress: Double // 0 to 1
@Persisted var currentTime: Double
@Persisted var isFinished: Bool
@Persisted var lastUpdate: Int64
@Persisted var startedAt: Int64
@Persisted var finishedAt: Int64? = nil
// For local lib items from server to support server sync // For local lib items from server to support server sync
@Persisted var serverConnectionConfigId: String? = "" var serverConnectionConfigId: String?
@Persisted var serverAddress: String? = "" var serverAddress: String?
@Persisted var serverUserId: String? = "" var serverUserId: String?
@Persisted var libraryItemId: String? = "" var libraryItemId: String?
@Persisted var episodeId: String? = "" var episodeId: String?
static func primaryKey() -> String? {
return "id"
}
} }

View file

@ -8,49 +8,12 @@
import Foundation import Foundation
extension LocalLibraryItem { extension LocalLibraryItem {
enum CodingKeys: CodingKey { init(_ item: LibraryItem, localUrl: URL, server: ServerConnectionConfig, files: [LocalFile]) {
case id
case basePath
case absolutePath
case contentUrl
case isInvalid
case mediaType
case media
case localFiles
case coverContentUrl
case coverAbsolutePath
case isLocal
case serverConnectionConfigId
case serverAddress
case serverUserId
case 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(absolutePath, forKey: .absolutePath)
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(localFiles, forKey: .localFiles)
try container.encode(coverContentUrl, forKey: .coverContentUrl)
try container.encode(coverAbsolutePath, forKey: .coverAbsolutePath)
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)
}
convenience init(_ item: LibraryItem, localUrl: URL, server: ServerConnectionConfig, files: [LocalFile]) {
self.init() self.init()
self.contentUrl = localUrl.absoluteString self.contentUrl = localUrl.absoluteString
self.mediaType = item.mediaType self.mediaType = item.mediaType
self.media = LocalMediaType(item.media, coverPath: "", files: files) self.media = item.media
self.localFiles.append(objectsIn: files) self.localFiles = files
// TODO: self.coverContentURL // TODO: self.coverContentURL
// TODO: self.converAbsolutePath // TODO: self.converAbsolutePath
self.libraryItemId = item.id self.libraryItemId = item.id
@ -61,7 +24,7 @@ extension LocalLibraryItem {
func getDuration() -> Double { func getDuration() -> Double {
var total = 0.0 var total = 0.0
self.media?.tracks.forEach { track in total += track.duration } self.media?.tracks?.forEach { track in total += track.duration }
return total return total
} }
@ -107,315 +70,14 @@ extension LocalLibraryItem {
} }
} }
extension LocalMediaType {
enum CodingKeys: CodingKey {
case libraryItemId
case metadata
case coverPath
case tags
case audioFiles
case chapters
case tracks
case size
case duration
case episodes
case 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(tags, forKey: .tags)
try container.encode(audioFiles, forKey: .audioFiles)
try container.encode(chapters, forKey: .chapters)
try container.encode(tracks, forKey: .tracks)
try container.encode(size, forKey: .size)
try container.encode(duration, forKey: .duration)
try container.encode(episodes, forKey: .episodes)
try container.encode(autoDownloadEpisodes, forKey: .autoDownloadEpisodes)
}
convenience init(_ mediaType: MediaType, coverPath: String, files: [LocalFile]) {
self.init()
self.libraryItemId = mediaType.libraryItemId
self.metadata = LocalMetadata(mediaType.metadata)
self.coverPath = coverPath
self.tags.append(objectsIn: mediaType.tags ?? [])
self.audioFiles.append(objectsIn: mediaType.audioFiles!.enumerated().map() {
i, audioFile -> LocalAudioFile in LocalAudioFile(audioFile)
})
self.chapters.append(objectsIn: mediaType.chapters!.enumerated().map() {
i, chapter -> LocalChapter in LocalChapter(chapter)
})
self.tracks.append(objectsIn: mediaType.tracks!.enumerated().map() {
i, track in LocalAudioTrack(track, libraryItemId: self.libraryItemId ?? "", filename: files[i].filename ?? "")
})
self.size = mediaType.size
self.duration = mediaType.duration
// TODO: self.episodes
// TODO: Handle podcast auto downloads
self.autoDownloadEpisodes = mediaType.autoDownloadEpisodes
}
}
extension LocalMetadata {
enum CodingKeys: CodingKey {
case title
case subtitle
case authors
case narrators
case genres
case publishedYear
case publishedDate
case publisher
case desc
case isbn
case asin
case language
case explicit
case authorName
case authorNameLF
case narratorName
case seriesName
case 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(authors, forKey: .authors)
try container.encode(narrators, forKey: .narrators)
try container.encode(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)
}
convenience init(_ metadata: Metadata) {
self.init()
self.title = metadata.title
self.subtitle = metadata.subtitle
self.narrators.append(objectsIn: metadata.narrators ?? [])
self.genres.append(objectsIn: metadata.genres)
self.publishedYear = metadata.publishedYear
self.publishedDate = metadata.publishedDate
self.publisher = metadata.publisher
self.desc = metadata.description
self.isbn = metadata.isbn
self.asin = metadata.asin
self.language = metadata.language
self.explicit = metadata.explicit
self.authorName = metadata.authorName
self.authorNameLF = metadata.authorNameLF
self.narratorName = metadata.narratorName
self.seriesName = metadata.seriesName
self.feedUrl = metadata.feedUrl
}
}
extension LocalPodcastEpisode {
enum CodingKeys: CodingKey {
case id
case index
case episode
case episodeType
case title
case subtitle
case desc
case audioFile
case audioTrack
case duration
case size
case serverEpisodeId
}
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)
}
}
extension LocalAudioFile {
enum CodingKeys: CodingKey {
case index
case ino
case metadata
}
convenience init(_ audioFile: AudioFile) {
self.init()
self.index = audioFile.index
self.ino = audioFile.ino
// self.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)
}
}
extension LocalAuthor {
enum CodingKeys: CodingKey {
case id
case name
case 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)
}
convenience init(_ author: Author) {
self.init()
self.id = author.id
self.name = author.name
// self.coverPath
}
}
extension LocalChapter {
enum CodingKeys: CodingKey {
case id
case start
case end
case 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)
}
convenience init(_ chapter: Chapter) {
self.init()
self.id = chapter.id
self.start = chapter.start
self.end = chapter.end
self.title = chapter.title
}
}
extension LocalAudioTrack {
enum CodingKeys: CodingKey {
case index
case startOffset
case duration
case title
case contentUrl
case mimeType
case metadata
case isLocal
case localFileId
case 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(isLocal, forKey: .isLocal)
try container.encode(localFileId, forKey: .localFileId)
try container.encode(serverIndex, forKey: .serverIndex)
}
convenience init(_ track: AudioTrack, libraryItemId: String, filename: String) {
self.init()
self.index = track.index
self.startOffset = track.startOffset
self.duration = track.duration
self.title = track.title
self.contentUrl = "" // TODO: Different URL
self.mimeType = track.mimeType
// TODO: self.metadata
self.localFileId = "\(libraryItemId)_\(filename.toBase64())"
self.serverIndex = track.serverIndex
}
}
extension LocalFileMetadata {
enum CodingKeys: CodingKey {
case filename
case ext
case path
case 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)
}
/* TODO: Can we skip this object? */
}
extension LocalFile { extension LocalFile {
enum CodingKeys: CodingKey { init(_ libraryItemId: String, _ filename: String, _ mimeType: String, _ localUrl: URL) {
case id
case filename
case contentUrl
case absolutePath
case mimeType
case 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(absolutePath, forKey: .absolutePath)
try container.encode(mimeType, forKey: .mimeType)
try container.encode(size, forKey: .size)
}
convenience init(_ libraryItemId: String, _ filename: String, _ mimeType: String, _ localUrl: URL) {
self.init() self.init()
self.id = "\(libraryItemId)_\(filename.toBase64())" self.id = "\(libraryItemId)_\(filename.toBase64())"
self.filename = filename self.filename = filename
self.contentUrl = localUrl.absoluteString self.contentUrl = localUrl.absoluteString
self.absolutePath = localUrl.path self.absolutePath = localUrl.path
self.size = localUrl.fileSize self.size = Int(localUrl.fileSize)
} }
func isAudioFile() -> Bool { func isAudioFile() -> Bool {
@ -428,42 +90,3 @@ extension LocalFile {
} }
} }
} }
extension LocalMediaProgress {
enum CodingKeys: CodingKey {
case id
case localLibraryItemId
case localEpisodeId
case duration
case progress
case currentTime
case isFinished
case lastUpdate
case startedAt
case finishedAt
case serverConnectionConfigId
case serverAddress
case serverUserId
case libraryItemId
case 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)
}
}

View file

@ -7,20 +7,29 @@
import Foundation import Foundation
import RealmSwift import RealmSwift
import Unrealm
class ServerConnectionConfig: Object { struct ServerConnectionConfig: Realmable {
@Persisted(primaryKey: true) var id: String var id: String = UUID().uuidString
@Persisted(indexed: true) var index: Int var index: Int = 0
@Persisted var name: String var name: String = ""
@Persisted var address: String var address: String = ""
@Persisted var userId: String var userId: String = ""
@Persisted var username: String var username: String = ""
@Persisted var token: String var token: String = ""
static func primaryKey() -> String? {
return "id"
}
static func indexedProperties() -> [String] {
return ["index"]
}
} }
class ServerConnectionConfigActiveIndex: Object {
// This could overflow, but you really would have to try
@Persisted var index: Int?
struct ServerConnectionConfigActiveIndex: Realmable {
// This could overflow, but you really would have to try
var index: Int?
} }
func convertServerConnectionConfigToJSON(config: ServerConnectionConfig) -> Dictionary<String, Any> { func convertServerConnectionConfigToJSON(config: ServerConnectionConfig) -> Dictionary<String, Any> {

View file

@ -114,9 +114,11 @@ class ApiClient {
callback(session) callback(session)
} }
} }
public static func reportPlaybackProgress(report: PlaybackReport, sessionId: String) { public static func reportPlaybackProgress(report: PlaybackReport, sessionId: String) {
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil) try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil)
} }
public static func getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, callback: @escaping (_ param: LibraryItem?) -> Void) { public static func getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, callback: @escaping (_ param: LibraryItem?) -> Void) {
var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress" var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress"
if episodeId != nil { if episodeId != nil {

View file

@ -23,16 +23,12 @@ class Database {
} }
public func setServerConnectionConfig(config: ServerConnectionConfig) { public func setServerConnectionConfig(config: ServerConnectionConfig) {
var refrence: ThreadSafeReference<ServerConnectionConfig>? var config = config
if config.realm != nil {
refrence = ThreadSafeReference(to: config)
}
Database.realmQueue.sync { Database.realmQueue.sync {
let existing: ServerConnectionConfig? = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id) var existing: ServerConnectionConfig? = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id)
if config.index == 0 { if config.index == 0 {
let lastConfig: ServerConnectionConfig? = instance.objects(ServerConnectionConfig.self).last var lastConfig: ServerConnectionConfig? = instance.objects(ServerConnectionConfig.self).last
if lastConfig != nil { if lastConfig != nil {
config.index = lastConfig!.index + 1 config.index = lastConfig!.index + 1
@ -46,15 +42,7 @@ class Database {
if existing != nil { if existing != nil {
instance.delete(existing!) instance.delete(existing!)
} }
if refrence == nil { instance.add(config)
instance.add(config)
} else {
guard let resolved = instance.resolve(refrence!) else {
throw "unable to resolve refrence"
}
instance.add(resolved);
}
} }
} catch(let exception) { } catch(let exception) {
NSLog("failed to save server config") NSLog("failed to save server config")
@ -83,25 +71,8 @@ class Database {
} }
public func getServerConnectionConfigs() -> [ServerConnectionConfig] { public func getServerConnectionConfigs() -> [ServerConnectionConfig] {
var refrences: [ThreadSafeReference<ServerConnectionConfig>] = []
Database.realmQueue.sync { Database.realmQueue.sync {
let configs = instance.objects(ServerConnectionConfig.self) return Array(instance.objects(ServerConnectionConfig.self))
refrences = configs.map { config in
return ThreadSafeReference(to: config)
}
}
do {
let realm = try Realm()
return refrences.map { refrence in
return realm.resolve(refrence)!
}
} catch(let exception) {
NSLog("error while readling configs")
debugPrint(exception)
return []
} }
} }
@ -113,7 +84,7 @@ class Database {
public func setLastActiveConfigIndex(index: Int?) { public func setLastActiveConfigIndex(index: Int?) {
let existing = instance.objects(ServerConnectionConfigActiveIndex.self) let existing = instance.objects(ServerConnectionConfigActiveIndex.self)
let obj = ServerConnectionConfigActiveIndex() var obj = ServerConnectionConfigActiveIndex()
obj.index = index obj.index = index
do { do {
@ -149,25 +120,8 @@ class Database {
} }
public func getLocalLibraryItems(mediaType: MediaType? = nil) -> [LocalLibraryItem] { public func getLocalLibraryItems(mediaType: MediaType? = nil) -> [LocalLibraryItem] {
var localLibraryItems: [ThreadSafeReference<LocalLibraryItem>] = []
Database.realmQueue.sync { Database.realmQueue.sync {
let items = instance.objects(LocalLibraryItem.self) Array(instance.objects(LocalLibraryItem.self))
localLibraryItems = items.map { item in
return ThreadSafeReference(to: item)
}
}
do {
let realm = try Realm()
return localLibraryItems.map { item in
return realm.resolve(item)!
}
} catch(let exception) {
NSLog("error while readling local library items")
debugPrint(exception)
return []
} }
} }
@ -183,26 +137,14 @@ class Database {
} }
public func getLocalLibraryItem(localLibraryItem: String) -> LocalLibraryItem? { public func getLocalLibraryItem(localLibraryItem: String) -> LocalLibraryItem? {
let items = getLocalLibraryItems() Database.realmQueue.sync {
for item in items { instance.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItem)
if (item.id == localLibraryItem) {
return item
}
} }
NSLog("Local library item with id \(localLibraryItem) not found")
return nil
} }
public func saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) { public func saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) {
Database.realmQueue.sync { Database.realmQueue.sync {
do { try! instance.write { instance.add(localLibraryItem) }
try instance.write {
instance.add(localLibraryItem);
}
} catch(let exception) {
NSLog("Unable to save local library item")
debugPrint(exception)
}
} }
} }
@ -213,19 +155,10 @@ class Database {
} }
public func removeLocalLibraryItem(localLibraryItemId: String) { public func removeLocalLibraryItem(localLibraryItemId: String) {
let item = getLocalLibraryItemByLLId(libraryItem: localLibraryItemId)
Database.realmQueue.sync { Database.realmQueue.sync {
do { try! instance.write {
try instance.write { let item = getLocalLibraryItemByLLId(libraryItem: localLibraryItemId)
if item != nil { instance.delete(item!)
instance.delete(item!)
} else {
NSLog("Unable to find local library item to delete")
}
}
} catch (let exception) {
NSLog("Unable to delete local library item")
debugPrint(exception)
} }
} }
} }

View file

@ -9,7 +9,7 @@ import Foundation
import RealmSwift import RealmSwift
class Store { class Store {
@ThreadSafe private static var _serverConfig: ServerConnectionConfig? private static var _serverConfig: ServerConnectionConfig?
public static var serverConfig: ServerConnectionConfig? { public static var serverConfig: ServerConnectionConfig? {
get { get {
return _serverConfig return _serverConfig

2
package-lock.json generated
View file

@ -32982,4 +32982,4 @@
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
} }
} }
} }