mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-04 18:15:01 +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
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 {
|
||||
return Database.realmQueue.sync {
|
||||
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