mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-31 15:47:13 +02:00
Merge branch 'ios-downloads' into ios-downloads-realm-native
This commit is contained in:
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
|
|
||||||
<div v-if="!isIos && userCanDownload">
|
<div v-if="userCanDownload">
|
||||||
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
|
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
|
||||||
<span v-else-if="!localEpisode" class="material-icons mx-1 mt-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
|
<span v-else-if="!localEpisode" class="material-icons mx-1 mt-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
|
||||||
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
|
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
|
||||||
@@ -143,7 +143,12 @@ export default {
|
|||||||
},
|
},
|
||||||
downloadClick() {
|
downloadClick() {
|
||||||
if (this.downloadItem) return
|
if (this.downloadItem) return
|
||||||
this.download()
|
if (this.isIos) {
|
||||||
|
// no local folders on iOS
|
||||||
|
this.startDownload()
|
||||||
|
} else {
|
||||||
|
this.download()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async download(selectedLocalFolder = null) {
|
async download(selectedLocalFolder = null) {
|
||||||
var localFolder = selectedLocalFolder
|
var localFolder = selectedLocalFolder
|
||||||
@@ -183,7 +188,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async startDownload(localFolder) {
|
async startDownload(localFolder) {
|
||||||
var downloadRes = await AbsDownloader.downloadLibraryItem({ libraryItemId: this.libraryItemId, localFolderId: localFolder.id, episodeId: this.episode.id })
|
var payload = {
|
||||||
|
libraryItemId: this.libraryItemId,
|
||||||
|
episodeId: this.episode.id
|
||||||
|
}
|
||||||
|
if (localFolder) {
|
||||||
|
this.localFolderId = localFolder.id
|
||||||
|
}
|
||||||
|
var downloadRes = await AbsDownloader.downloadLibraryItem(payload)
|
||||||
if (downloadRes && downloadRes.error) {
|
if (downloadRes && downloadRes.error) {
|
||||||
var errorMsg = downloadRes.error || 'Unknown error'
|
var errorMsg = downloadRes.error || 'Unknown error'
|
||||||
console.error('Download error', errorMsg)
|
console.error('Download error', errorMsg)
|
||||||
|
|||||||
@@ -34,18 +34,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||||
|
NSLog("Audiobookself is now in the background")
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||||
|
NSLog("Audiobookself is now in the foreground")
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||||
|
NSLog("Audiobookself is now active")
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ application: UIApplication) {
|
func applicationWillTerminate(_ application: UIApplication) {
|
||||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||||
|
NSLog("Audiobookself is terminating")
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
NotificationCenter.default.addObserver(self, selector: #selector(sendSleepTimerSet), name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(sendSleepTimerSet), name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(sendSleepTimerEnded), name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(sendSleepTimerEnded), name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(onPlaybackFailed), name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(onPlaybackFailed), name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(onLocalMediaProgressUpdate), name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil)
|
||||||
|
|
||||||
self.bridge?.webView?.allowsBackForwardNavigationGestures = true;
|
self.bridge?.webView?.allowsBackForwardNavigationGestures = true;
|
||||||
|
|
||||||
@@ -44,12 +45,20 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
|
|
||||||
let isLocalItem = libraryItemId?.starts(with: "local_") ?? false
|
let isLocalItem = libraryItemId?.starts(with: "local_") ?? false
|
||||||
if (isLocalItem) {
|
if (isLocalItem) {
|
||||||
let item = Database.shared.getLocalLibraryItem(localLibraryItem: libraryItemId!)
|
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!)
|
||||||
// TODO: Logic required for podcasts here
|
let episode = item?.getPodcastEpisode(episodeId: episodeId)
|
||||||
let playbackSession = item?.getPlaybackSession(episode: nil)
|
let playbackSession = item?.getPlaybackSession(episode: episode)
|
||||||
|
sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady)
|
||||||
|
do {
|
||||||
|
self.sendPlaybackSession(session: try playbackSession.asDictionary())
|
||||||
|
call.resolve(try playbackSession.asDictionary())
|
||||||
|
} catch(let exception) {
|
||||||
|
NSLog("failed to convert session to json")
|
||||||
|
debugPrint(exception)
|
||||||
|
call.resolve([:])
|
||||||
|
}
|
||||||
PlayerHandler.startPlayback(session: playbackSession!, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
PlayerHandler.startPlayback(session: playbackSession!, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||||
self.sendMetadata()
|
self.sendMetadata()
|
||||||
call.resolve()
|
|
||||||
} else { // Playing from the server
|
} else { // Playing from the server
|
||||||
sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady)
|
sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady)
|
||||||
ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId, forceTranscode: false) { session in
|
ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId, forceTranscode: false) { session in
|
||||||
@@ -62,12 +71,12 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
call.resolve([:])
|
call.resolve([:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
PlayerHandler.startPlayback(session: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
PlayerHandler.startPlayback(session: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||||
self.sendMetadata()
|
self.sendMetadata()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func closePlayback(_ call: CAPPluginCall) {
|
@objc func closePlayback(_ call: CAPPluginCall) {
|
||||||
NSLog("Close playback")
|
NSLog("Close playback")
|
||||||
|
|
||||||
@@ -171,12 +180,21 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
"value": PlayerHandler.getCurrentTime()
|
"value": PlayerHandler.getCurrentTime()
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func sendSleepTimerSet() {
|
@objc func sendSleepTimerSet() {
|
||||||
self.notifyListeners("onSleepTimerSet", data: [
|
self.notifyListeners("onSleepTimerSet", data: [
|
||||||
"value": PlayerHandler.remainingSleepTime
|
"value": PlayerHandler.remainingSleepTime
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func onLocalMediaProgressUpdate() {
|
||||||
|
guard let localMediaProgressId = PlayerHandler.getPlaybackSession()?.localMediaProgressId else { return }
|
||||||
|
guard let localMediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) else { return }
|
||||||
|
guard let progressUpdate = try? localMediaProgress.asDictionary() else { return }
|
||||||
|
NSLog("Sending local progress back to the UI")
|
||||||
|
self.notifyListeners("onLocalMediaProgressUpdate", data: progressUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
@objc func onPlaybackFailed() {
|
@objc func onPlaybackFailed() {
|
||||||
if (PlayerHandler.getPlayMethod() == PlayMethod.directplay.rawValue) {
|
if (PlayerHandler.getPlayMethod() == PlayMethod.directplay.rawValue) {
|
||||||
let playbackSession = PlayerHandler.getPlaybackSession()
|
let playbackSession = PlayerHandler.getPlaybackSession()
|
||||||
@@ -211,6 +229,7 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
"playWhenReady": playWhenReady,
|
"playWhenReady": playWhenReady,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func sendPlaybackSession(session: [String: Any]) {
|
@objc func sendPlaybackSession(session: [String: Any]) {
|
||||||
self.notifyListeners("onPlaybackSession", data: session)
|
self.notifyListeners("onPlaybackSession", data: session)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ CAP_PLUGIN(AbsDatabase, "AbsDatabase",
|
|||||||
CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(getAllLocalMediaProgress, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getAllLocalMediaProgress, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(removeLocalMediaProgress, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(syncLocalMediaProgressWithServer, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(syncServerMediaProgressWithLocalMediaProgress, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(updateLocalMediaProgressFinished, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Capacitor
|
import Capacitor
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ public class AbsDatabase: CAPPlugin {
|
|||||||
|
|
||||||
@objc func getLocalLibraryItem(_ call: CAPPluginCall) {
|
@objc func getLocalLibraryItem(_ call: CAPPluginCall) {
|
||||||
do {
|
do {
|
||||||
let item = Database.shared.getLocalLibraryItem(localLibraryItem: call.getString("id") ?? "")
|
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: call.getString("id") ?? "")
|
||||||
switch item {
|
switch item {
|
||||||
case .some(let foundItem):
|
case .some(let foundItem):
|
||||||
call.resolve(try foundItem.asDictionary())
|
call.resolve(try foundItem.asDictionary())
|
||||||
@@ -104,7 +105,7 @@ public class AbsDatabase: CAPPlugin {
|
|||||||
|
|
||||||
@objc func getLocalLibraryItemByLId(_ call: CAPPluginCall) {
|
@objc func getLocalLibraryItemByLId(_ call: CAPPluginCall) {
|
||||||
do {
|
do {
|
||||||
let item = Database.shared.getLocalLibraryItemByLLId(libraryItem: call.getString("libraryItemId") ?? "")
|
let item = Database.shared.getLocalLibraryItem(byServerLibraryItemId: call.getString("libraryItemId") ?? "")
|
||||||
switch item {
|
switch item {
|
||||||
case .some(let foundItem):
|
case .some(let foundItem):
|
||||||
call.resolve(try foundItem.asDictionary())
|
call.resolve(try foundItem.asDictionary())
|
||||||
@@ -121,9 +122,107 @@ public class AbsDatabase: CAPPlugin {
|
|||||||
@objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) {
|
@objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) {
|
||||||
call.resolve([ "value": [] ])
|
call.resolve([ "value": [] ])
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getAllLocalMediaProgress(_ call: CAPPluginCall) {
|
@objc func getAllLocalMediaProgress(_ call: CAPPluginCall) {
|
||||||
call.resolve([ "value": [] ])
|
do {
|
||||||
|
call.resolve([ "value": try Database.shared.getAllLocalMediaProgress().asDictionaryArray() ])
|
||||||
|
} catch {
|
||||||
|
NSLog("Error while loading local media progress")
|
||||||
|
debugPrint(error)
|
||||||
|
call.resolve(["value": []])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func removeLocalMediaProgress(_ call: CAPPluginCall) {
|
||||||
|
let localMediaProgressId = call.getString("localMediaProgressId")
|
||||||
|
guard let localMediaProgressId = localMediaProgressId else {
|
||||||
|
call.reject("localMediaProgressId not specificed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Database.shared.removeLocalMediaProgress(localMediaProgressId: localMediaProgressId)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func syncLocalMediaProgressWithServer(_ call: CAPPluginCall) {
|
||||||
|
guard Store.serverConfig != nil else {
|
||||||
|
call.reject("syncLocalMediaProgressWithServer not connected to server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ApiClient.syncMediaProgress { results in
|
||||||
|
do {
|
||||||
|
call.resolve(try results.asDictionary())
|
||||||
|
} catch {
|
||||||
|
call.reject("Failed to report synced media progress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func syncServerMediaProgressWithLocalMediaProgress(_ call: CAPPluginCall) {
|
||||||
|
let serverMediaProgress = call.getJson("mediaProgress", type: MediaProgress.self)
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
let localEpisodeId = call.getString("localEpisodeId")
|
||||||
|
let localMediaProgressId = call.getString("localMediaProgressId")
|
||||||
|
|
||||||
|
do {
|
||||||
|
guard let serverMediaProgress = serverMediaProgress else {
|
||||||
|
return call.reject("serverMediaProgress not specified")
|
||||||
|
}
|
||||||
|
guard localLibraryItemId != nil || localMediaProgressId != nil else {
|
||||||
|
return call.reject("localLibraryItemId or localMediaProgressId must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
|
||||||
|
guard var localMediaProgress = localMediaProgress else {
|
||||||
|
call.reject("Local media progress not found or created")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localMediaProgress.updateFromServerMediaProgress(serverMediaProgress)
|
||||||
|
|
||||||
|
NSLog("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress")
|
||||||
|
Database.shared.saveLocalMediaProgress(localMediaProgress)
|
||||||
|
call.resolve(try localMediaProgress.asDictionary())
|
||||||
|
} catch {
|
||||||
|
call.reject("Failed to sync media progress")
|
||||||
|
debugPrint(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func updateLocalMediaProgressFinished(_ call: CAPPluginCall) {
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
let localEpisodeId = call.getString("localEpisodeId")
|
||||||
|
let localMediaProgressId = call.getString("localMediaProgressId")
|
||||||
|
let isFinished = call.getBool("isFinished", false)
|
||||||
|
|
||||||
|
NSLog("updateLocalMediaProgressFinished \(localMediaProgressId ?? "Unknown") | Is Finished: \(isFinished)")
|
||||||
|
|
||||||
|
let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
|
||||||
|
guard var localMediaProgress = localMediaProgress else {
|
||||||
|
call.resolve(["error": "Library Item not found"])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update finished status
|
||||||
|
localMediaProgress.updateIsFinished(isFinished)
|
||||||
|
Database.shared.saveLocalMediaProgress(localMediaProgress)
|
||||||
|
|
||||||
|
// Build API response
|
||||||
|
let progressDictionary = try? localMediaProgress.asDictionary()
|
||||||
|
var response: [String: Any] = ["local": true, "server": false, "localMediaProgress": progressDictionary ?? ""]
|
||||||
|
|
||||||
|
// Send update to the server if logged in
|
||||||
|
let hasLinkedServer = localMediaProgress.serverConnectionConfigId != nil
|
||||||
|
let loggedIntoServer = Store.serverConfig?.id == localMediaProgress.serverConnectionConfigId
|
||||||
|
if hasLinkedServer && loggedIntoServer {
|
||||||
|
response["server"] = true
|
||||||
|
let payload = ["isFinished": isFinished]
|
||||||
|
ApiClient.updateMediaProgress(libraryItemId: localMediaProgress.libraryItemId!, episodeId: localEpisodeId, payload: payload) {
|
||||||
|
call.resolve(response)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
call.resolve(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc func updateDeviceSettings(_ call: CAPPluginCall) {
|
@objc func updateDeviceSettings(_ call: CAPPluginCall) {
|
||||||
let disableAutoRewind = call.getBool("disableAutoRewind") ?? false
|
let disableAutoRewind = call.getBool("disableAutoRewind") ?? false
|
||||||
let enableAltView = call.getBool("enableAltView") ?? false
|
let enableAltView = call.getBool("enableAltView") ?? false
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import RealmSwift
|
|||||||
@objc(AbsDownloader)
|
@objc(AbsDownloader)
|
||||||
public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
|
|
||||||
static let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
|
||||||
typealias DownloadProgressHandler = (_ downloadItem: DownloadItem, _ downloadItemPart: inout DownloadItemPart) throws -> Void
|
typealias DownloadProgressHandler = (_ downloadItem: DownloadItem, _ downloadItemPart: inout DownloadItemPart) throws -> Void
|
||||||
|
|
||||||
@@ -23,11 +23,11 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
|||||||
}()
|
}()
|
||||||
private let progressStatusQueue = DispatchQueue(label: "progress-status-queue", attributes: .concurrent)
|
private let progressStatusQueue = DispatchQueue(label: "progress-status-queue", attributes: .concurrent)
|
||||||
private var downloadItemProgress = [String: DownloadItem]()
|
private var downloadItemProgress = [String: DownloadItem]()
|
||||||
private var isMonitoringProgress = false
|
private var monitoringProgressTimer: Timer?
|
||||||
|
|
||||||
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||||
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
|
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
|
||||||
downloadItemPart.progress = 1
|
downloadItemPart.progress = 100
|
||||||
downloadItemPart.completed = true
|
downloadItemPart.completed = true
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -103,47 +103,49 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
|||||||
|
|
||||||
// We want to handle updating the UI in the background and throttled so we don't overload the UI with progress updates
|
// We want to handle updating the UI in the background and throttled so we don't overload the UI with progress updates
|
||||||
private func notifyDownloadProgress() {
|
private func notifyDownloadProgress() {
|
||||||
if !self.isMonitoringProgress {
|
if self.monitoringProgressTimer == nil {
|
||||||
self.isMonitoringProgress = true
|
NSLog("Already monitoring progress, no need to start timer again")
|
||||||
DispatchQueue.global(qos: .userInteractive).async {
|
} else {
|
||||||
NSLog("Starting monitoring download progress...")
|
DispatchQueue.runOnMainQueue {
|
||||||
|
self.monitoringProgressTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true, block: { t in
|
||||||
|
NSLog("Starting monitoring download progress...")
|
||||||
|
|
||||||
// Fetch active downloads in a thread-safe way
|
// Fetch active downloads in a thread-safe way
|
||||||
func fetchActiveDownloads() -> [String: DownloadItem]? {
|
func fetchActiveDownloads() -> [String: DownloadItem]? {
|
||||||
self.progressStatusQueue.sync { self.downloadItemProgress }
|
self.progressStatusQueue.sync {
|
||||||
}
|
let activeDownloads = self.downloadItemProgress
|
||||||
|
if activeDownloads.isEmpty {
|
||||||
// Remove a completed download item in a thread-safe way
|
NSLog("Finishing monitoring download progress...")
|
||||||
func handleDoneDownloadItem(_ item: DownloadItem) {
|
t.invalidate()
|
||||||
self.progressStatusQueue.async(flags: .barrier) {
|
}
|
||||||
self.downloadItemProgress.removeValue(forKey: item.id!)
|
return activeDownloads
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Database.shared.removeDownloadItem(item)
|
|
||||||
self.handleDownloadTaskCompleteFromDownloadItem(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// While there are active download items, emit status updates
|
// Remove a completed download item in a thread-safe way
|
||||||
while !(fetchActiveDownloads()?.isEmpty ?? false) {
|
func handleDoneDownloadItem(_ item: DownloadItem) {
|
||||||
|
self.progressStatusQueue.async(flags: .barrier) {
|
||||||
|
self.downloadItemProgress.removeValue(forKey: item.id!)
|
||||||
|
}
|
||||||
|
Database.shared.removeDownloadItem(item)
|
||||||
|
self.handleDownloadTaskCompleteFromDownloadItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit status for active downloads
|
||||||
if let activeDownloads = fetchActiveDownloads() {
|
if let activeDownloads = fetchActiveDownloads() {
|
||||||
for item in activeDownloads.values {
|
for item in activeDownloads.values {
|
||||||
try? self.notifyListeners("onItemDownloadUpdate", data: item.asDictionary())
|
try? self.notifyListeners("onItemDownloadUpdate", data: item.asDictionary())
|
||||||
if item.isDoneDownloading() { handleDoneDownloadItem(item) }
|
if item.isDoneDownloading() { handleDoneDownloadItem(item) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
// Wait 200ms before reporting status again
|
|
||||||
Thread.sleep(forTimeInterval: TimeInterval(0.2))
|
|
||||||
}
|
|
||||||
|
|
||||||
NSLog("Finished monitoring download progress...")
|
|
||||||
self.isMonitoringProgress = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleDownloadTaskCompleteFromDownloadItem(_ downloadItem: DownloadItem) {
|
private func handleDownloadTaskCompleteFromDownloadItem(_ downloadItem: DownloadItem) {
|
||||||
var statusNotification = [String: Any]()
|
var statusNotification = [String: Any]()
|
||||||
statusNotification["libraryItemId"] = downloadItem.libraryItemId
|
statusNotification["libraryItemId"] = downloadItem.id
|
||||||
|
|
||||||
if ( downloadItem.didDownloadSuccessfully() ) {
|
if ( downloadItem.didDownloadSuccessfully() ) {
|
||||||
ApiClient.getLibraryItemWithProgress(libraryItemId: downloadItem.libraryItemId!, episodeId: downloadItem.episodeId) { libraryItem in
|
ApiClient.getLibraryItemWithProgress(libraryItemId: downloadItem.libraryItemId!, episodeId: downloadItem.episodeId) { libraryItem in
|
||||||
@@ -155,19 +157,22 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
|||||||
let files = downloadItem.downloadItemParts.enumerated().compactMap { _, part -> LocalFile? in
|
let files = downloadItem.downloadItemParts.enumerated().compactMap { _, part -> LocalFile? in
|
||||||
if part.filename == "cover.jpg" {
|
if part.filename == "cover.jpg" {
|
||||||
coverFile = part.destinationUri
|
coverFile = part.destinationUri
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return LocalFile(libraryItem.id, part.filename!, part.mimeType()!, part.destinationUri!, fileSize: Int(part.destinationURL!.fileSize))
|
|
||||||
}
|
}
|
||||||
|
return LocalFile(libraryItem.id, part.filename!, part.mimeType()!, part.destinationUri!, fileSize: Int(part.destinationURL!.fileSize))
|
||||||
|
}
|
||||||
|
var localLibraryItem = Database.shared.getLocalLibraryItem(byServerLibraryItemId: libraryItem.id)
|
||||||
|
if (localLibraryItem != nil && localLibraryItem!.isPodcast) {
|
||||||
|
try! localLibraryItem?.addFiles(files, item: libraryItem)
|
||||||
|
} else {
|
||||||
|
localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files, coverPath: coverFile)
|
||||||
}
|
}
|
||||||
let localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files, coverPath: coverFile)
|
|
||||||
|
|
||||||
Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem)
|
Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem!)
|
||||||
statusNotification["localLibraryItem"] = try? localLibraryItem.asDictionary()
|
statusNotification["localLibraryItem"] = try? localLibraryItem.asDictionary()
|
||||||
|
|
||||||
if let progress = libraryItem.userMediaProgress {
|
if let progress = libraryItem.userMediaProgress {
|
||||||
// TODO: Handle podcast
|
let episode = downloadItem.media?.episodes?.first(where: { $0.id == downloadItem.episodeId })
|
||||||
let localMediaProgress = LocalMediaProgress(localLibraryItem: localLibraryItem, episode: nil, progress: progress)
|
let localMediaProgress = LocalMediaProgress(localLibraryItem: localLibraryItem!, episode: episode, progress: progress)
|
||||||
Database.shared.saveLocalMediaProgress(localMediaProgress)
|
Database.shared.saveLocalMediaProgress(localMediaProgress)
|
||||||
statusNotification["localMediaProgress"] = try? localMediaProgress.asDictionary()
|
statusNotification["localMediaProgress"] = try? localMediaProgress.asDictionary()
|
||||||
}
|
}
|
||||||
@@ -237,7 +242,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
|||||||
// Queue up everything for downloading
|
// Queue up everything for downloading
|
||||||
let downloadItem = DownloadItem(libraryItem: item, episodeId: episodeId, server: Store.serverConfig!)
|
let downloadItem = DownloadItem(libraryItem: item, episodeId: episodeId, server: Store.serverConfig!)
|
||||||
for (i, track) in tracks.enumerated() {
|
for (i, track) in tracks.enumerated() {
|
||||||
downloadItem.downloadItemParts.append(try startLibraryItemTrackDownload(item: item, position: i, track: track))
|
downloadItem.downloadItemParts.append(try startLibraryItemTrackDownload(item: item, position: i, track: track, episode: episode))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also download the cover
|
// Also download the cover
|
||||||
@@ -256,7 +261,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack) throws -> DownloadItemPart {
|
private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPart {
|
||||||
NSLog("TRACK \(track.contentUrl!)")
|
NSLog("TRACK \(track.contentUrl!)")
|
||||||
|
|
||||||
// If we don't name metadata, then we can't proceed
|
// If we don't name metadata, then we can't proceed
|
||||||
@@ -269,7 +274,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
|||||||
let localUrl = "\(itemDirectory)/\(filename)"
|
let localUrl = "\(itemDirectory)/\(filename)"
|
||||||
|
|
||||||
let task = session.downloadTask(with: serverUrl)
|
let task = session.downloadTask(with: serverUrl)
|
||||||
let downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: nil)
|
var downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode)
|
||||||
|
|
||||||
// Store the id on the task so the download item can be pulled from the database later
|
// Store the id on the task so the download item can be pulled from the database later
|
||||||
task.taskDescription = downloadItemPart.id
|
task.taskDescription = downloadItemPart.id
|
||||||
@@ -305,16 +310,34 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
|||||||
let itemDirectory = item.id
|
let itemDirectory = item.id
|
||||||
NSLog("ITEM DIR \(itemDirectory)")
|
NSLog("ITEM DIR \(itemDirectory)")
|
||||||
|
|
||||||
do {
|
guard AbsDownloader.itemDownloadFolder(path: itemDirectory) != nil else {
|
||||||
try FileManager.default.createDirectory(at: AbsDownloader.downloadsDirectory.appendingPathComponent(itemDirectory), withIntermediateDirectories: true)
|
NSLog("Failed to CREATE LI DIRECTORY \(itemDirectory)")
|
||||||
} catch {
|
|
||||||
NSLog("Failed to CREATE LI DIRECTORY \(error)")
|
|
||||||
throw LibraryItemDownloadError.failedDirectory
|
throw LibraryItemDownloadError.failedDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
return itemDirectory
|
return itemDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func itemDownloadFolder(path: String) -> URL? {
|
||||||
|
do {
|
||||||
|
var itemFolder = AbsDownloader.downloadsDirectory.appendingPathComponent(path)
|
||||||
|
|
||||||
|
if !FileManager.default.fileExists(atPath: itemFolder.path) {
|
||||||
|
try FileManager.default.createDirectory(at: itemFolder, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we don't backup download files to iCloud
|
||||||
|
var resourceValues = URLResourceValues()
|
||||||
|
resourceValues.isExcludedFromBackup = true
|
||||||
|
try itemFolder.setResourceValues(resourceValues)
|
||||||
|
|
||||||
|
return itemFolder
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to CREATE LI DIRECTORY \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LibraryItemDownloadError: String, Error {
|
enum LibraryItemDownloadError: String, Error {
|
||||||
@@ -322,6 +345,7 @@ enum LibraryItemDownloadError: String, Error {
|
|||||||
case noMetadata = "No metadata for track, unable to download"
|
case noMetadata = "No metadata for track, unable to download"
|
||||||
case libraryItemNotPodcast = "Library item is not a podcast but episode was requested"
|
case libraryItemNotPodcast = "Library item is not a podcast but episode was requested"
|
||||||
case podcastEpisodeNotFound = "Invalid podcast episode not found"
|
case podcastEpisodeNotFound = "Invalid podcast episode not found"
|
||||||
|
case podcastOnlySupported = "Only podcasts are supported for this function"
|
||||||
case unknownMediaType = "Unknown media type"
|
case unknownMediaType = "Unknown media type"
|
||||||
case failedDirectory = "Failed to create directory"
|
case failedDirectory = "Failed to create directory"
|
||||||
case failedDownload = "Failed to download item"
|
case failedDownload = "Failed to download item"
|
||||||
|
|||||||
@@ -13,79 +13,102 @@ public class AbsFileSystem: CAPPlugin {
|
|||||||
@objc func selectFolder(_ call: CAPPluginCall) {
|
@objc func selectFolder(_ call: CAPPluginCall) {
|
||||||
let mediaType = call.getString("mediaType")
|
let mediaType = call.getString("mediaType")
|
||||||
|
|
||||||
// TODO: Implement
|
|
||||||
NSLog("Select Folder for media type \(mediaType ?? "UNSET")")
|
NSLog("Select Folder for media type \(mediaType ?? "UNSET")")
|
||||||
|
|
||||||
call.resolve()
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func checkFolderPermission(_ call: CAPPluginCall) {
|
@objc func checkFolderPermission(_ call: CAPPluginCall) {
|
||||||
let folderUrl = call.getString("folderUrl")
|
let folderUrl = call.getString("folderUrl")
|
||||||
|
|
||||||
// TODO: Is this even necessary on iOS?
|
|
||||||
NSLog("checkFolderPermission for folder \(folderUrl ?? "UNSET")")
|
NSLog("checkFolderPermission for folder \(folderUrl ?? "UNSET")")
|
||||||
|
|
||||||
call.resolve([
|
call.unavailable("Not available on iOS")
|
||||||
"value": true
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func scanFolder(_ call: CAPPluginCall) {
|
@objc func scanFolder(_ call: CAPPluginCall) {
|
||||||
let folderId = call.getString("folderId")
|
let folderId = call.getString("folderId")
|
||||||
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
||||||
|
|
||||||
// TODO: Implement
|
|
||||||
NSLog("scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
NSLog("scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||||
|
|
||||||
call.resolve()
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func removeFolder(_ call: CAPPluginCall) {
|
@objc func removeFolder(_ call: CAPPluginCall) {
|
||||||
let folderId = call.getString("folderId")
|
let folderId = call.getString("folderId")
|
||||||
|
|
||||||
// TODO: Implement
|
|
||||||
NSLog("removeFolder \(folderId ?? "UNSET")")
|
NSLog("removeFolder \(folderId ?? "UNSET")")
|
||||||
|
|
||||||
call.resolve()
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func removeLocalLibraryItem(_ call: CAPPluginCall) {
|
@objc func removeLocalLibraryItem(_ call: CAPPluginCall) {
|
||||||
let localLibraryItemId = call.getString("localLibraryItemId")
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
|
||||||
// TODO: Implement
|
|
||||||
NSLog("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
|
NSLog("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
|
||||||
|
|
||||||
call.resolve()
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func scanLocalLibraryItem(_ call: CAPPluginCall) {
|
@objc func scanLocalLibraryItem(_ call: CAPPluginCall) {
|
||||||
let localLibraryItemId = call.getString("localLibraryItemId")
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
||||||
|
|
||||||
// TODO: Implement
|
|
||||||
NSLog("scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
NSLog("scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||||
|
|
||||||
call.resolve()
|
call.unavailable("Not available on iOS")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func deleteItem(_ call: CAPPluginCall) {
|
@objc func deleteItem(_ call: CAPPluginCall) {
|
||||||
let localLibraryItemId = call.getString("localLibraryItemId")
|
let localLibraryItemId = call.getString("id")
|
||||||
let contentUrl = call.getString("contentUrl")
|
let contentUrl = call.getString("contentUrl")
|
||||||
|
|
||||||
// TODO: Implement
|
|
||||||
NSLog("deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
NSLog("deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
||||||
|
|
||||||
call.resolve()
|
var success = false
|
||||||
|
do {
|
||||||
|
if let localLibraryItemId = localLibraryItemId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) {
|
||||||
|
try FileManager.default.removeItem(at: item.contentDirectory!)
|
||||||
|
Database.shared.removeLocalLibraryItem(localLibraryItemId: localLibraryItemId)
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to delete \(error)")
|
||||||
|
success = false
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve(["success": success])
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func deleteTrackFromItem(_ call: CAPPluginCall) {
|
@objc func deleteTrackFromItem(_ call: CAPPluginCall) {
|
||||||
let localLibraryItemId = call.getString("localLibraryItemId")
|
let localLibraryItemId = call.getString("id")
|
||||||
let trackLocalFileId = call.getString("trackLocalFileId")
|
let trackLocalFileId = call.getString("trackLocalFileId")
|
||||||
let contentUrl = call.getString("contentUrl")
|
|
||||||
|
|
||||||
// TODO: Implement
|
NSLog("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET")")
|
||||||
NSLog("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
|
||||||
|
|
||||||
call.resolve()
|
var success = false
|
||||||
|
do {
|
||||||
|
if let localLibraryItemId = localLibraryItemId, let trackLocalFileId = trackLocalFileId, var 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, var media = item.media {
|
||||||
|
media.episodes = media.episodes?.filter { $0.audioTrack?.localFileId != trackLocalFileId }
|
||||||
|
item.media = media
|
||||||
|
}
|
||||||
|
Database.shared.saveLocalLibraryItem(localLibraryItem: item)
|
||||||
|
call.resolve(try item.asDictionary())
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to delete \(error)")
|
||||||
|
success = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
call.resolve(["success": success])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ class PodcastEpisode: Object, Codable {
|
|||||||
@Persisted var audioTrack: AudioTrack?
|
@Persisted var audioTrack: AudioTrack?
|
||||||
@Persisted var duration: Double = 0
|
@Persisted var duration: Double = 0
|
||||||
@Persisted var size: Int = 0
|
@Persisted var size: Int = 0
|
||||||
// var serverEpisodeId: String?
|
var serverEpisodeId: String { self.id }
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
private enum CodingKeys : String, CodingKey {
|
||||||
case id,
|
case id,
|
||||||
@@ -277,14 +277,44 @@ class PodcastEpisode: Object, Codable {
|
|||||||
audioFile,
|
audioFile,
|
||||||
audioTrack,
|
audioTrack,
|
||||||
duration,
|
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 {
|
class AudioFile: Object, Codable {
|
||||||
@Persisted var index: Int = 0
|
@Persisted var index: Int?
|
||||||
@Persisted var ino: String = ""
|
@Persisted var ino: String = ""
|
||||||
@Persisted var metadata: FileMetadata?
|
@Persisted var metadata: FileMetadata?
|
||||||
|
|
||||||
@@ -299,7 +329,7 @@ class AudioFile: Object, Codable {
|
|||||||
required init(from decoder: Decoder) throws {
|
required init(from decoder: Decoder) throws {
|
||||||
super.init()
|
super.init()
|
||||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
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)
|
ino = try values.decode(String.self, forKey: .ino)
|
||||||
metadata = try? values.decode(FileMetadata.self, forKey: .metadata)
|
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(name, forKey: .name)
|
||||||
try container.encode(coverPath, forKey: .coverPath)
|
try container.encode(coverPath, forKey: .coverPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
class Chapter: Object, Codable {
|
class Chapter: Object, Codable {
|
||||||
@@ -381,8 +414,7 @@ class AudioTrack: Object, Codable {
|
|||||||
@Persisted var contentUrl: String?
|
@Persisted var contentUrl: String?
|
||||||
@Persisted var mimeType: String = ""
|
@Persisted var mimeType: String = ""
|
||||||
@Persisted var metadata: FileMetadata?
|
@Persisted var metadata: FileMetadata?
|
||||||
// var isLocal: Bool
|
var localFileId: String?
|
||||||
// var localFileId: String?
|
|
||||||
// var audioProbeResult: AudioProbeResult? Needed for local playback
|
// var audioProbeResult: AudioProbeResult? Needed for local playback
|
||||||
@Persisted var serverIndex: Int?
|
@Persisted var serverIndex: Int?
|
||||||
|
|
||||||
@@ -418,6 +450,20 @@ class AudioTrack: Object, Codable {
|
|||||||
try container.encode(metadata, forKey: .metadata)
|
try container.encode(metadata, forKey: .metadata)
|
||||||
try container.encode(serverIndex, forKey: .serverIndex)
|
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 {
|
class FileMetadata: Object, Codable {
|
||||||
@@ -562,18 +608,17 @@ class MediaProgress: Object, Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
required init(from decoder: Decoder) throws {
|
||||||
super.init()
|
|
||||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
id = try values.decode(String.self, forKey: .id)
|
id = try values.decode(String.self, forKey: .id)
|
||||||
libraryItemId = try values.decode(String.self, forKey: .libraryItemId)
|
libraryItemId = try values.decode(String.self, forKey: .libraryItemId)
|
||||||
episodeId = try? values.decode(String.self, forKey: .episodeId)
|
episodeId = try? values.decode(String.self, forKey: .episodeId)
|
||||||
duration = try values.decode(Double.self, forKey: .duration)
|
duration = try values.doubleOrStringDecoder(key: .duration)
|
||||||
progress = try values.decode(Double.self, forKey: .progress)
|
progress = try values.doubleOrStringDecoder(key: .progress)
|
||||||
currentTime = try values.decode(Double.self, forKey: .currentTime)
|
currentTime = try values.doubleOrStringDecoder(key: .currentTime)
|
||||||
isFinished = try values.decode(Bool.self, forKey: .isFinished)
|
isFinished = try values.decode(Bool.self, forKey: .isFinished)
|
||||||
lastUpdate = try values.decode(Int.self, forKey: .lastUpdate)
|
lastUpdate = try values.intOrStringDecoder(key: .lastUpdate)
|
||||||
startedAt = try values.decode(Int.self, forKey: .startedAt)
|
startedAt = try values.intOrStringDecoder(key: .startedAt)
|
||||||
finishedAt = try? values.decode(Int.self, forKey: .finishedAt)
|
finishedAt = try? values.intOrStringDecoder(key: .finishedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
|
|||||||
@@ -73,6 +73,35 @@ class DownloadItemPart: Object, Codable {
|
|||||||
@Persisted var uri: String?
|
@Persisted var uri: String?
|
||||||
@Persisted var destinationUri: String?
|
@Persisted var destinationUri: String?
|
||||||
@Persisted var progress: Double = 0
|
@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!
|
var task: URLSessionDownloadTask!
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
private enum CodingKeys : String, CodingKey {
|
||||||
@@ -105,3 +134,36 @@ class DownloadItemPart: Object, Codable {
|
|||||||
try container.encode(progress, forKey: .progress)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,33 @@ class LocalLibraryItem: Object, Codable {
|
|||||||
@Persisted var serverUserId: String?
|
@Persisted var serverUserId: String?
|
||||||
@Persisted(indexed: true) var libraryItemId: 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 {
|
private enum CodingKeys : String, CodingKey {
|
||||||
case id, basePath, contentUrl, isInvalid, mediaType, media, localFiles, coverContentUrl, isLocal, serverConnectionConfigId, serverAddress, serverUserId, libraryItemId
|
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)
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
id = try values.decode(String.self, forKey: .id)
|
id = try values.decode(String.self, forKey: .id)
|
||||||
basePath = try values.decode(String.self, forKey: .basePath)
|
basePath = try values.decode(String.self, forKey: .basePath)
|
||||||
contentUrl = try values.decode(String.self, forKey: .contentUrl)
|
|
||||||
isInvalid = try values.decode(Bool.self, forKey: .isInvalid)
|
isInvalid = try values.decode(Bool.self, forKey: .isInvalid)
|
||||||
mediaType = try values.decode(String.self, forKey: .mediaType)
|
mediaType = try values.decode(String.self, forKey: .mediaType)
|
||||||
media = try? values.decode(MediaType.self, forKey: .media)
|
media = try? values.decode(MediaType.self, forKey: .media)
|
||||||
if let files = try? values.decode([LocalFile].self, forKey: .localFiles) {
|
if let files = try? values.decode([LocalFile].self, forKey: .localFiles) {
|
||||||
localFiles.append(objectsIn: files)
|
localFiles.append(objectsIn: files)
|
||||||
}
|
}
|
||||||
_coverContentUrl = try values.decode(String.self, forKey: .coverContentUrl)
|
|
||||||
isLocal = try values.decode(Bool.self, forKey: .isLocal)
|
isLocal = try values.decode(Bool.self, forKey: .isLocal)
|
||||||
serverConnectionConfigId = try? values.decode(String.self, forKey: .serverConnectionConfigId)
|
serverConnectionConfigId = try? values.decode(String.self, forKey: .serverConnectionConfigId)
|
||||||
serverAddress = try? values.decode(String.self, forKey: .serverAddress)
|
serverAddress = try? values.decode(String.self, forKey: .serverAddress)
|
||||||
@@ -112,8 +137,12 @@ class LocalFile: Object, Codable {
|
|||||||
@Persisted var mimeType: String?
|
@Persisted var mimeType: String?
|
||||||
@Persisted var size: Int = 0
|
@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 {
|
private enum CodingKeys : String, CodingKey {
|
||||||
case id, filename, contentUrl, absolutePath, mimeType, size
|
case id, filename, contentUrl, mimeType, size, basePath
|
||||||
}
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
@@ -124,7 +153,6 @@ class LocalFile: Object, Codable {
|
|||||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
id = try values.decode(String.self, forKey: .id)
|
id = try values.decode(String.self, forKey: .id)
|
||||||
filename = try? values.decode(String.self, forKey: .filename)
|
filename = try? values.decode(String.self, forKey: .filename)
|
||||||
contentUrl = try values.decode(String.self, forKey: .contentUrl)
|
|
||||||
mimeType = try? values.decode(String.self, forKey: .mimeType)
|
mimeType = try? values.decode(String.self, forKey: .mimeType)
|
||||||
size = try values.decode(Int.self, forKey: .size)
|
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(id, forKey: .id)
|
||||||
try container.encode(filename, forKey: .filename)
|
try container.encode(filename, forKey: .filename)
|
||||||
try container.encode(contentUrl, forKey: .contentUrl)
|
try container.encode(contentUrl, forKey: .contentUrl)
|
||||||
try container.encode(absolutePath, forKey: .absolutePath)
|
|
||||||
try container.encode(mimeType, forKey: .mimeType)
|
try container.encode(mimeType, forKey: .mimeType)
|
||||||
try container.encode(size, forKey: .size)
|
try container.encode(size, forKey: .size)
|
||||||
|
try container.encode(basePath, forKey: .basePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +186,8 @@ class LocalMediaProgress: Object, Codable {
|
|||||||
@Persisted(indexed: true) var libraryItemId: String?
|
@Persisted(indexed: true) var libraryItemId: String?
|
||||||
@Persisted(indexed: true) var episodeId: String?
|
@Persisted(indexed: true) var episodeId: String?
|
||||||
|
|
||||||
|
var progressPercent: Int { Int(self.progress * 100) }
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
private enum CodingKeys : String, CodingKey {
|
||||||
case id, localLibraryItemId, localEpisodeId, duration, progress, currentTime, isFinished, lastUpdate, startedAt, finishedAt, serverConnectionConfigId, serverAddress, serverUserId, libraryItemId, episodeId
|
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.contentUrl = localUrl
|
||||||
self.mediaType = item.mediaType
|
self.mediaType = item.mediaType
|
||||||
self.media = item.media
|
|
||||||
self.localFiles.append(objectsIn: files)
|
self.localFiles.append(objectsIn: files)
|
||||||
self.coverContentUrl = coverPath
|
self._coverContentUrl = coverPath
|
||||||
self.libraryItemId = item.id
|
self.libraryItemId = item.id
|
||||||
self.serverConnectionConfigId = server.id
|
self.serverConnectionConfigId = server.id
|
||||||
self.serverAddress = server.address
|
self.serverAddress = server.address
|
||||||
self.serverUserId = server.userId
|
self.serverUserId = server.userId
|
||||||
|
|
||||||
|
// Link the audio tracks and files
|
||||||
|
linkLocalFiles(files, fromMedia: item.media)
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentUrl: String? {
|
mutating func addFiles(_ files: [LocalFile], item: LibraryItem) throws {
|
||||||
set(url) {
|
guard self.isPodcast else { throw LibraryItemDownloadError.podcastOnlySupported }
|
||||||
_contentUrl = url
|
self.localFiles.append(contentsOf: files.filter({ $0.isAudioFile() }))
|
||||||
}
|
linkLocalFiles(self.localFiles, fromMedia: item.media)
|
||||||
get {
|
|
||||||
if let path = _contentUrl {
|
|
||||||
return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var coverContentUrl: String? {
|
mutating private func linkLocalFiles(_ files: [LocalFile], fromMedia: MediaType) {
|
||||||
set(url) {
|
var fromMedia = fromMedia
|
||||||
_coverContentUrl = url
|
let fileMap = files.map { ($0.filename ?? "", $0.id) }
|
||||||
}
|
let fileIdByFilename = Dictionary(fileMap, uniquingKeysWith: { (_, last) in last })
|
||||||
get {
|
if ( self.isBook ) {
|
||||||
if let path = self._coverContentUrl {
|
if let tracks = fromMedia.tracks {
|
||||||
return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString
|
for i in tracks.indices {
|
||||||
} else {
|
_ = fromMedia.tracks?[i].setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: i)
|
||||||
return nil
|
}
|
||||||
|
}
|
||||||
|
} 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 {
|
func getDuration() -> Double {
|
||||||
@@ -54,7 +59,13 @@ extension LocalLibraryItem {
|
|||||||
return total
|
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 localEpisodeId = episode?.id
|
||||||
let sessionId = "play_local_\(UUID().uuidString)"
|
let sessionId = "play_local_\(UUID().uuidString)"
|
||||||
|
|
||||||
@@ -62,13 +73,13 @@ extension LocalLibraryItem {
|
|||||||
let mediaProgressId = (localEpisodeId != nil) ? "\(self.id)-\(localEpisodeId!)" : self.id
|
let mediaProgressId = (localEpisodeId != nil) ? "\(self.id)-\(localEpisodeId!)" : self.id
|
||||||
let mediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: mediaProgressId)
|
let mediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: mediaProgressId)
|
||||||
|
|
||||||
// TODO: Clean up add mediaType methods for displayTitle and displayAuthor
|
|
||||||
let mediaMetadata = self.media?.metadata
|
let mediaMetadata = self.media?.metadata
|
||||||
let audioTracks = self.media?.tracks
|
let chapters = self.media?.chapters
|
||||||
let authorName = mediaMetadata?.authorName
|
var audioTracks = self.media?.tracks
|
||||||
|
let authorName = mediaMetadata?.authorDisplayName
|
||||||
|
|
||||||
if let episode = episode {
|
if let episode = episode, let track = episode.audioTrack {
|
||||||
// TODO: Implement podcast
|
audioTracks = [track]
|
||||||
}
|
}
|
||||||
|
|
||||||
let dateNow = Date().timeIntervalSince1970
|
let dateNow = Date().timeIntervalSince1970
|
||||||
@@ -78,18 +89,19 @@ extension LocalLibraryItem {
|
|||||||
libraryItemId: self.libraryItemId,
|
libraryItemId: self.libraryItemId,
|
||||||
episodeId: episode?.serverEpisodeId,
|
episodeId: episode?.serverEpisodeId,
|
||||||
mediaType: self.mediaType,
|
mediaType: self.mediaType,
|
||||||
chapters: [],
|
chapters: chapters ?? [],
|
||||||
displayTitle: mediaMetadata?.title,
|
displayTitle: mediaMetadata?.title,
|
||||||
displayAuthor: authorName,
|
displayAuthor: authorName,
|
||||||
coverPath: nil,
|
coverPath: self.coverContentUrl,
|
||||||
duration: self.getDuration(),
|
duration: self.getDuration(),
|
||||||
playMethod: 3,
|
playMethod: PlayMethod.local.rawValue,
|
||||||
startedAt: dateNow,
|
startedAt: dateNow,
|
||||||
updatedAt: 0,
|
updatedAt: 0,
|
||||||
timeListening: 0.0,
|
timeListening: 0.0,
|
||||||
audioTracks: [],
|
audioTracks: audioTracks ?? [],
|
||||||
currentTime: mediaProgress?.currentTime ?? 0.0,
|
currentTime: mediaProgress?.currentTime ?? 0.0,
|
||||||
libraryItem: nil,
|
libraryItem: nil,
|
||||||
|
localLibraryItem: self,
|
||||||
serverConnectionConfigId: self.serverConnectionConfigId,
|
serverConnectionConfigId: self.serverConnectionConfigId,
|
||||||
serverAddress: self.serverAddress
|
serverAddress: self.serverAddress
|
||||||
)
|
)
|
||||||
@@ -103,7 +115,7 @@ extension LocalFile {
|
|||||||
self.id = "\(libraryItemId)_\(filename.toBase64())"
|
self.id = "\(libraryItemId)_\(filename.toBase64())"
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.mimeType = mimeType
|
self.mimeType = mimeType
|
||||||
self.contentUrl = localUrl
|
self._contentUrl = localUrl
|
||||||
self.size = fileSize
|
self.size = fileSize
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,27 +135,84 @@ extension LocalFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension LocalMediaProgress {
|
extension LocalMediaProgress {
|
||||||
convenience init(localLibraryItem: LocalLibraryItem, episode: LocalPodcastEpisode?, progress: MediaProgress) {
|
convenience init(localLibraryItem: LocalLibraryItem, episode: PodcastEpisode?) {
|
||||||
self.init()
|
self.init()
|
||||||
|
|
||||||
self.id = localLibraryItem.id
|
self.id = localLibraryItem.id
|
||||||
self.localLibraryItemId = localLibraryItem.id
|
self.localLibraryItemId = localLibraryItem.id
|
||||||
self.libraryItemId = localLibraryItem.libraryItemId
|
self.libraryItemId = localLibraryItem.libraryItemId
|
||||||
|
|
||||||
if let episode = episode {
|
|
||||||
self.id += "-\(episode.id)"
|
|
||||||
self.episodeId = episode.id
|
|
||||||
}
|
|
||||||
|
|
||||||
self.serverAddress = localLibraryItem.serverAddress
|
self.serverAddress = localLibraryItem.serverAddress
|
||||||
self.serverUserId = localLibraryItem.serverUserId
|
self.serverUserId = localLibraryItem.serverUserId
|
||||||
self.serverConnectionConfigId = localLibraryItem.serverConnectionConfigId
|
self.serverConnectionConfigId = localLibraryItem.serverConnectionConfigId
|
||||||
|
|
||||||
self.duration = progress.duration
|
self.duration = localLibraryItem.getDuration()
|
||||||
self.currentTime = progress.currentTime
|
self.progress = 0.0
|
||||||
|
self.currentTime = 0.0
|
||||||
self.isFinished = false
|
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.lastUpdate = progress.lastUpdate
|
||||||
self.startedAt = progress.startedAt
|
self.startedAt = progress.startedAt
|
||||||
self.finishedAt = progress.finishedAt
|
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
|
import Foundation
|
||||||
|
|
||||||
struct PlaybackSession: Decodable, Encodable {
|
struct PlaybackSession: Codable {
|
||||||
var id: String
|
var id: String
|
||||||
var userId: String?
|
var userId: String?
|
||||||
var libraryItemId: String?
|
var libraryItemId: String?
|
||||||
@@ -26,7 +26,25 @@ struct PlaybackSession: Decodable, Encodable {
|
|||||||
var audioTracks: [AudioTrack]
|
var audioTracks: [AudioTrack]
|
||||||
var currentTime: Double
|
var currentTime: Double
|
||||||
var libraryItem: LibraryItem?
|
var libraryItem: LibraryItem?
|
||||||
//var localLibraryItem: LocalLibraryItem?
|
var localLibraryItem: LocalLibraryItem?
|
||||||
var serverConnectionConfigId: String?
|
var serverConnectionConfigId: String?
|
||||||
var serverAddress: 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
|
let startOffset = audioTrack.startOffset ?? 0.0
|
||||||
return startOffset + currentTrackTime
|
return startOffset + currentTrackTime
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getPlayMethod() -> Int {
|
public func getPlayMethod() -> Int {
|
||||||
return self.playbackSession.playMethod
|
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 urlstr = "\(Store.serverConfig!.address)/s/item/\(itemId)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
|
||||||
let url = URL(string: urlstr)!
|
let url = URL(string: urlstr)!
|
||||||
return AVURLAsset(url: url)
|
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
|
} else { // HLS Transcode
|
||||||
let headers: [String: String] = [
|
let headers: [String: String] = [
|
||||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class PlayerHandler {
|
|||||||
private static var player: AudioPlayer?
|
private static var player: AudioPlayer?
|
||||||
private static var session: PlaybackSession?
|
private static var session: PlaybackSession?
|
||||||
private static var timer: Timer?
|
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
|
private static var _remainingSleepTime: Int? = nil
|
||||||
public static var remainingSleepTime: Int? {
|
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) {
|
public static func startPlayback(session: PlaybackSession, playWhenReady: Bool, playbackRate: Float) {
|
||||||
if player != nil {
|
if player != nil {
|
||||||
player?.destroy()
|
player?.destroy()
|
||||||
@@ -63,31 +79,34 @@ class PlayerHandler {
|
|||||||
self.session = session
|
self.session = session
|
||||||
player = AudioPlayer(playbackSession: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
player = AudioPlayer(playbackSession: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||||
|
|
||||||
DispatchQueue.runOnMainQueue {
|
startTickTimer()
|
||||||
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
|
||||||
self.tick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func stopPlayback() {
|
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?.destroy()
|
||||||
player = nil
|
player = nil
|
||||||
|
|
||||||
timer?.invalidate()
|
|
||||||
timer = nil
|
|
||||||
|
|
||||||
NowPlayingInfo.shared.reset()
|
NowPlayingInfo.shared.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getCurrentTime() -> Double? {
|
public static func getCurrentTime() -> Double? {
|
||||||
self.player?.getCurrentTime()
|
self.player?.getCurrentTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func setPlaybackSpeed(speed: Float) {
|
public static func setPlaybackSpeed(speed: Float) {
|
||||||
self.player?.setPlaybackRate(speed)
|
self.player?.setPlaybackRate(speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getPlayMethod() -> Int? {
|
public static func getPlayMethod() -> Int? {
|
||||||
self.player?.getPlayMethod()
|
self.player?.getPlayMethod()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getPlaybackSession() -> PlaybackSession? {
|
public static func getPlaybackSession() -> PlaybackSession? {
|
||||||
self.player?.getPlaybackSession()
|
self.player?.getPlaybackSession()
|
||||||
}
|
}
|
||||||
@@ -100,6 +119,7 @@ class PlayerHandler {
|
|||||||
let destinationTime = player.getCurrentTime() + amount
|
let destinationTime = player.getCurrentTime() + amount
|
||||||
player.seek(destinationTime, from: "handler")
|
player.seek(destinationTime, from: "handler")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func seekBackward(amount: Double) {
|
public static func seekBackward(amount: Double) {
|
||||||
guard let player = player else {
|
guard let player = player else {
|
||||||
return
|
return
|
||||||
@@ -108,9 +128,11 @@ class PlayerHandler {
|
|||||||
let destinationTime = player.getCurrentTime() - amount
|
let destinationTime = player.getCurrentTime() - amount
|
||||||
player.seek(destinationTime, from: "handler")
|
player.seek(destinationTime, from: "handler")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func seek(amount: Double) {
|
public static func seek(amount: Double) {
|
||||||
player?.seek(amount, from: "handler")
|
player?.seek(amount, from: "handler")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getMetdata() -> [String: Any] {
|
public static func getMetdata() -> [String: Any] {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
syncProgress()
|
syncProgress()
|
||||||
@@ -140,15 +162,15 @@ class PlayerHandler {
|
|||||||
remainingSleepTime! -= 1
|
remainingSleepTime! -= 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func syncProgress() {
|
public static func syncProgress() {
|
||||||
if session == nil { return }
|
if session == nil { return }
|
||||||
guard let player = player else { return }
|
guard let player = player else { return }
|
||||||
|
|
||||||
|
// Prevent a sync at the current time
|
||||||
let playerCurrentTime = player.getCurrentTime()
|
let playerCurrentTime = player.getCurrentTime()
|
||||||
if (lastSyncReport != nil && lastSyncReport?.currentTime == playerCurrentTime) {
|
let hasSyncAtCurrentTime = lastSyncReport?.currentTime.isEqual(to: playerCurrentTime) ?? false
|
||||||
// No need to syncProgress
|
if hasSyncAtCurrentTime { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent multiple sync requests
|
// Prevent multiple sync requests
|
||||||
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
|
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
|
||||||
@@ -157,16 +179,51 @@ class PlayerHandler {
|
|||||||
return
|
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)
|
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
|
||||||
|
|
||||||
session!.currentTime = playerCurrentTime
|
session!.currentTime = playerCurrentTime
|
||||||
listeningTimePassedSinceLastSync = 0
|
listeningTimePassedSinceLastSync = 0
|
||||||
lastSyncReport = report
|
lastSyncReport = report
|
||||||
|
|
||||||
// TODO: check if online
|
let sessionIsLocal = session!.isLocal
|
||||||
NSLog("sending playback report")
|
if !sessionIsLocal {
|
||||||
ApiClient.reportPlaybackProgress(report: report, sessionId: session!.id)
|
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) {
|
if (Store.serverConfig == nil) {
|
||||||
NSLog("Server config not set")
|
NSLog("Server config not set")
|
||||||
callback?(false)
|
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
|
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).response { response in
|
||||||
switch response.result {
|
switch response.result {
|
||||||
case .success(let _):
|
case .success(_):
|
||||||
callback?(true)
|
callback?(true)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
NSLog("api request to \(endpoint) failed")
|
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)?) {
|
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
NSLog("Server config not set")
|
NSLog("Server config not set")
|
||||||
@@ -96,7 +142,7 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiClient.postResource(endpoint: endpoint, parameters: [
|
let parameters: [String: Any] = [
|
||||||
"forceDirectPlay": !forceTranscode ? "1" : "",
|
"forceDirectPlay": !forceTranscode ? "1" : "",
|
||||||
"forceTranscode": forceTranscode ? "1" : "",
|
"forceTranscode": forceTranscode ? "1" : "",
|
||||||
"mediaPlayer": "AVPlayer",
|
"mediaPlayer": "AVPlayer",
|
||||||
@@ -105,7 +151,8 @@ class ApiClient {
|
|||||||
"model": modelCode,
|
"model": modelCode,
|
||||||
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
"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
|
var session = obj
|
||||||
|
|
||||||
session.serverConnectionConfigId = Store.serverConfig!.id
|
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)
|
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) {
|
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 {
|
||||||
@@ -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))
|
return Array(realm.objects(LocalLibraryItem.self))
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getLocalLibraryItemByLLId(libraryItem: String) -> LocalLibraryItem? {
|
public func getLocalLibraryItem(byServerLibraryItemId: String) -> LocalLibraryItem? {
|
||||||
let realm = try! Realm()
|
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()
|
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) {
|
public func saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) {
|
||||||
@@ -131,6 +131,19 @@ class Database {
|
|||||||
try! realm.write { realm.add(localLibraryItem, update: .modified) }
|
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? {
|
public func getDownloadItem(downloadItemId: String) -> DownloadItem? {
|
||||||
let realm = try! Realm()
|
let realm = try! Realm()
|
||||||
return realm.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId)
|
return realm.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId)
|
||||||
@@ -166,17 +179,14 @@ class Database {
|
|||||||
return realm.objects(DeviceSettings.self).first ?? getDefaultDeviceSettings()
|
return realm.objects(DeviceSettings.self).first ?? getDefaultDeviceSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeLocalLibraryItem(localLibraryItemId: String) {
|
public func getAllLocalMediaProgress() -> [LocalMediaProgress] {
|
||||||
let realm = try! Realm()
|
let realm = try! Realm()
|
||||||
try! realm.write {
|
return Array(realm.objects(LocalMediaProgress.self))
|
||||||
let item = getLocalLibraryItemByLLId(libraryItem: localLibraryItemId)
|
|
||||||
realm.delete(item!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func saveLocalMediaProgress(_ mediaProgress: LocalMediaProgress) {
|
public func saveLocalMediaProgress(_ mediaProgress: LocalMediaProgress) {
|
||||||
let realm = try! Realm()
|
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}"
|
// For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}"
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
|
import Capacitor
|
||||||
|
import CoreMedia
|
||||||
|
|
||||||
extension String: Error {}
|
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 {
|
extension DispatchQueue {
|
||||||
static func runOnMainQueue(callback: @escaping (() -> Void)) {
|
static func runOnMainQueue(callback: @escaping (() -> Void)) {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ struct NowPlayingMetadata {
|
|||||||
var title: String
|
var title: String
|
||||||
var author: String?
|
var author: String?
|
||||||
var series: 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 {
|
class NowPlayingInfo {
|
||||||
@@ -30,18 +34,27 @@ class NowPlayingInfo {
|
|||||||
public func setSessionMetadata(metadata: NowPlayingMetadata) {
|
public func setSessionMetadata(metadata: NowPlayingMetadata) {
|
||||||
setMetadata(artwork: nil, metadata: metadata)
|
setMetadata(artwork: nil, metadata: metadata)
|
||||||
|
|
||||||
guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(metadata.itemId)/cover?token=\(Store.serverConfig!.token)") else {
|
let isLocalItem = metadata.itemId.starts(with: "local_")
|
||||||
return
|
if isLocalItem {
|
||||||
}
|
guard let artworkUrl = metadata.artworkUrl else { return }
|
||||||
ApiClient.getData(from: url) { [self] image in
|
let coverImage = UIImage(contentsOfFile: artworkUrl)
|
||||||
guard let downloadedImage = image else {
|
guard let coverImage = coverImage else { return }
|
||||||
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)
|
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) {
|
public func update(duration: Double, currentTime: Double, rate: Float) {
|
||||||
@@ -52,6 +65,7 @@ class NowPlayingInfo {
|
|||||||
|
|
||||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reset() {
|
public func reset() {
|
||||||
nowPlayingInfo = [:]
|
nowPlayingInfo = [:]
|
||||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
||||||
@@ -76,6 +90,7 @@ class NowPlayingInfo {
|
|||||||
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
|
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
|
||||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
|
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
|
||||||
}
|
}
|
||||||
|
|
||||||
private func shouldFetchCover(id: String) -> Bool {
|
private func shouldFetchCover(id: String) -> Bool {
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != id || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil
|
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != id || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ enum PlayerEvents: String {
|
|||||||
case sleepSet = "com.audiobookshelf.app.player.sleep.set"
|
case sleepSet = "com.audiobookshelf.app.player.sleep.set"
|
||||||
case sleepEnded = "com.audiobookshelf.app.player.sleep.ended"
|
case sleepEnded = "com.audiobookshelf.app.player.sleep.ended"
|
||||||
case failed = "com.audiobookshelf.app.player.failed"
|
case failed = "com.audiobookshelf.app.player.failed"
|
||||||
|
case localProgress = "com.audiobookshelf.app.player.localProgress"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<span class="material-icons" @click="showItemDialog">more_vert</span>
|
<span class="material-icons" @click="showItemDialog">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="px-2 text-sm mb-0.5 text-white text-opacity-75">Folder: {{ folderName }}</p>
|
<p v-if="!isIos" class="px-2 text-sm mb-0.5 text-white text-opacity-75">Folder: {{ folderName }}</p>
|
||||||
|
|
||||||
<p class="px-2 mb-4 text-xs text-gray-400">{{ libraryItemId ? 'Linked to item on server ' + liServerAddress : 'Not linked to server item' }}</p>
|
<p class="px-2 mb-4 text-xs text-gray-400">{{ libraryItemId ? 'Linked to item on server ' + liServerAddress : 'Not linked to server item' }}</p>
|
||||||
|
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
<div v-if="!isPodcast" class="w-full">
|
<div v-if="!isPodcast" class="w-full">
|
||||||
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p>
|
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p>
|
||||||
|
|
||||||
<draggable v-model="audioTracksCopy" v-bind="dragOptions" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
<draggable v-model="audioTracksCopy" v-bind="dragOptions" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate" :disabled="isIos">
|
||||||
<transition-group type="transition" :name="!drag ? 'dragtrack' : null">
|
<transition-group type="transition" :name="!drag ? 'dragtrack' : null">
|
||||||
<template v-for="track in audioTracksCopy">
|
<template v-for="track in audioTracksCopy">
|
||||||
<div :key="track.localFileId" class="flex items-center my-1 item">
|
<div :key="track.localFileId" class="flex items-center my-1 item">
|
||||||
<div class="w-8 h-12 flex items-center justify-center" style="min-width: 32px">
|
<div v-if="!isIos" class="w-8 h-12 flex items-center justify-center" style="min-width: 32px">
|
||||||
<span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span>
|
<span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-8 h-12 flex items-center justify-center" style="min-width: 32px">
|
<div class="w-8 h-12 flex items-center justify-center" style="min-width: 32px">
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<p class="text-xs">{{ track.mimeType }}</p>
|
<p class="text-xs">{{ track.mimeType }}</p>
|
||||||
<p class="text-sm">{{ $elapsedPretty(track.duration) }}</p>
|
<p class="text-sm">{{ $elapsedPretty(track.duration) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-12 h-12 flex items-center justify-center" style="min-width: 48px">
|
<div v-if="!isIos" class="w-12 h-12 flex items-center justify-center" style="min-width: 48px">
|
||||||
<span class="material-icons" @click="showTrackDialog(track)">more_vert</span>
|
<span class="material-icons" @click="showTrackDialog(track)">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,6 +138,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
|
},
|
||||||
basePath() {
|
basePath() {
|
||||||
return this.localLibraryItem ? this.localLibraryItem.basePath : null
|
return this.localLibraryItem ? this.localLibraryItem.basePath : null
|
||||||
},
|
},
|
||||||
@@ -194,24 +197,14 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
return [
|
var options = []
|
||||||
{
|
if ( !this.isIos ) {
|
||||||
text: 'Scan',
|
options.push({ text: 'Scan', value: 'scan'})
|
||||||
value: 'scan'
|
options.push({ text: 'Force Re-Scan', value: 'rescan'})
|
||||||
},
|
options.push({ text: 'Remove', value: 'remove'})
|
||||||
{
|
}
|
||||||
text: 'Force Re-Scan',
|
options.push({ text: 'Remove & Delete Files', value: 'delete'})
|
||||||
value: 'rescan'
|
return options
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Remove',
|
|
||||||
value: 'remove'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Remove & Delete Files',
|
|
||||||
value: 'delete'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -329,13 +322,13 @@ export default {
|
|||||||
async deleteItem() {
|
async deleteItem() {
|
||||||
const { value } = await Dialog.confirm({
|
const { value } = await Dialog.confirm({
|
||||||
title: 'Confirm',
|
title: 'Confirm',
|
||||||
message: `Warning! This will delete the folder "${this.basePath}" and all contents. Are you sure?`
|
message: `Warning! This will delete "${this.media.metadata.title}" and all associated local files. Are you sure?`
|
||||||
})
|
})
|
||||||
if (value) {
|
if (value) {
|
||||||
var res = await AbsFileSystem.deleteItem(this.localLibraryItem)
|
var res = await AbsFileSystem.deleteItem(this.localLibraryItem)
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
this.$toast.success('Deleted Successfully')
|
this.$toast.success('Deleted Successfully')
|
||||||
this.$router.replace(`/localMedia/folders/${this.folderId}`)
|
this.$router.replace(this.isIos ? '/bookshelf' : `/localMedia/folders/${this.folderId}`)
|
||||||
} else this.$toast.error('Failed to delete')
|
} else this.$toast.error('Failed to delete')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user