Improve the file downloader

This commit is contained in:
Jackson Harper 2022-08-17 19:48:52 +08:00
parent 0b6fde935d
commit 23ba93aa4c
3 changed files with 85 additions and 62 deletions

View file

@ -18,7 +18,7 @@ public final class Services {
let networker = Networker(appEnvironment: appEnvironment)
self.authenticator = Authenticator(networker: networker)
self.dataService = DataService(appEnvironment: appEnvironment, networker: networker)
self.audioSession = AudioSession()
self.audioSession = AudioSession(appEnvironment: appEnvironment, networker: networker)
}
}

View file

@ -6,9 +6,9 @@
//
import AVFoundation
import CryptoKit
import Foundation
import MediaPlayer
import Models
import Utils
@ -24,12 +24,16 @@ public class AudioSession: ObservableObject {
@Published public var state: AudioSessionState = .stopped
@Published public var item: LinkedItem?
@Published public var audioUrl: URL?
let appEnvironment: AppEnvironment
let networker: Networker
var timer: Timer?
var player: AVAudioPlayer?
public init() {}
public init(appEnvironment: AppEnvironment, networker: Networker) {
self.appEnvironment = appEnvironment
self.networker = networker
}
public func play(item: LinkedItem) {
// Stop any existing session
@ -58,21 +62,37 @@ public class AudioSession: ObservableObject {
public func startAudio() {
state = .loading
// Just simulating some loading delay here
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) {
self.audioUrl = Bundle.main.url(forResource: "speech-sample", withExtension: "mp3")!
self.audioUrl = URL(string: "https://storage.googleapis.com/omnivore-demo-files/speech/df31ee24-cd3d-412f-9a7c-e8a0afe2e4cd.mp3")!
let pageId = item!.unwrappedID
Task {
do {
try await downloadAudioFile(pageId: pageId)
} catch {
print("FAILED TO DOWNLOAD AUDIO URL")
print(error.localizedDescription)
}
DispatchQueue.main.async {
let audioUrl = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent(pageId + ".mp3")
if !FileManager.default.fileExists(atPath: audioUrl.path) {
self.stop()
return
}
if let url = self.audioUrl {
do {
try? AVAudioSession.sharedInstance().setCategory(.playback)
self.player = try AVAudioPlayer(contentsOf: url)
try AVAudioSession.sharedInstance().setCategory(.playback)
self.player = try AVAudioPlayer(contentsOf: audioUrl)
if self.player?.play() ?? false {
self.state = .playing
self.startTimer()
self.setupRemoteControl()
}
} catch {
print("error playing MP3 file", error)
print(error.localizedDescription)
self.state = .stopped
}
@ -161,4 +181,56 @@ public class AudioSession: ObservableObject {
return .success
}
}
func downloadAudioFile(pageId: String) async throws -> URL? {
let audioUrl = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent(pageId + ".mp3")
// if FileManager.default.fileExists(atPath: audioUrl.path) {
// // Prevent re-download
// // TODO: We aren't doing this very safely, we should be verifying a checksum
// return audioUrl
// }
guard let url = URL(string: "/api/article/\(pageId)/mp3", relativeTo: appEnvironment.serverBaseURL) else {
throw BasicError.message(messageText: "Invalid audio URL")
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.timeoutInterval = 600
for (header, value) in networker.defaultHeaders {
request.setValue(value, forHTTPHeaderField: header)
}
let result: (Data, URLResponse)? = try? await URLSession.shared.data(for: request)
guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
throw BasicError.message(messageText: "audioFetch failed. no response or bad status code.")
}
print("httpResponse: ", httpResponse)
guard let data = result?.0 else {
throw BasicError.message(messageText: "audioFetch failed. no data received.")
}
let tempPath = FileManager.default
.urls(for: .cachesDirectory, in: .userDomainMask)[0]
.appendingPathComponent(UUID().uuidString + ".mp3")
do {
let hash = Data(Insecure.MD5.hash(data: data)).base64EncodedString()
// if let googleHash = httpResponse.value(forHTTPHeaderField: "x-goog-hash"), googleHash.contains("md5=\(hash)") {
// throw BasicError.message(messageText: "Downloaded mp3 file hashes do not match: returned: \(googleHash) v computed: \(hash)")
// }
try data.write(to: tempPath)
try FileManager.default.moveItem(at: tempPath, to: audioUrl)
} catch {
let errorMessage = "audioFetch failed. could not write MP3 data to disk"
throw BasicError.message(messageText: errorMessage)
}
return audioUrl
}
}

View file

@ -1,6 +1,6 @@
//
// File.swift
//
//
//
// Created by Jackson Harper on 8/17/22.
//
@ -12,53 +12,4 @@ import Foundation
import Models
import Utils
public extension DataService {
func loadAudioData(pageId: String) async throws -> URL? {
// pageId
guard let url = URL(string: pageURLString) else {
throw BasicError.message(messageText: "No PDF URL found")
}
let result: (Data, URLResponse)? = try? await URLSession.shared.data(from: url)
guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
throw BasicError.message(messageText: "pdfFetch failed. no response or bad status code.")
}
guard let data = result?.0 else {
throw BasicError.message(messageText: "pdfFetch failed. no data received.")
}
var localPdfURL: URL?
let tempPath = FileManager.default
.urls(for: .cachesDirectory, in: .userDomainMask)[0]
.appendingPathComponent(UUID().uuidString + ".pdf")
try await backgroundContext.perform { [weak self] in
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LinkedItem.slug), slug)
let linkedItem = try? self?.backgroundContext.fetch(fetchRequest).first
guard let linkedItem = linkedItem else {
let errorMessage = "pdfFetch failed. could not find LinkedItem from fetch request"
throw BasicError.message(messageText: errorMessage)
}
do {
try data.write(to: tempPath)
let localPDF = try PDFUtils.moveToLocal(url: tempPath)
localPdfURL = PDFUtils.localPdfURL(filename: localPDF)
linkedItem.tempPDFURL = nil
linkedItem.localPDF = localPDF
try self?.backgroundContext.save()
} catch {
self?.backgroundContext.rollback()
let errorMessage = "pdfFetch failed. core data save failed."
throw BasicError.message(messageText: errorMessage)
}
}
return localPdfURL
}
}
public extension AudioSession {}