From 33041608f800bf25e1fafafa3b006e2b7c392f23 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 4 Aug 2022 18:25:27 -0400 Subject: [PATCH] Switch to async logic --- ios/App/App.xcodeproj/project.pbxproj | 4 + ios/App/App/plugins/AbsDownloader.swift | 93 ++-------------- .../util/LibraryItemDownloadSession.swift | 102 ++++++++++++++++++ 3 files changed, 116 insertions(+), 83 deletions(-) create mode 100644 ios/App/Shared/util/LibraryItemDownloadSession.swift diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index fa1fb216..65920f9d 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ C4B265F5285A5A6600E1B5C3 /* LocalLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */; }; C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D0677428106D0C00B8F875 /* DataClasses.swift */; }; E99C8C932883A00F00E3279A /* LocalLibraryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */; }; + E9D38158289A0A6F0019EEED /* LibraryItemDownloadSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D38157289A0A6F0019EEED /* LibraryItemDownloadSession.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -76,6 +77,7 @@ C4B265F4285A5A6600E1B5C3 /* LocalLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalLibrary.swift; sourceTree = ""; }; C4D0677428106D0C00B8F875 /* DataClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataClasses.swift; sourceTree = ""; }; E99C8C922883A00F00E3279A /* LocalLibraryExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalLibraryExtensions.swift; sourceTree = ""; }; + E9D38157289A0A6F0019EEED /* LibraryItemDownloadSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryItemDownloadSession.swift; sourceTree = ""; }; 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 = ""; }; /* End PBXFileReference section */ @@ -156,6 +158,7 @@ 3AF1970B2806E2590096F747 /* ApiClient.swift */, 3AB34052280829BF0039308B /* Extensions.swift */, 3AB34054280832720039308B /* PlayerEvents.swift */, + E9D38157289A0A6F0019EEED /* LibraryItemDownloadSession.swift */, ); path = util; sourceTree = ""; @@ -341,6 +344,7 @@ C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */, 4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */, 3AB34055280832720039308B /* PlayerEvents.swift in Sources */, + E9D38158289A0A6F0019EEED /* LibraryItemDownloadSession.swift in Sources */, E99C8C932883A00F00E3279A /* LocalLibraryExtensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index b7f4f3a3..46c475b7 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -10,8 +10,6 @@ import Capacitor @objc(AbsDownloader) public class AbsDownloader: CAPPlugin { - let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - @objc func downloadLibraryItem(_ call: CAPPluginCall) { let libraryItemId = call.getString("libraryItemId") let episodeId = call.getString("episodeId") @@ -24,90 +22,19 @@ public class AbsDownloader: CAPPlugin { call.resolve() } else { NSLog("Got library item from server \(libraryItem!.id)") - self.startLibraryItemDownload(item: libraryItem!) - call.resolve() + Task { + do { + let downloadSession = LibraryItemDownloadSession(libraryItem!) + let localLibraryItem = try await downloadSession.startDownload() + Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem) + } catch { + NSLog("Failed to download \(error)") + } + call.resolve() + } } } } - - private func startLibraryItemDownload(item: LibraryItem) { - let length = item.media.tracks?.count ?? 0 - if length > 0 { - let downloadDispatch = DispatchGroup() - let files = item.media.tracks!.enumerated().map { - position, track -> LocalFile in startLibraryItemTrackDownload(item: item, position: position, track: track, dispatch: downloadDispatch) - } - downloadDispatch.notify(queue: .main) { - let localLibraryItem = LocalLibraryItem(item, localUrl: self.documentsDirectory, server: Store.serverConfig!, files: files) - Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem) - } - } else { - NSLog("No audio tracks for the supplied library item") - } - } - - private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack, dispatch: DispatchGroup) -> LocalFile { - NSLog("TRACK \(track.contentUrl!)") - - // If we don't name metadata, then we can't proceed - guard let filename = track.metadata?.filename else { - NSLog("No metadata for track, unable to download") - return LocalFile() - } - - let serverUrl = urlForTrack(item: item, track: track) - let itemDirectory = createLibraryItemFileDirectory(item: item) - let localUrl = itemDirectory.appendingPathComponent("\(filename)") - - downloadTrack(serverUrl: serverUrl, localUrl: localUrl, dispatch: dispatch) - return LocalFile(item.id, filename, track.mimeType, localUrl) - } - - 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) -> 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 downloadTrack(serverUrl: URL, localUrl: URL, dispatch: DispatchGroup) { - dispatch.enter() - - let downloadTask = URLSession.shared.downloadTask(with: serverUrl) { urlOrNil, responseOrNil, errorOrNil in - defer { dispatch.leave() } - - guard let fileURL = urlOrNil else { - return - } - - do { - NSLog("Download TMP file URL \(fileURL)") - let audioData = try Data(contentsOf:fileURL) - try audioData.write(to: localUrl) - NSLog("Download written to \(localUrl)") - } catch { - NSLog("FILE ERROR: \(error)") - } - } - - // Start the download - downloadTask.resume() - } } struct DownloadItem: Codable { diff --git a/ios/App/Shared/util/LibraryItemDownloadSession.swift b/ios/App/Shared/util/LibraryItemDownloadSession.swift new file mode 100644 index 00000000..7a571117 --- /dev/null +++ b/ios/App/Shared/util/LibraryItemDownloadSession.swift @@ -0,0 +1,102 @@ +// +// 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() + } + } + +}