mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-29 06:37:16 +02:00
Merge branch 'ios-downloads' into ios-downloads-realm-native
This commit is contained in:
@@ -264,7 +264,7 @@ class PodcastEpisode: Object, Codable {
|
||||
@Persisted var audioTrack: AudioTrack?
|
||||
@Persisted var duration: Double = 0
|
||||
@Persisted var size: Int = 0
|
||||
// var serverEpisodeId: String?
|
||||
var serverEpisodeId: String { self.id }
|
||||
|
||||
private enum CodingKeys : String, CodingKey {
|
||||
case id,
|
||||
@@ -277,14 +277,44 @@ class PodcastEpisode: Object, Codable {
|
||||
audioFile,
|
||||
audioTrack,
|
||||
duration,
|
||||
size
|
||||
size,
|
||||
serverEpisodeId
|
||||
}
|
||||
|
||||
// TODO: Encoding
|
||||
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 = 0
|
||||
@Persisted var index: Int?
|
||||
@Persisted var ino: String = ""
|
||||
@Persisted var metadata: FileMetadata?
|
||||
|
||||
@@ -299,7 +329,7 @@ class AudioFile: Object, Codable {
|
||||
required init(from decoder: Decoder) throws {
|
||||
super.init()
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
index = try values.decode(Int.self, forKey: .index)
|
||||
index = try? values.decode(Int.self, forKey: .index)
|
||||
ino = try values.decode(String.self, forKey: .ino)
|
||||
metadata = try? values.decode(FileMetadata.self, forKey: .metadata)
|
||||
}
|
||||
@@ -339,6 +369,9 @@ class Author: Object, Codable {
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(coverPath, forKey: .coverPath)
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
class Chapter: Object, Codable {
|
||||
@@ -381,8 +414,7 @@ class AudioTrack: Object, Codable {
|
||||
@Persisted var contentUrl: String?
|
||||
@Persisted var mimeType: String = ""
|
||||
@Persisted var metadata: FileMetadata?
|
||||
// var isLocal: Bool
|
||||
// var localFileId: String?
|
||||
var localFileId: String?
|
||||
// var audioProbeResult: AudioProbeResult? Needed for local playback
|
||||
@Persisted var serverIndex: Int?
|
||||
|
||||
@@ -418,6 +450,20 @@ class AudioTrack: Object, Codable {
|
||||
try container.encode(metadata, forKey: .metadata)
|
||||
try container.encode(serverIndex, forKey: .serverIndex)
|
||||
}
|
||||
|
||||
mutating 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 {
|
||||
@@ -562,18 +608,17 @@ class MediaProgress: Object, Codable {
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
super.init()
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
libraryItemId = try values.decode(String.self, forKey: .libraryItemId)
|
||||
episodeId = try? values.decode(String.self, forKey: .episodeId)
|
||||
duration = try values.decode(Double.self, forKey: .duration)
|
||||
progress = try values.decode(Double.self, forKey: .progress)
|
||||
currentTime = try values.decode(Double.self, forKey: .currentTime)
|
||||
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.decode(Int.self, forKey: .lastUpdate)
|
||||
startedAt = try values.decode(Int.self, forKey: .startedAt)
|
||||
finishedAt = try? values.decode(Int.self, forKey: .finishedAt)
|
||||
lastUpdate = try values.intOrStringDecoder(key: .lastUpdate)
|
||||
startedAt = try values.intOrStringDecoder(key: .startedAt)
|
||||
finishedAt = try? values.intOrStringDecoder(key: .finishedAt)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
|
||||
@@ -73,6 +73,35 @@ class DownloadItemPart: Object, Codable {
|
||||
@Persisted var uri: String?
|
||||
@Persisted var destinationUri: String?
|
||||
@Persisted var progress: Double = 0
|
||||
var task: URLSessionDownloadTask?
|
||||
|
||||
struct DownloadItemPart: Realmable, Codable {
|
||||
var id: String = UUID().uuidString
|
||||
var filename: String?
|
||||
var itemTitle: String?
|
||||
var serverPath: String?
|
||||
var audioTrack: AudioTrack?
|
||||
var episode: PodcastEpisode?
|
||||
var completed: Bool = false
|
||||
var moved: Bool = false
|
||||
var failed: Bool = false
|
||||
var uri: String?
|
||||
var downloadURL: URL? {
|
||||
if let uri = self.uri {
|
||||
return URL(string: uri)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var destinationUri: String?
|
||||
var destinationURL: URL? {
|
||||
if let destinationUri = self.destinationUri {
|
||||
return AbsDownloader.itemDownloadFolder(path: destinationUri)!
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var progress: Double = 0
|
||||
var task: URLSessionDownloadTask!
|
||||
|
||||
private enum CodingKeys : String, CodingKey {
|
||||
@@ -105,3 +134,36 @@ class DownloadItemPart: Object, Codable {
|
||||
try container.encode(progress, forKey: .progress)
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadItemPart {
|
||||
init(filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) {
|
||||
self.filename = filename
|
||||
self.itemTitle = itemTitle
|
||||
self.serverPath = serverPath
|
||||
self.audioTrack = audioTrack
|
||||
self.episode = episode
|
||||
|
||||
let config = Store.serverConfig!
|
||||
var downloadUrl = ""
|
||||
if (serverPath.hasSuffix("/cover")) {
|
||||
downloadUrl += "\(config.address)\(serverPath)?token=\(config.token)"
|
||||
downloadUrl += "&format=jpeg" // For cover images force to jpeg
|
||||
} else {
|
||||
downloadUrl = destination
|
||||
}
|
||||
self.uri = downloadUrl
|
||||
self.destinationUri = destination
|
||||
}
|
||||
|
||||
func mimeType() -> String? {
|
||||
if let track = audioTrack {
|
||||
return track.mimeType
|
||||
} else if let podcastTrack = episode?.audioTrack {
|
||||
return podcastTrack.mimeType
|
||||
} else if serverPath?.hasSuffix("/cover") ?? false {
|
||||
return "image/jpg"
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,33 @@ class LocalLibraryItem: Object, Codable {
|
||||
@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
|
||||
@@ -37,14 +64,12 @@ class LocalLibraryItem: Object, Codable {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
basePath = try values.decode(String.self, forKey: .basePath)
|
||||
contentUrl = try values.decode(String.self, forKey: .contentUrl)
|
||||
isInvalid = try values.decode(Bool.self, forKey: .isInvalid)
|
||||
mediaType = try values.decode(String.self, forKey: .mediaType)
|
||||
media = try? values.decode(MediaType.self, forKey: .media)
|
||||
if let files = try? values.decode([LocalFile].self, forKey: .localFiles) {
|
||||
localFiles.append(objectsIn: files)
|
||||
}
|
||||
_coverContentUrl = try values.decode(String.self, forKey: .coverContentUrl)
|
||||
isLocal = try values.decode(Bool.self, forKey: .isLocal)
|
||||
serverConnectionConfigId = try? values.decode(String.self, forKey: .serverConnectionConfigId)
|
||||
serverAddress = try? values.decode(String.self, forKey: .serverAddress)
|
||||
@@ -111,9 +136,13 @@ class LocalFile: Object, Codable {
|
||||
@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, absolutePath, mimeType, size
|
||||
case id, filename, contentUrl, mimeType, size, basePath
|
||||
}
|
||||
|
||||
override init() {
|
||||
@@ -124,7 +153,6 @@ class LocalFile: Object, Codable {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
filename = try? values.decode(String.self, forKey: .filename)
|
||||
contentUrl = try values.decode(String.self, forKey: .contentUrl)
|
||||
mimeType = try? values.decode(String.self, forKey: .mimeType)
|
||||
size = try values.decode(Int.self, forKey: .size)
|
||||
}
|
||||
@@ -134,9 +162,9 @@ class LocalFile: Object, Codable {
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(filename, forKey: .filename)
|
||||
try container.encode(contentUrl, forKey: .contentUrl)
|
||||
try container.encode(absolutePath, forKey: .absolutePath)
|
||||
try container.encode(mimeType, forKey: .mimeType)
|
||||
try container.encode(size, forKey: .size)
|
||||
try container.encode(basePath, forKey: .basePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +185,8 @@ class LocalMediaProgress: Object, Codable {
|
||||
@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
|
||||
|
||||
@@ -13,39 +13,44 @@ extension LocalLibraryItem {
|
||||
|
||||
self.contentUrl = localUrl
|
||||
self.mediaType = item.mediaType
|
||||
self.media = item.media
|
||||
self.localFiles.append(objectsIn: files)
|
||||
self.coverContentUrl = coverPath
|
||||
self._coverContentUrl = coverPath
|
||||
self.libraryItemId = item.id
|
||||
self.serverConnectionConfigId = server.id
|
||||
self.serverAddress = server.address
|
||||
self.serverUserId = server.userId
|
||||
|
||||
// Link the audio tracks and files
|
||||
linkLocalFiles(files, fromMedia: item.media)
|
||||
}
|
||||
|
||||
var contentUrl: String? {
|
||||
set(url) {
|
||||
_contentUrl = url
|
||||
}
|
||||
get {
|
||||
if let path = _contentUrl {
|
||||
return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
mutating func addFiles(_ files: [LocalFile], item: LibraryItem) throws {
|
||||
guard self.isPodcast else { throw LibraryItemDownloadError.podcastOnlySupported }
|
||||
self.localFiles.append(contentsOf: files.filter({ $0.isAudioFile() }))
|
||||
linkLocalFiles(self.localFiles, fromMedia: item.media)
|
||||
}
|
||||
|
||||
var coverContentUrl: String? {
|
||||
set(url) {
|
||||
_coverContentUrl = url
|
||||
}
|
||||
get {
|
||||
if let path = self._coverContentUrl {
|
||||
return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString
|
||||
} else {
|
||||
return nil
|
||||
mutating private func linkLocalFiles(_ files: [LocalFile], fromMedia: MediaType) {
|
||||
var fromMedia = fromMedia
|
||||
let fileMap = files.map { ($0.filename ?? "", $0.id) }
|
||||
let fileIdByFilename = Dictionary(fileMap, uniquingKeysWith: { (_, last) in last })
|
||||
if ( self.isBook ) {
|
||||
if let tracks = fromMedia.tracks {
|
||||
for i in tracks.indices {
|
||||
_ = fromMedia.tracks?[i].setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: i)
|
||||
}
|
||||
}
|
||||
} else if ( self.isPodcast ) {
|
||||
if let episodes = fromMedia.episodes {
|
||||
fromMedia.episodes = episodes.compactMap { episode in
|
||||
// Filter out episodes not downloaded
|
||||
var episode = episode
|
||||
let episodeIsDownloaded = episode.audioTrack?.setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: 0) ?? false
|
||||
return episodeIsDownloaded ? episode : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
self.media = fromMedia
|
||||
}
|
||||
|
||||
func getDuration() -> Double {
|
||||
@@ -54,7 +59,13 @@ extension LocalLibraryItem {
|
||||
return total
|
||||
}
|
||||
|
||||
func getPlaybackSession(episode: LocalPodcastEpisode?) -> PlaybackSession {
|
||||
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)"
|
||||
|
||||
@@ -62,13 +73,13 @@ extension LocalLibraryItem {
|
||||
let mediaProgressId = (localEpisodeId != nil) ? "\(self.id)-\(localEpisodeId!)" : self.id
|
||||
let mediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: mediaProgressId)
|
||||
|
||||
// TODO: Clean up add mediaType methods for displayTitle and displayAuthor
|
||||
let mediaMetadata = self.media?.metadata
|
||||
let audioTracks = self.media?.tracks
|
||||
let authorName = mediaMetadata?.authorName
|
||||
let chapters = self.media?.chapters
|
||||
var audioTracks = self.media?.tracks
|
||||
let authorName = mediaMetadata?.authorDisplayName
|
||||
|
||||
if let episode = episode {
|
||||
// TODO: Implement podcast
|
||||
if let episode = episode, let track = episode.audioTrack {
|
||||
audioTracks = [track]
|
||||
}
|
||||
|
||||
let dateNow = Date().timeIntervalSince1970
|
||||
@@ -78,18 +89,19 @@ extension LocalLibraryItem {
|
||||
libraryItemId: self.libraryItemId,
|
||||
episodeId: episode?.serverEpisodeId,
|
||||
mediaType: self.mediaType,
|
||||
chapters: [],
|
||||
chapters: chapters ?? [],
|
||||
displayTitle: mediaMetadata?.title,
|
||||
displayAuthor: authorName,
|
||||
coverPath: nil,
|
||||
coverPath: self.coverContentUrl,
|
||||
duration: self.getDuration(),
|
||||
playMethod: 3,
|
||||
playMethod: PlayMethod.local.rawValue,
|
||||
startedAt: dateNow,
|
||||
updatedAt: 0,
|
||||
timeListening: 0.0,
|
||||
audioTracks: [],
|
||||
audioTracks: audioTracks ?? [],
|
||||
currentTime: mediaProgress?.currentTime ?? 0.0,
|
||||
libraryItem: nil,
|
||||
localLibraryItem: self,
|
||||
serverConnectionConfigId: self.serverConnectionConfigId,
|
||||
serverAddress: self.serverAddress
|
||||
)
|
||||
@@ -103,7 +115,7 @@ extension LocalFile {
|
||||
self.id = "\(libraryItemId)_\(filename.toBase64())"
|
||||
self.filename = filename
|
||||
self.mimeType = mimeType
|
||||
self.contentUrl = localUrl
|
||||
self._contentUrl = localUrl
|
||||
self.size = fileSize
|
||||
}
|
||||
|
||||
@@ -123,27 +135,84 @@ extension LocalFile {
|
||||
}
|
||||
|
||||
extension LocalMediaProgress {
|
||||
convenience init(localLibraryItem: LocalLibraryItem, episode: LocalPodcastEpisode?, progress: MediaProgress) {
|
||||
convenience init(localLibraryItem: LocalLibraryItem, episode: PodcastEpisode?) {
|
||||
self.init()
|
||||
|
||||
self.id = localLibraryItem.id
|
||||
self.localLibraryItemId = localLibraryItem.id
|
||||
self.libraryItemId = localLibraryItem.libraryItemId
|
||||
|
||||
if let episode = episode {
|
||||
self.id += "-\(episode.id)"
|
||||
self.episodeId = episode.id
|
||||
}
|
||||
|
||||
self.serverAddress = localLibraryItem.serverAddress
|
||||
self.serverUserId = localLibraryItem.serverUserId
|
||||
self.serverConnectionConfigId = localLibraryItem.serverConnectionConfigId
|
||||
|
||||
self.duration = progress.duration
|
||||
self.currentTime = progress.currentTime
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
mutating func updateIsFinished(_ finished: Bool) {
|
||||
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
|
||||
}
|
||||
|
||||
mutating func updateFromPlaybackSession(_ playbackSession: PlaybackSession) {
|
||||
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
|
||||
}
|
||||
|
||||
mutating func updateFromServerMediaProgress(_ serverMediaProgress: MediaProgress) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct PlaybackSession: Decodable, Encodable {
|
||||
struct PlaybackSession: Codable {
|
||||
var id: String
|
||||
var userId: String?
|
||||
var libraryItemId: String?
|
||||
@@ -26,7 +26,25 @@ struct PlaybackSession: Decodable, Encodable {
|
||||
var audioTracks: [AudioTrack]
|
||||
var currentTime: Double
|
||||
var libraryItem: LibraryItem?
|
||||
//var localLibraryItem: LocalLibraryItem?
|
||||
var localLibraryItem: LocalLibraryItem?
|
||||
var serverConnectionConfigId: String?
|
||||
var serverAddress: String?
|
||||
|
||||
var isLocal: Bool { self.localLibraryItem != nil }
|
||||
|
||||
var localMediaProgressId: String {
|
||||
if let episodeId = episodeId {
|
||||
return "\(localLibraryItem!.id)-\(episodeId)"
|
||||
} else {
|
||||
return localLibraryItem!.id
|
||||
}
|
||||
}
|
||||
|
||||
var totalDuration: Double {
|
||||
var total = 0.0
|
||||
self.audioTracks.forEach { total += $0.duration }
|
||||
return total
|
||||
}
|
||||
|
||||
var progress: Double { self.currentTime / self.totalDuration }
|
||||
}
|
||||
|
||||
@@ -267,6 +267,7 @@ class AudioPlayer: NSObject {
|
||||
let startOffset = audioTrack.startOffset ?? 0.0
|
||||
return startOffset + currentTrackTime
|
||||
}
|
||||
|
||||
public func getPlayMethod() -> Int {
|
||||
return self.playbackSession.playMethod
|
||||
}
|
||||
@@ -286,6 +287,17 @@ class AudioPlayer: NSObject {
|
||||
let urlstr = "\(Store.serverConfig!.address)/s/item/\(itemId)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
|
||||
let url = URL(string: urlstr)!
|
||||
return AVURLAsset(url: url)
|
||||
} else if (playbackSession.playMethod == PlayMethod.local.rawValue) {
|
||||
guard let localFile = track.getLocalFile() else {
|
||||
// Worst case we can stream the file
|
||||
NSLog("Unable to play local file. Resulting to streaming \(track.localFileId ?? "Unknown")")
|
||||
let filename = track.metadata?.filename ?? ""
|
||||
let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
|
||||
let urlstr = "\(Store.serverConfig!.address)/s/item/\(itemId)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
|
||||
let url = URL(string: urlstr)!
|
||||
return AVURLAsset(url: url)
|
||||
}
|
||||
return AVURLAsset(url: localFile.contentPath)
|
||||
} else { // HLS Transcode
|
||||
let headers: [String: String] = [
|
||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||
|
||||
@@ -11,7 +11,7 @@ class PlayerHandler {
|
||||
private static var player: AudioPlayer?
|
||||
private static var session: PlaybackSession?
|
||||
private static var timer: Timer?
|
||||
private static var lastSyncTime:Double = 0.0
|
||||
private static var lastSyncTime: Double = 0.0
|
||||
|
||||
private static var _remainingSleepTime: Int? = nil
|
||||
public static var remainingSleepTime: Int? {
|
||||
@@ -52,6 +52,22 @@ class PlayerHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public static func startTickTimer() {
|
||||
DispatchQueue.runOnMainQueue {
|
||||
NSLog("Starting the tick timer")
|
||||
timer?.invalidate()
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
self.tick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func stopTickTimer() {
|
||||
NSLog("Stopping the tick timer")
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
public static func startPlayback(session: PlaybackSession, playWhenReady: Bool, playbackRate: Float) {
|
||||
if player != nil {
|
||||
player?.destroy()
|
||||
@@ -63,31 +79,34 @@ class PlayerHandler {
|
||||
self.session = session
|
||||
player = AudioPlayer(playbackSession: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||
|
||||
DispatchQueue.runOnMainQueue {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
self.tick()
|
||||
}
|
||||
}
|
||||
startTickTimer()
|
||||
}
|
||||
|
||||
public static func stopPlayback() {
|
||||
// Pause playback first, so we can sync our current progress
|
||||
player?.pause()
|
||||
|
||||
// Stop updating progress before we destory the player, so we don't receive bad data
|
||||
stopTickTimer()
|
||||
|
||||
player?.destroy()
|
||||
player = nil
|
||||
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
|
||||
NowPlayingInfo.shared.reset()
|
||||
}
|
||||
|
||||
public static func getCurrentTime() -> Double? {
|
||||
self.player?.getCurrentTime()
|
||||
}
|
||||
|
||||
public static func setPlaybackSpeed(speed: Float) {
|
||||
self.player?.setPlaybackRate(speed)
|
||||
}
|
||||
|
||||
public static func getPlayMethod() -> Int? {
|
||||
self.player?.getPlayMethod()
|
||||
}
|
||||
|
||||
public static func getPlaybackSession() -> PlaybackSession? {
|
||||
self.player?.getPlaybackSession()
|
||||
}
|
||||
@@ -100,6 +119,7 @@ class PlayerHandler {
|
||||
let destinationTime = player.getCurrentTime() + amount
|
||||
player.seek(destinationTime, from: "handler")
|
||||
}
|
||||
|
||||
public static func seekBackward(amount: Double) {
|
||||
guard let player = player else {
|
||||
return
|
||||
@@ -108,9 +128,11 @@ class PlayerHandler {
|
||||
let destinationTime = player.getCurrentTime() - amount
|
||||
player.seek(destinationTime, from: "handler")
|
||||
}
|
||||
|
||||
public static func seek(amount: Double) {
|
||||
player?.seek(amount, from: "handler")
|
||||
}
|
||||
|
||||
public static func getMetdata() -> [String: Any] {
|
||||
DispatchQueue.main.async {
|
||||
syncProgress()
|
||||
@@ -140,15 +162,15 @@ class PlayerHandler {
|
||||
remainingSleepTime! -= 1
|
||||
}
|
||||
}
|
||||
|
||||
public static func syncProgress() {
|
||||
if session == nil { return }
|
||||
guard let player = player else { return }
|
||||
|
||||
// Prevent a sync at the current time
|
||||
let playerCurrentTime = player.getCurrentTime()
|
||||
if (lastSyncReport != nil && lastSyncReport?.currentTime == playerCurrentTime) {
|
||||
// No need to syncProgress
|
||||
return
|
||||
}
|
||||
let hasSyncAtCurrentTime = lastSyncReport?.currentTime.isEqual(to: playerCurrentTime) ?? false
|
||||
if hasSyncAtCurrentTime { return }
|
||||
|
||||
// Prevent multiple sync requests
|
||||
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
|
||||
@@ -157,16 +179,51 @@ class PlayerHandler {
|
||||
return
|
||||
}
|
||||
|
||||
lastSyncTime = Date().timeIntervalSince1970 // seconds
|
||||
// Prevent a sync if we got junk data from the player (occurs when exiting out of memory
|
||||
guard !playerCurrentTime.isNaN else { return }
|
||||
|
||||
lastSyncTime = Date().timeIntervalSince1970 // seconds
|
||||
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
|
||||
|
||||
session!.currentTime = playerCurrentTime
|
||||
listeningTimePassedSinceLastSync = 0
|
||||
lastSyncReport = report
|
||||
|
||||
// TODO: check if online
|
||||
NSLog("sending playback report")
|
||||
ApiClient.reportPlaybackProgress(report: report, sessionId: session!.id)
|
||||
let sessionIsLocal = session!.isLocal
|
||||
if !sessionIsLocal {
|
||||
if Connectivity.isConnectedToInternet {
|
||||
NSLog("sending playback report")
|
||||
ApiClient.reportPlaybackProgress(report: report, sessionId: session!.id)
|
||||
}
|
||||
} else {
|
||||
if let localMediaProgress = syncLocalProgress() {
|
||||
if Connectivity.isConnectedToInternet {
|
||||
ApiClient.reportLocalMediaProgress(localMediaProgress) { success in
|
||||
NSLog("Synced local media progress: \(success)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func syncLocalProgress() -> LocalMediaProgress? {
|
||||
guard let session = session else { return nil }
|
||||
|
||||
let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId)
|
||||
guard var localMediaProgress = localMediaProgress else {
|
||||
// Local media progress should have been created
|
||||
// If we're here, it means a library id is invalid
|
||||
return nil
|
||||
}
|
||||
|
||||
localMediaProgress.updateFromPlaybackSession(session)
|
||||
Database.shared.saveLocalMediaProgress(localMediaProgress)
|
||||
|
||||
NSLog("Local progress saved to the database")
|
||||
|
||||
// Send the local progress back to front-end
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil)
|
||||
|
||||
return localMediaProgress
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,29 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
public static func postResource(endpoint: String, parameters: [String: String], callback: ((_ success: Bool) -> Void)?) {
|
||||
|
||||
public static func postResource<T: Encodable, U: Decodable>(endpoint: String, parameters: T, decodable: U.Type = U.self, callback: ((_ param: U) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
return
|
||||
}
|
||||
|
||||
let headers: HTTPHeaders = [
|
||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||
]
|
||||
|
||||
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).responseDecodable(of: decodable) { response in
|
||||
switch response.result {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func postResource<T:Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
callback?(false)
|
||||
@@ -50,7 +72,7 @@ class ApiClient {
|
||||
|
||||
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).response { response in
|
||||
switch response.result {
|
||||
case .success(let _):
|
||||
case .success(_):
|
||||
callback?(true)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
@@ -60,6 +82,30 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func patchResource<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
callback?(false)
|
||||
return
|
||||
}
|
||||
|
||||
let headers: HTTPHeaders = [
|
||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||
]
|
||||
|
||||
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .patch, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).response { response in
|
||||
switch response.result {
|
||||
case .success(_):
|
||||
callback?(true)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
print(error)
|
||||
callback?(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
@@ -96,7 +142,7 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
ApiClient.postResource(endpoint: endpoint, parameters: [
|
||||
let parameters: [String: Any] = [
|
||||
"forceDirectPlay": !forceTranscode ? "1" : "",
|
||||
"forceTranscode": forceTranscode ? "1" : "",
|
||||
"mediaPlayer": "AVPlayer",
|
||||
@@ -105,7 +151,8 @@ class ApiClient {
|
||||
"model": modelCode,
|
||||
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
]
|
||||
], decodable: PlaybackSession.self) { obj in
|
||||
]
|
||||
ApiClient.postResource(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { obj in
|
||||
var session = obj
|
||||
|
||||
session.serverConnectionConfigId = Store.serverConfig!.id
|
||||
@@ -119,6 +166,44 @@ class ApiClient {
|
||||
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil)
|
||||
}
|
||||
|
||||
public static func reportLocalMediaProgress(_ localMediaProgress: LocalMediaProgress, callback: @escaping (_ success: Bool) -> Void) {
|
||||
postResource(endpoint: "api/session/local", parameters: localMediaProgress, callback: callback)
|
||||
}
|
||||
|
||||
public static func syncMediaProgress(callback: @escaping (_ results: LocalMediaProgressSyncResultsPayload) -> Void) {
|
||||
let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter {
|
||||
$0.serverConnectionConfigId == Store.serverConfig?.id
|
||||
}
|
||||
|
||||
if ( !localMediaProgressList.isEmpty ) {
|
||||
let payload = LocalMediaProgressSyncPayload(localMediaProgress: localMediaProgressList)
|
||||
NSLog("Sending sync local progress request with \(localMediaProgressList.count) progress items")
|
||||
postResource(endpoint: "api/me/sync-local-progress", parameters: payload, decodable: MediaProgressSyncResponsePayload.self) { response in
|
||||
let resultsPayload = LocalMediaProgressSyncResultsPayload(numLocalMediaProgressForServer: localMediaProgressList.count, numServerProgressUpdates: response.numServerProgressUpdates, numLocalProgressUpdates: response.localProgressUpdates?.count)
|
||||
NSLog("Media Progress Sync | \(String(describing: try? resultsPayload.asDictionary()))")
|
||||
|
||||
if let updates = response.localProgressUpdates {
|
||||
for update in updates {
|
||||
Database.shared.saveLocalMediaProgress(update)
|
||||
}
|
||||
}
|
||||
|
||||
callback(resultsPayload)
|
||||
}
|
||||
} else {
|
||||
NSLog("No local media progress to sync")
|
||||
callback(LocalMediaProgressSyncResultsPayload(numLocalMediaProgressForServer: 0, numServerProgressUpdates: 0, numLocalProgressUpdates: 0))
|
||||
}
|
||||
}
|
||||
|
||||
public static func updateMediaProgress<T:Encodable>(libraryItemId: String, episodeId: String?, payload: T, callback: @escaping () -> Void) {
|
||||
NSLog("updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)")
|
||||
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
|
||||
patchResource(endpoint: endpoint, parameters: payload) { success in
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
public static func getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, callback: @escaping (_ param: LibraryItem?) -> Void) {
|
||||
var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress"
|
||||
if episodeId != nil {
|
||||
@@ -130,3 +215,35 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalMediaProgressSyncPayload: Codable {
|
||||
var localMediaProgress: [LocalMediaProgress]
|
||||
}
|
||||
|
||||
struct MediaProgressSyncResponsePayload: Decodable {
|
||||
var numServerProgressUpdates: Int?
|
||||
var localProgressUpdates: [LocalMediaProgress]?
|
||||
|
||||
private enum CodingKeys : String, CodingKey {
|
||||
case numServerProgressUpdates, localProgressUpdates
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
numServerProgressUpdates = try? values.intOrStringDecoder(key: .numServerProgressUpdates)
|
||||
localProgressUpdates = try? values.decode([LocalMediaProgress].self, forKey: .localProgressUpdates)
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalMediaProgressSyncResultsPayload: Codable {
|
||||
var numLocalMediaProgressForServer: Int?
|
||||
var numServerProgressUpdates: Int?
|
||||
var numLocalProgressUpdates: Int?
|
||||
}
|
||||
|
||||
struct Connectivity {
|
||||
static private let sharedInstance = NetworkReachabilityManager()!
|
||||
static var isConnectedToInternet:Bool {
|
||||
return self.sharedInstance.isReachable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +116,14 @@ class Database {
|
||||
return Array(realm.objects(LocalLibraryItem.self))
|
||||
}
|
||||
|
||||
public func getLocalLibraryItemByLLId(libraryItem: String) -> LocalLibraryItem? {
|
||||
public func getLocalLibraryItem(byServerLibraryItemId: String) -> LocalLibraryItem? {
|
||||
let realm = try! Realm()
|
||||
return realm.objects(LocalLibraryItem.self).first(where: { $0.libraryItemId == libraryItem })
|
||||
return realm.objects(LocalLibraryItem.self).first(where: { $0.libraryItemId == byServerLibraryItemId })
|
||||
}
|
||||
|
||||
public func getLocalLibraryItem(localLibraryItem: String) -> LocalLibraryItem? {
|
||||
public func getLocalLibraryItem(localLibraryItemId: String) -> LocalLibraryItem? {
|
||||
let realm = try! Realm()
|
||||
return realm.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItem)
|
||||
return realm.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItemId)
|
||||
}
|
||||
|
||||
public func saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) {
|
||||
@@ -131,6 +131,19 @@ 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)
|
||||
}
|
||||
|
||||
public func getDownloadItem(downloadItemId: String) -> DownloadItem? {
|
||||
let realm = try! Realm()
|
||||
return realm.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId)
|
||||
@@ -166,17 +179,14 @@ class Database {
|
||||
return realm.objects(DeviceSettings.self).first ?? getDefaultDeviceSettings()
|
||||
}
|
||||
|
||||
public func removeLocalLibraryItem(localLibraryItemId: String) {
|
||||
public func getAllLocalMediaProgress() -> [LocalMediaProgress] {
|
||||
let realm = try! Realm()
|
||||
try! realm.write {
|
||||
let item = getLocalLibraryItemByLLId(libraryItem: localLibraryItemId)
|
||||
realm.delete(item!)
|
||||
}
|
||||
return Array(realm.objects(LocalMediaProgress.self))
|
||||
}
|
||||
|
||||
public func saveLocalMediaProgress(_ mediaProgress: LocalMediaProgress) {
|
||||
let realm = try! Realm()
|
||||
try! realm.write { realm.add(mediaProgress) }
|
||||
try! realm.write { realm.add(mediaProgress, update: .modified) }
|
||||
}
|
||||
|
||||
// For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}"
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import RealmSwift
|
||||
import Capacitor
|
||||
import CoreMedia
|
||||
|
||||
extension String: Error {}
|
||||
|
||||
@@ -31,6 +32,34 @@ extension Collection where Iterator.Element: Encodable {
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyedDecodingContainer {
|
||||
func doubleOrStringDecoder(key: KeyedDecodingContainer<K>.Key) throws -> Double {
|
||||
do {
|
||||
return try decode(Double.self, forKey: key)
|
||||
} catch {
|
||||
let stringValue = try decode(String.self, forKey: key)
|
||||
return Double(stringValue) ?? 0.0
|
||||
}
|
||||
}
|
||||
|
||||
func intOrStringDecoder(key: KeyedDecodingContainer<K>.Key) throws -> Int {
|
||||
do {
|
||||
return try decode(Int.self, forKey: key)
|
||||
} catch {
|
||||
let stringValue = try decode(String.self, forKey: key)
|
||||
return Int(stringValue) ?? 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CAPPluginCall {
|
||||
func getJson<T: Decodable>(_ key: String, type: T.Type) -> T? {
|
||||
guard let value = getObject(key) else { return nil }
|
||||
guard let json = try? JSONSerialization.data(withJSONObject: value) else { return nil }
|
||||
return try? JSONDecoder().decode(type, from: json)
|
||||
}
|
||||
}
|
||||
|
||||
extension DispatchQueue {
|
||||
static func runOnMainQueue(callback: @escaping (() -> Void)) {
|
||||
if Thread.isMainThread {
|
||||
|
||||
@@ -15,6 +15,10 @@ struct NowPlayingMetadata {
|
||||
var title: String
|
||||
var author: String?
|
||||
var series: String?
|
||||
var coverUrl: URL? {
|
||||
guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(itemId)/cover?token=\(Store.serverConfig!.token)") else { return nil }
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
class NowPlayingInfo {
|
||||
@@ -30,18 +34,27 @@ class NowPlayingInfo {
|
||||
public func setSessionMetadata(metadata: NowPlayingMetadata) {
|
||||
setMetadata(artwork: nil, metadata: metadata)
|
||||
|
||||
guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(metadata.itemId)/cover?token=\(Store.serverConfig!.token)") else {
|
||||
return
|
||||
}
|
||||
ApiClient.getData(from: url) { [self] image in
|
||||
guard let downloadedImage = image else {
|
||||
return
|
||||
let isLocalItem = metadata.itemId.starts(with: "local_")
|
||||
if isLocalItem {
|
||||
guard let artworkUrl = metadata.artworkUrl else { return }
|
||||
let coverImage = UIImage(contentsOfFile: artworkUrl)
|
||||
guard let coverImage = coverImage else { return }
|
||||
let artwork = MPMediaItemArtwork(boundsSize: coverImage.size) { _ -> UIImage in
|
||||
return coverImage
|
||||
}
|
||||
let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in
|
||||
return downloadedImage
|
||||
})
|
||||
|
||||
self.setMetadata(artwork: artwork, metadata: metadata)
|
||||
} else {
|
||||
guard let url = metadata.coverUrl else { return }
|
||||
ApiClient.getData(from: url) { [self] image in
|
||||
guard let downloadedImage = image else {
|
||||
return
|
||||
}
|
||||
let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in
|
||||
return downloadedImage
|
||||
})
|
||||
|
||||
self.setMetadata(artwork: artwork, metadata: metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
public func update(duration: Double, currentTime: Double, rate: Float) {
|
||||
@@ -52,6 +65,7 @@ class NowPlayingInfo {
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
nowPlayingInfo = [:]
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
||||
@@ -76,6 +90,7 @@ class NowPlayingInfo {
|
||||
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
|
||||
}
|
||||
|
||||
private func shouldFetchCover(id: String) -> Bool {
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != id || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil
|
||||
}
|
||||
|
||||
@@ -13,4 +13,5 @@ enum PlayerEvents: String {
|
||||
case sleepSet = "com.audiobookshelf.app.player.sleep.set"
|
||||
case sleepEnded = "com.audiobookshelf.app.player.sleep.ended"
|
||||
case failed = "com.audiobookshelf.app.player.failed"
|
||||
case localProgress = "com.audiobookshelf.app.player.localProgress"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user