mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-31 16:14:59 +02:00
Handle a documents directory that can change
Thanks iOS
This commit is contained in:
parent
8e2be4704e
commit
e9961f64a9
4 changed files with 137 additions and 53 deletions
|
@ -11,9 +11,10 @@ import Capacitor
|
||||||
@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]
|
||||||
|
|
||||||
typealias DownloadProgressHandler = (_ downloadItem: DownloadItem, _ downloadItemPart: inout DownloadItemPart) throws -> Void
|
typealias DownloadProgressHandler = (_ downloadItem: DownloadItem, _ downloadItemPart: inout DownloadItemPart) throws -> Void
|
||||||
|
|
||||||
private let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
||||||
private lazy var session: URLSession = {
|
private lazy var session: URLSession = {
|
||||||
let queue = OperationQueue()
|
let queue = OperationQueue()
|
||||||
queue.maxConcurrentOperationCount = 5
|
queue.maxConcurrentOperationCount = 5
|
||||||
|
@ -30,7 +31,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// Move the downloaded file into place
|
// Move the downloaded file into place
|
||||||
guard let destinationUrl = downloadItemPart.destinationURL() else {
|
guard let destinationUrl = downloadItemPart.destinationURL else {
|
||||||
throw LibraryItemDownloadError.downloadItemPartDestinationUrlNotDefined
|
throw LibraryItemDownloadError.downloadItemPartDestinationUrlNotDefined
|
||||||
}
|
}
|
||||||
try? FileManager.default.removeItem(at: destinationUrl)
|
try? FileManager.default.removeItem(at: destinationUrl)
|
||||||
|
@ -142,31 +143,25 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleDownloadTaskCompleteFromDownloadItem(_ downloadItem: DownloadItem) {
|
private func handleDownloadTaskCompleteFromDownloadItem(_ downloadItem: DownloadItem) {
|
||||||
|
var statusNotification = [String: Any]()
|
||||||
|
statusNotification["libraryItemId"] = downloadItem.libraryItemId
|
||||||
|
|
||||||
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
|
||||||
var statusNotification = [String: Any]()
|
|
||||||
statusNotification["libraryItemId"] = libraryItem?.id
|
|
||||||
|
|
||||||
guard let libraryItem = libraryItem else { NSLog("LibraryItem not found"); return }
|
guard let libraryItem = libraryItem else { NSLog("LibraryItem not found"); return }
|
||||||
let localDirectory = self.documentsDirectory.appendingPathComponent("\(libraryItem.id)")
|
let localDirectory = libraryItem.id
|
||||||
var coverFile: URL?
|
var coverFile: String?
|
||||||
|
|
||||||
// Assemble the local library item
|
// Assemble the local library item
|
||||||
let files = downloadItem.downloadItemParts.compactMap { part -> LocalFile? in
|
let files = downloadItem.downloadItemParts.compactMap { part -> LocalFile? in
|
||||||
if part.filename == "cover.jpg" {
|
if part.filename == "cover.jpg" {
|
||||||
coverFile = part.destinationURL()
|
coverFile = part.destinationUri
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return LocalFile(libraryItem.id, part.filename!, part.mimeType()!, part.destinationURL()!)
|
return LocalFile(libraryItem.id, part.filename!, part.mimeType()!, part.destinationUri!, fileSize: Int(part.destinationURL!.fileSize))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files)
|
let localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files, coverPath: coverFile)
|
||||||
|
|
||||||
// Store the cover file
|
|
||||||
if let coverFile = coverFile {
|
|
||||||
localLibraryItem.coverContentUrl = coverFile.absoluteString
|
|
||||||
localLibraryItem.coverAbsolutePath = coverFile.path
|
|
||||||
}
|
|
||||||
|
|
||||||
Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem)
|
Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem)
|
||||||
statusNotification["localLibraryItem"] = try? localLibraryItem.asDictionary()
|
statusNotification["localLibraryItem"] = try? localLibraryItem.asDictionary()
|
||||||
|
@ -180,6 +175,8 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
|
|
||||||
self.notifyListeners("onItemDownloadComplete", data: statusNotification)
|
self.notifyListeners("onItemDownloadComplete", data: statusNotification)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self.notifyListeners("onItemDownloadComplete", data: statusNotification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,7 +267,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
|
|
||||||
let serverUrl = urlForTrack(item: item, track: track)
|
let serverUrl = urlForTrack(item: item, track: track)
|
||||||
let itemDirectory = try createLibraryItemFileDirectory(item: item)
|
let itemDirectory = try createLibraryItemFileDirectory(item: item)
|
||||||
let localUrl = itemDirectory.appendingPathComponent("\(filename)")
|
let localUrl = "\(itemDirectory)/\(filename)"
|
||||||
|
|
||||||
let task = session.downloadTask(with: serverUrl)
|
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)
|
var downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: nil)
|
||||||
|
@ -286,10 +283,10 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
let filename = "cover.jpg"
|
let filename = "cover.jpg"
|
||||||
let serverPath = "/api/items/\(item.id)/cover"
|
let serverPath = "/api/items/\(item.id)/cover"
|
||||||
let itemDirectory = try createLibraryItemFileDirectory(item: item)
|
let itemDirectory = try createLibraryItemFileDirectory(item: item)
|
||||||
let localUrl = itemDirectory.appendingPathComponent("\(filename)")
|
let localUrl = "\(itemDirectory)/\(filename)"
|
||||||
|
|
||||||
var downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil)
|
var downloadItemPart = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil)
|
||||||
let task = session.downloadTask(with: downloadItemPart.downloadURL()!)
|
let task = session.downloadTask(with: downloadItemPart.downloadURL!)
|
||||||
|
|
||||||
// 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,12 +302,12 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
return URL(string: urlstr)!
|
return URL(string: urlstr)!
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createLibraryItemFileDirectory(item: LibraryItem) throws -> URL {
|
private func createLibraryItemFileDirectory(item: LibraryItem) throws -> String {
|
||||||
let itemDirectory = documentsDirectory.appendingPathComponent("\(item.id)")
|
let itemDirectory = item.id
|
||||||
NSLog("ITEM DIR \(itemDirectory)")
|
NSLog("ITEM DIR \(itemDirectory)")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try FileManager.default.createDirectory(at: itemDirectory, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: AbsDownloader.downloadsDirectory.appendingPathComponent(itemDirectory), withIntermediateDirectories: true)
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("Failed to CREATE LI DIRECTORY \(error)")
|
NSLog("Failed to CREATE LI DIRECTORY \(error)")
|
||||||
throw LibraryItemDownloadError.failedDirectory
|
throw LibraryItemDownloadError.failedDirectory
|
||||||
|
|
|
@ -72,7 +72,21 @@ struct DownloadItemPart: Realmable, Codable {
|
||||||
var moved: Bool = false
|
var moved: Bool = false
|
||||||
var failed: Bool = false
|
var failed: Bool = false
|
||||||
var uri: String?
|
var uri: String?
|
||||||
|
var downloadURL: URL? {
|
||||||
|
if let uri = self.uri {
|
||||||
|
return URL(string: uri)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
var destinationUri: String?
|
var destinationUri: String?
|
||||||
|
var destinationURL: URL? {
|
||||||
|
if let destinationUri = self.destinationUri {
|
||||||
|
return AbsDownloader.downloadsDirectory.appendingPathComponent(destinationUri)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
var progress: Double = 0
|
var progress: Double = 0
|
||||||
var task: URLSessionDownloadTask!
|
var task: URLSessionDownloadTask!
|
||||||
|
|
||||||
|
@ -90,7 +104,7 @@ struct DownloadItemPart: Realmable, Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DownloadItemPart {
|
extension DownloadItemPart {
|
||||||
init(filename: String, destination: URL, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) {
|
init(filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) {
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.itemTitle = itemTitle
|
self.itemTitle = itemTitle
|
||||||
self.serverPath = serverPath
|
self.serverPath = serverPath
|
||||||
|
@ -103,26 +117,10 @@ extension DownloadItemPart {
|
||||||
downloadUrl += "&format=jpeg" // For cover images force to jpeg
|
downloadUrl += "&format=jpeg" // For cover images force to jpeg
|
||||||
}
|
}
|
||||||
self.uri = downloadUrl
|
self.uri = downloadUrl
|
||||||
self.destinationUri = destination.path
|
self.destinationUri = destination
|
||||||
}
|
}
|
||||||
|
|
||||||
func mimeType() -> String? {
|
func mimeType() -> String? {
|
||||||
audioTrack?.mimeType ?? episode?.audioTrack?.mimeType
|
audioTrack?.mimeType ?? episode?.audioTrack?.mimeType
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadURL() -> URL? {
|
|
||||||
if let uri = self.uri {
|
|
||||||
return URL(string: uri)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func destinationURL() -> URL? {
|
|
||||||
if let destinationUri = self.destinationUri {
|
|
||||||
return URL(fileURLWithPath: destinationUri)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,23 +11,87 @@ import Unrealm
|
||||||
struct LocalLibraryItem: Realmable, Codable {
|
struct LocalLibraryItem: Realmable, Codable {
|
||||||
var id: String = "local_\(UUID().uuidString)"
|
var id: String = "local_\(UUID().uuidString)"
|
||||||
var basePath: String = ""
|
var basePath: String = ""
|
||||||
var absolutePath: String = ""
|
dynamic var _contentUrl: String?
|
||||||
var contentUrl: String = ""
|
|
||||||
var isInvalid: Bool = false
|
var isInvalid: Bool = false
|
||||||
var mediaType: String = ""
|
var mediaType: String = ""
|
||||||
var media: MediaType?
|
var media: MediaType?
|
||||||
var localFiles: [LocalFile] = []
|
var localFiles: [LocalFile] = []
|
||||||
var coverContentUrl: String?
|
dynamic var _coverContentUrl: String?
|
||||||
var coverAbsolutePath: String?
|
|
||||||
var isLocal: Bool = true
|
var isLocal: Bool = true
|
||||||
var serverConnectionConfigId: String?
|
var serverConnectionConfigId: String?
|
||||||
var serverAddress: String?
|
var serverAddress: String?
|
||||||
var serverUserId: String?
|
var serverUserId: String?
|
||||||
var libraryItemId: String?
|
var libraryItemId: String?
|
||||||
|
|
||||||
|
var contentUrl: String? {
|
||||||
|
set(url) {
|
||||||
|
_contentUrl = url
|
||||||
|
}
|
||||||
|
get {
|
||||||
|
if let path = _contentUrl {
|
||||||
|
return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var coverContentUrl: String? {
|
||||||
|
set(url) {
|
||||||
|
_coverContentUrl = url
|
||||||
|
}
|
||||||
|
get {
|
||||||
|
if let path = self._coverContentUrl {
|
||||||
|
return AbsDownloader.downloadsDirectory.appendingPathComponent(path).absoluteString
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func primaryKey() -> String? {
|
static func primaryKey() -> String? {
|
||||||
return "id"
|
return "id"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys : String, CodingKey {
|
||||||
|
case id, basePath, contentUrl, isInvalid, mediaType, media, localFiles, coverContentUrl, isLocal, serverConnectionConfigId, serverAddress, serverUserId, libraryItemId
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try values.decode(String.self, forKey: .id)
|
||||||
|
basePath = try values.decode(String.self, forKey: .basePath)
|
||||||
|
contentUrl = try values.decode(String.self, forKey: .contentUrl)
|
||||||
|
isInvalid = try values.decode(Bool.self, forKey: .isInvalid)
|
||||||
|
mediaType = try values.decode(String.self, forKey: .mediaType)
|
||||||
|
media = try values.decode(MediaType.self, forKey: .media)
|
||||||
|
localFiles = try values.decode([LocalFile].self, forKey: .localFiles)
|
||||||
|
coverContentUrl = try values.decode(String.self, forKey: .coverContentUrl)
|
||||||
|
isLocal = try values.decode(Bool.self, forKey: .isLocal)
|
||||||
|
serverConnectionConfigId = try values.decode(String.self, forKey: .serverConnectionConfigId)
|
||||||
|
serverAddress = try values.decode(String.self, forKey: .serverAddress)
|
||||||
|
serverUserId = try values.decode(String.self, forKey: .serverUserId)
|
||||||
|
libraryItemId = try values.decode(String.self, forKey: .libraryItemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(id, forKey: .id)
|
||||||
|
try container.encode(basePath, forKey: .basePath)
|
||||||
|
try container.encode(contentUrl, forKey: .contentUrl)
|
||||||
|
try container.encode(isInvalid, forKey: .isInvalid)
|
||||||
|
try container.encode(mediaType, forKey: .mediaType)
|
||||||
|
try container.encode(media, forKey: .media)
|
||||||
|
try container.encode(localFiles, forKey: .localFiles)
|
||||||
|
try container.encode(coverContentUrl, forKey: .coverContentUrl)
|
||||||
|
try container.encode(isLocal, forKey: .isLocal)
|
||||||
|
try container.encode(serverConnectionConfigId, forKey: .serverConnectionConfigId)
|
||||||
|
try container.encode(serverAddress, forKey: .serverAddress)
|
||||||
|
try container.encode(serverUserId, forKey: .serverUserId)
|
||||||
|
try container.encode(libraryItemId, forKey: .libraryItemId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LocalPodcastEpisode: Realmable, Codable {
|
struct LocalPodcastEpisode: Realmable, Codable {
|
||||||
|
@ -53,13 +117,40 @@ struct LocalFile: Realmable, Codable {
|
||||||
var id: String = UUID().uuidString
|
var id: String = UUID().uuidString
|
||||||
var filename: String?
|
var filename: String?
|
||||||
var contentUrl: String = ""
|
var contentUrl: String = ""
|
||||||
var absolutePath: String = ""
|
var absolutePath: String {
|
||||||
|
return AbsDownloader.downloadsDirectory.appendingPathComponent(self.contentUrl).absoluteString
|
||||||
|
}
|
||||||
var mimeType: String?
|
var mimeType: String?
|
||||||
var size: Int = 0
|
var size: Int = 0
|
||||||
|
|
||||||
static func primaryKey() -> String? {
|
static func primaryKey() -> String? {
|
||||||
return "id"
|
return "id"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys : String, CodingKey {
|
||||||
|
case id, filename, contentUrl, absolutePath, mimeType, size
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try values.decode(String.self, forKey: .id)
|
||||||
|
filename = try values.decode(String.self, forKey: .filename)
|
||||||
|
contentUrl = try values.decode(String.self, forKey: .contentUrl)
|
||||||
|
mimeType = try values.decode(String.self, forKey: .mimeType)
|
||||||
|
size = try values.decode(Int.self, forKey: .size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(id, forKey: .id)
|
||||||
|
try container.encode(filename, forKey: .filename)
|
||||||
|
try container.encode(contentUrl, forKey: .contentUrl)
|
||||||
|
try container.encode(absolutePath, forKey: .absolutePath)
|
||||||
|
try container.encode(mimeType, forKey: .mimeType)
|
||||||
|
try container.encode(size, forKey: .size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LocalMediaProgress: Realmable, Codable {
|
struct LocalMediaProgress: Realmable, Codable {
|
||||||
|
|
|
@ -8,14 +8,13 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension LocalLibraryItem {
|
extension LocalLibraryItem {
|
||||||
init(_ item: LibraryItem, localUrl: URL, server: ServerConnectionConfig, files: [LocalFile]) {
|
init(_ item: LibraryItem, localUrl: String, server: ServerConnectionConfig, files: [LocalFile], coverPath: String?) {
|
||||||
self.init()
|
self.init()
|
||||||
self.contentUrl = localUrl.absoluteString
|
self.contentUrl = localUrl
|
||||||
self.mediaType = item.mediaType
|
self.mediaType = item.mediaType
|
||||||
self.media = item.media
|
self.media = item.media
|
||||||
self.localFiles = files
|
self.localFiles = files
|
||||||
// TODO: self.coverContentURL
|
self.coverContentUrl = coverPath
|
||||||
// TODO: self.converAbsolutePath
|
|
||||||
self.libraryItemId = item.id
|
self.libraryItemId = item.id
|
||||||
self.serverConnectionConfigId = server.id
|
self.serverConnectionConfigId = server.id
|
||||||
self.serverAddress = server.address
|
self.serverAddress = server.address
|
||||||
|
@ -71,14 +70,13 @@ extension LocalLibraryItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalFile {
|
extension LocalFile {
|
||||||
init(_ libraryItemId: String, _ filename: String, _ mimeType: String, _ localUrl: URL) {
|
init(_ libraryItemId: String, _ filename: String, _ mimeType: String, _ localUrl: String, fileSize: Int) {
|
||||||
self.init()
|
self.init()
|
||||||
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.absoluteString
|
self.contentUrl = localUrl
|
||||||
self.absolutePath = localUrl.path
|
self.size = fileSize
|
||||||
self.size = Int(localUrl.fileSize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAudioFile() -> Bool {
|
func isAudioFile() -> Bool {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue