mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-28 22:08:47 +02:00
Rewrite downloader to use delegate and download item
This commit is contained in:
parent
33041608f8
commit
d5d65e244b
6 changed files with 222 additions and 123 deletions
|
@ -38,7 +38,7 @@
|
||||||
C4B265F5285A5A6600E1B5C3 /* LocalLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */; };
|
C4B265F5285A5A6600E1B5C3 /* LocalLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */; };
|
||||||
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D0677428106D0C00B8F875 /* DataClasses.swift */; };
|
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D0677428106D0C00B8F875 /* DataClasses.swift */; };
|
||||||
E99C8C932883A00F00E3279A /* LocalLibraryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */; };
|
E99C8C932883A00F00E3279A /* LocalLibraryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */; };
|
||||||
E9D38158289A0A6F0019EEED /* LibraryItemDownloadSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D38157289A0A6F0019EEED /* LibraryItemDownloadSession.swift */; };
|
E9D3815C289E0C9B0019EEED /* DownloadItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D3815B289E0C9B0019EEED /* DownloadItem.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalLibrary.swift; sourceTree = "<group>"; };
|
C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalLibrary.swift; sourceTree = "<group>"; };
|
||||||
C4D0677428106D0C00B8F875 /* DataClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataClasses.swift; sourceTree = "<group>"; };
|
C4D0677428106D0C00B8F875 /* DataClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataClasses.swift; sourceTree = "<group>"; };
|
||||||
E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalLibraryExtensions.swift; sourceTree = "<group>"; };
|
E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalLibraryExtensions.swift; sourceTree = "<group>"; };
|
||||||
E9D38157289A0A6F0019EEED /* LibraryItemDownloadSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryItemDownloadSession.swift; sourceTree = "<group>"; };
|
E9D3815B289E0C9B0019EEED /* DownloadItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadItem.swift; sourceTree = "<group>"; };
|
||||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
@ -143,6 +143,7 @@
|
||||||
C4D0677428106D0C00B8F875 /* DataClasses.swift */,
|
C4D0677428106D0C00B8F875 /* DataClasses.swift */,
|
||||||
C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */,
|
C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */,
|
||||||
E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */,
|
E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */,
|
||||||
|
E9D3815B289E0C9B0019EEED /* DownloadItem.swift */,
|
||||||
3A90295E280968E700E1D427 /* PlaybackReport.swift */,
|
3A90295E280968E700E1D427 /* PlaybackReport.swift */,
|
||||||
4DF74911287105C600AC7814 /* DeviceSettings.swift */,
|
4DF74911287105C600AC7814 /* DeviceSettings.swift */,
|
||||||
);
|
);
|
||||||
|
@ -158,7 +159,6 @@
|
||||||
3AF1970B2806E2590096F747 /* ApiClient.swift */,
|
3AF1970B2806E2590096F747 /* ApiClient.swift */,
|
||||||
3AB34052280829BF0039308B /* Extensions.swift */,
|
3AB34052280829BF0039308B /* Extensions.swift */,
|
||||||
3AB34054280832720039308B /* PlayerEvents.swift */,
|
3AB34054280832720039308B /* PlayerEvents.swift */,
|
||||||
E9D38157289A0A6F0019EEED /* LibraryItemDownloadSession.swift */,
|
|
||||||
);
|
);
|
||||||
path = util;
|
path = util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -335,6 +335,7 @@
|
||||||
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */,
|
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */,
|
||||||
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
|
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
|
||||||
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
|
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
|
||||||
|
E9D3815C289E0C9B0019EEED /* DownloadItem.swift in Sources */,
|
||||||
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
||||||
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
||||||
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */,
|
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */,
|
||||||
|
@ -344,7 +345,6 @@
|
||||||
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */,
|
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */,
|
||||||
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */,
|
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */,
|
||||||
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
|
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
|
||||||
E9D38158289A0A6F0019EEED /* LibraryItemDownloadSession.swift in Sources */,
|
|
||||||
E99C8C932883A00F00E3279A /* LocalLibraryExtensions.swift in Sources */,
|
E99C8C932883A00F00E3279A /* LocalLibraryExtensions.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -49,6 +49,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
Realm.registerRealmables(LocalFile.self)
|
Realm.registerRealmables(LocalFile.self)
|
||||||
Realm.registerRealmables(LocalMediaProgress.self)
|
Realm.registerRealmables(LocalMediaProgress.self)
|
||||||
|
|
||||||
|
// Download item
|
||||||
|
Realm.registerRealmables(DownloadItem.self)
|
||||||
|
Realm.registerRealmables(DownloadItemPart.self)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,37 +9,129 @@ import Foundation
|
||||||
import Capacitor
|
import Capacitor
|
||||||
|
|
||||||
@objc(AbsDownloader)
|
@objc(AbsDownloader)
|
||||||
public class AbsDownloader: CAPPlugin {
|
public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
|
|
||||||
|
private let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
private lazy var session = URLSession(configuration: .default, delegate: self, delegateQueue: .current)
|
||||||
|
|
||||||
|
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||||
|
NSLog("Finished downloading \(downloadTask.taskDescription ?? "Unknown Task")")
|
||||||
|
|
||||||
|
guard let downloadItemPartId = downloadTask.taskDescription else { return }
|
||||||
|
|
||||||
|
let downloadItem = Database.shared.getDownloadItem(downloadItemPartId: downloadItemPartId)
|
||||||
|
guard let downloadItem = downloadItem else {
|
||||||
|
NSLog("Download item part (%@) not found! Unable to move file!", downloadItemPartId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog("Found downloadItem(%@)", downloadItem.id)
|
||||||
|
self.moveLibraryItemToFileDirectory(tempUrl: location)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||||
|
NSLog("Error downloading \(task.taskDescription ?? "Unknown Task"): \(error ?? "Unknown Error")")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||||
|
NSLog("Received download status \(downloadTask.taskDescription ?? "Unknown Task"): \(totalBytesWritten)")
|
||||||
|
}
|
||||||
|
|
||||||
@objc func downloadLibraryItem(_ call: CAPPluginCall) {
|
@objc func downloadLibraryItem(_ call: CAPPluginCall) {
|
||||||
let libraryItemId = call.getString("libraryItemId")
|
let libraryItemId = call.getString("libraryItemId")
|
||||||
let episodeId = call.getString("episodeId")
|
let episodeId = call.getString("episodeId")
|
||||||
|
|
||||||
NSLog("Download library item \(libraryItemId ?? "N/A") / episode \(episodeId ?? "")")
|
NSLog("Download library item \(libraryItemId ?? "N/A") / episode \(episodeId ?? "")")
|
||||||
|
guard let libraryItemId = libraryItemId else { call.resolve(); return; }
|
||||||
|
|
||||||
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId!, episodeId: episodeId) { libraryItem in
|
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId, episodeId: episodeId) { libraryItem in
|
||||||
if (libraryItem == nil) {
|
if (libraryItem == nil) {
|
||||||
NSLog("Library item not found")
|
NSLog("Library item not found")
|
||||||
call.resolve()
|
|
||||||
} else {
|
} else {
|
||||||
NSLog("Got library item from server \(libraryItem!.id)")
|
NSLog("Got library item from server \(libraryItem!.id)")
|
||||||
Task {
|
do {
|
||||||
do {
|
try self.startLibraryItemDownload(libraryItem!)
|
||||||
let downloadSession = LibraryItemDownloadSession(libraryItem!)
|
//Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem)
|
||||||
let localLibraryItem = try await downloadSession.startDownload()
|
} catch {
|
||||||
Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem)
|
NSLog("Failed to download \(error)")
|
||||||
} catch {
|
|
||||||
NSLog("Failed to download \(error)")
|
|
||||||
}
|
|
||||||
call.resolve()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func startLibraryItemDownload(_ item: LibraryItem) throws {
|
||||||
|
guard let tracks = item.media.tracks else {
|
||||||
|
throw LibraryItemDownloadError.noTracks
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue up everything for downloading
|
||||||
|
var downloadItem = DownloadItem(libraryItem: item, server: Store.serverConfig!)
|
||||||
|
downloadItem.downloadItemParts = try tracks.enumerated().map({ i, track in
|
||||||
|
try startLibraryItemTrackDownload(item: item, position: i, track: track)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Persist in the database before status start coming in
|
||||||
|
Database.shared.saveDownloadItem(downloadItem)
|
||||||
|
|
||||||
|
// Start all the downloads
|
||||||
|
for downloadItemPart in downloadItem.downloadItemParts {
|
||||||
|
downloadItemPart.task.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack) throws -> DownloadItemPart {
|
||||||
|
NSLog("TRACK \(track.contentUrl!)")
|
||||||
|
|
||||||
|
// If we don't name metadata, then we can't proceed
|
||||||
|
guard let filename = track.metadata?.filename else {
|
||||||
|
throw LibraryItemDownloadError.noMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
let serverUrl = urlForTrack(item: item, track: track)
|
||||||
|
let itemDirectory = try createLibraryItemFileDirectory(item: item)
|
||||||
|
let localUrl = itemDirectory.appendingPathComponent("\(filename)")
|
||||||
|
|
||||||
|
let task = session.downloadTask(with: serverUrl)
|
||||||
|
var downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: nil)
|
||||||
|
|
||||||
|
// Store the id on the task so the download item can be pulled from the database later
|
||||||
|
task.taskDescription = downloadItemPart.id
|
||||||
|
downloadItemPart.task = task
|
||||||
|
|
||||||
|
return downloadItemPart
|
||||||
|
}
|
||||||
|
|
||||||
|
private func urlForTrack(item: LibraryItem, track: AudioTrack) -> URL {
|
||||||
|
// filename needs to be encoded otherwise would just use contentUrl
|
||||||
|
let filenameEncoded = track.metadata?.filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
|
||||||
|
let urlstr = "\(Store.serverConfig!.address)/s/item/\(item.id)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
|
||||||
|
return URL(string: urlstr)!
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createLibraryItemFileDirectory(item: LibraryItem) throws -> URL {
|
||||||
|
let itemDirectory = documentsDirectory.appendingPathComponent("\(item.id)")
|
||||||
|
NSLog("ITEM DIR \(itemDirectory)")
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.createDirectory(at: itemDirectory, withIntermediateDirectories: true)
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to CREATE LI DIRECTORY \(error)")
|
||||||
|
throw LibraryItemDownloadError.failedDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
private func moveLibraryItemToFileDirectory(tempUrl: URL) {
|
||||||
|
//try FileManager.default.moveItem(at: tempUrl, to: localUrl)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DownloadItem: Codable {
|
enum LibraryItemDownloadError: String, Error {
|
||||||
var isDownloading = false
|
case noTracks = "No tracks on library item"
|
||||||
var progress: Float = 0
|
case noMetadata = "No metadata for track, unable to download"
|
||||||
var resumeData: Data?
|
case failedDirectory = "Failed to create directory"
|
||||||
// var task: URLSessionDownloadTask?
|
case failedDownload = "Failed to download item"
|
||||||
}
|
}
|
||||||
|
|
81
ios/App/Shared/models/DownloadItem.swift
Normal file
81
ios/App/Shared/models/DownloadItem.swift
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
//
|
||||||
|
// DownloadItem.swift
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by Ron Heft on 8/5/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Unrealm
|
||||||
|
|
||||||
|
struct DownloadItem: Realmable, Codable {
|
||||||
|
var id: String = UUID().uuidString
|
||||||
|
var libraryItemId: String?
|
||||||
|
var episodeId: String?
|
||||||
|
var userMediaProgress: MediaProgress?
|
||||||
|
var serverConnectionConfigId: String?
|
||||||
|
var serverAddress: String?
|
||||||
|
var serverUserId: String?
|
||||||
|
var mediaType: String?
|
||||||
|
var itemTitle: String?
|
||||||
|
var media: MediaType?
|
||||||
|
var downloadItemParts: [DownloadItemPart] = []
|
||||||
|
|
||||||
|
static func primaryKey() -> String? {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func indexedProperties() -> [String] {
|
||||||
|
["libraryItemId"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DownloadItem {
|
||||||
|
init(libraryItem: LibraryItem, server: ServerConnectionConfig) {
|
||||||
|
self.libraryItemId = libraryItem.id
|
||||||
|
//self.episodeId // TODO
|
||||||
|
self.userMediaProgress = libraryItem.userMediaProgress
|
||||||
|
self.serverConnectionConfigId = server.id
|
||||||
|
self.serverAddress = server.address
|
||||||
|
self.serverUserId = server.userId
|
||||||
|
self.mediaType = libraryItem.mediaType
|
||||||
|
self.itemTitle = libraryItem.media.metadata.title
|
||||||
|
self.media = libraryItem.media
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DownloadItemPart: Realmable, Codable {
|
||||||
|
var id: String = UUID().uuidString
|
||||||
|
var filename: String?
|
||||||
|
var finalDestinationPath: 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 destinationUri: String?
|
||||||
|
var finalDestinationUri: String?
|
||||||
|
var downloadId: Int?
|
||||||
|
var progress: Int = 0
|
||||||
|
var task: URLSessionDownloadTask!
|
||||||
|
|
||||||
|
private enum CodingKeys : String, CodingKey {
|
||||||
|
case id, progress
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ignoredProperties() -> [String] {
|
||||||
|
["task"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DownloadItemPart {
|
||||||
|
init(filename: String, destination: URL, itemTitle: String, serverPath: String, audioTrack:AudioTrack?, episode: PodcastEpisode?) {
|
||||||
|
var downloadUrl = "" // TODO: Set this
|
||||||
|
if (serverPath.hasSuffix("/cover")) {
|
||||||
|
downloadUrl += "&format=jpeg" // For cover images force to jpeg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -145,6 +145,30 @@ class Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func getDownloadItem(downloadItemId: String) -> DownloadItem? {
|
||||||
|
Database.realmQueue.sync {
|
||||||
|
instance.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getDownloadItem(libraryItemId: String) -> DownloadItem? {
|
||||||
|
Database.realmQueue.sync {
|
||||||
|
instance.objects(DownloadItem.self).filter("libraryItemId == %@", libraryItemId).first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getDownloadItem(downloadItemPartId: String) -> DownloadItem? {
|
||||||
|
Database.realmQueue.sync {
|
||||||
|
instance.objects(DownloadItem.self).filter("SUBQUERY(downloadItemParts, $part, $part.id == %@) .@count > 0", downloadItemPartId).first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func saveDownloadItem(_ downloadItem: DownloadItem) {
|
||||||
|
Database.realmQueue.sync {
|
||||||
|
try! instance.write { instance.add(downloadItem) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func getDeviceSettings() -> DeviceSettings {
|
public func getDeviceSettings() -> DeviceSettings {
|
||||||
return Database.realmQueue.sync {
|
return Database.realmQueue.sync {
|
||||||
return instance.objects(DeviceSettings.self).first ?? getDefaultDeviceSettings()
|
return instance.objects(DeviceSettings.self).first ?? getDefaultDeviceSettings()
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
//
|
|
||||||
// LibraryItemDownloadSession.swift
|
|
||||||
// App
|
|
||||||
//
|
|
||||||
// Created by Ron Heft on 8/2/22.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum LibraryItemDownloadError: String, Error {
|
|
||||||
case noTracks = "No tracks on library item"
|
|
||||||
case noMetadata = "No metadata for track, unable to download"
|
|
||||||
case failedDownload = "Failed to download item"
|
|
||||||
}
|
|
||||||
|
|
||||||
class LibraryItemDownloadSession {
|
|
||||||
|
|
||||||
let item: LibraryItem
|
|
||||||
|
|
||||||
private let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
||||||
|
|
||||||
init(_ item: LibraryItem) {
|
|
||||||
self.item = item
|
|
||||||
}
|
|
||||||
|
|
||||||
public func startDownload() async throws -> LocalLibraryItem {
|
|
||||||
guard let tracks = item.media.tracks else {
|
|
||||||
throw LibraryItemDownloadError.noTracks
|
|
||||||
}
|
|
||||||
|
|
||||||
return try await withThrowingTaskGroup(of: LocalFile.self, returning: LocalLibraryItem.self) { group in
|
|
||||||
for (position, track) in tracks.enumerated() {
|
|
||||||
group.addTask { try await self.startLibraryItemTrackDownload(item: self.item, position: position, track: track) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var files = [LocalFile]()
|
|
||||||
for try await file in group {
|
|
||||||
files.append(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
return LocalLibraryItem(self.item, localUrl: self.documentsDirectory, server: Store.serverConfig!, files: files)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack) async throws -> LocalFile {
|
|
||||||
NSLog("TRACK \(track.contentUrl!)")
|
|
||||||
|
|
||||||
// If we don't name metadata, then we can't proceed
|
|
||||||
guard let filename = track.metadata?.filename else {
|
|
||||||
throw LibraryItemDownloadError.noMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
let serverUrl = urlForTrack(item: item, track: track)
|
|
||||||
let itemDirectory = createLibraryItemFileDirectory(item: item)
|
|
||||||
let localUrl = itemDirectory.appendingPathComponent("\(filename)")
|
|
||||||
|
|
||||||
try await downloadFile(serverUrl: serverUrl, localUrl: localUrl)
|
|
||||||
return LocalFile(item.id, filename, track.mimeType, localUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createLibraryItemFileDirectory(item: LibraryItem) -> URL {
|
|
||||||
let itemDirectory = documentsDirectory.appendingPathComponent("\(item.id)")
|
|
||||||
|
|
||||||
NSLog("ITEM DIR \(itemDirectory)")
|
|
||||||
|
|
||||||
// Create library item directory
|
|
||||||
do {
|
|
||||||
try FileManager.default.createDirectory(at: itemDirectory, withIntermediateDirectories: true)
|
|
||||||
} catch {
|
|
||||||
NSLog("Failed to CREATE LI DIRECTORY \(error)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return itemDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
private func urlForTrack(item: LibraryItem, track: AudioTrack) -> URL {
|
|
||||||
// filename needs to be encoded otherwise would just use contentUrl
|
|
||||||
let filenameEncoded = track.metadata?.filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
|
|
||||||
let urlstr = "\(Store.serverConfig!.address)/s/item/\(item.id)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
|
|
||||||
return URL(string: urlstr)!
|
|
||||||
}
|
|
||||||
|
|
||||||
private func downloadFile(serverUrl: URL, localUrl: URL) async throws {
|
|
||||||
return try await withCheckedThrowingContinuation { continuation in
|
|
||||||
let downloadTask = URLSession.shared.downloadTask(with: serverUrl) { urlOrNil, responseOrNil, errorOrNil in
|
|
||||||
guard let tempUrl = urlOrNil else {
|
|
||||||
continuation.resume(throwing: errorOrNil!)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
try FileManager.default.moveItem(at: tempUrl, to: localUrl)
|
|
||||||
continuation.resume()
|
|
||||||
} catch {
|
|
||||||
continuation.resume(throwing: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
downloadTask.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue