Handle a documents directory that can change

Thanks iOS
This commit is contained in:
ronaldheft 2022-08-08 19:25:59 -04:00
parent 8e2be4704e
commit e9961f64a9
4 changed files with 137 additions and 53 deletions

View file

@ -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

View file

@ -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
}
}
} }

View file

@ -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 {

View file

@ -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 {