Music_Player3/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_DownloadManager.swift

344 lines
16 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// MP_DownloadManager.swift
// MusicPlayer
//
// Created by 16 on 2024/5/14.
//
import Foundation
import Tiercel
import DownloadButton
///
protocol MP_DownloadManagerDelegate:AnyObject {
///VideoId
func downloadProgressDidUpdate(for videoId: String, progress: CGFloat)
///
func downloadResult(for videoId:String, result:Result<MPPositive_SongItemModel, Error>)
}
enum MP_DownloadTaskStatus {
///
case queued
///
case downloading
///
case finished
///
case failed
}
///
class MP_DownloadManager: NSObject {
static let shared = MP_DownloadManager()
//
private let fileManager = FileManager.default
//
private let DocumentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
///
var loadQueue:DispatchQueue = .init(label: "com.relax.offline.mp3.loadQueue", qos: .background)
///
weak var delegate:MP_DownloadManagerDelegate?
//
var session: SessionManager!
//
private var downloadURLs:[String: URL] = [:]
//
private var downloadTasks: [String: (task: DownloadTask, status: MP_DownloadTaskStatus)] = [:]
//
private var queuedTaskVideoIds: [String] = []
// backgroundTaskID
private var backgroundTaskIDs: [String: UIBackgroundTaskIdentifier] = [:]
//
private var progressStorage: [String: CGFloat] = [:] //
//
private var songHandlers:[String: MPPositive_SongItemModel] = [:]
//
private var downloadDocumentName:String = "Downloads"
//
private var isDownloading: Bool = false
//
private var maxTasksCount:Int = 6
private override init() {
super.init()
var configuration = SessionConfiguration()
configuration.timeoutIntervalForRequest = 60
configuration.maxConcurrentTasksLimit = maxTasksCount
configuration.allowsCellularAccess = true
//
session = SessionManager("com.relax.offline.mp3.music.backgroundDownload", configuration: configuration, operationQueue: loadQueue)
//
let downloadsURL = DocumentsURL.appendingPathComponent(downloadDocumentName)
//
if !fileManager.fileExists(atPath: downloadsURL.path) {
//
do{
//
try fileManager.createDirectory(at: downloadsURL, withIntermediateDirectories: true, attributes: nil)
}catch {
print("创建下载文件夹失败, 失败原因:\(error.localizedDescription)")
}
}
}
///
func prepareVideoDownloadTask(from song:MPPositive_SongItemModel){
//()
guard let url = URL(string: song.resourceUrls?.first ?? ""), let videoId = song.videoId else {
//
MP_HUD.error("Download failed, please try again later!", delay: 2.0, completion: nil)
//
MP_AnalyticsManager.shared.player_b_downloadfailure_errorAction(song.videoId ?? "", videoname: song.title ?? "", artistname: song.shortBylineText ?? "", error: "Failed to create download link")
return
}
//
songHandlers[videoId] = song
//
downloadURLs[videoId] = url
queuedTaskVideoIds.append(videoId)
//
executeVideoDownloadTask()
}
//
private func executeVideoDownloadTask() {
print("当前运行的任务数量: \(session.tasks.filter({ $0.status == .running }).count)")
print("待下载队列: \(queuedTaskVideoIds)")
while session.tasks.filter({ $0.status == .running }).count < maxTasksCount, let nextVideoId = queuedTaskVideoIds.first, let url = downloadURLs[nextVideoId] {
queuedTaskVideoIds.removeFirst()
//
let downloadTask = session.download(url, headers: ["Accept-Encoding": "gzip, deflate"])!
//
downloadTasks[nextVideoId] = (downloadTask, .queued)
//
if let taskData = downloadTasks[nextVideoId], taskData.status == .queued {
let task = taskData.task
var bgTaskID: UIBackgroundTaskIdentifier = .invalid
bgTaskID = UIApplication.shared.beginBackgroundTask(withName: "DownloadTask-\(nextVideoId)") {
UIApplication.shared.endBackgroundTask(bgTaskID)
self.backgroundTaskIDs[nextVideoId] = .invalid
}
self.backgroundTaskIDs[nextVideoId] = bgTaskID
//
task.progress { [weak self] (task) in
guard let self = self else {return}
if self.downloadTasks[nextVideoId]?.status == .queued {
self.downloadTasks[nextVideoId]?.status = .downloading
}
//videoId
progressStorage[nextVideoId] = task.progress.fractionCompleted
DispatchQueue.main.async {
[weak self] in
guard let self = self else {return}
//
delegate?.downloadProgressDidUpdate(for: nextVideoId, progress: task.progress.fractionCompleted)
}
}
//
task.success {[weak self] (task) in
//
guard let self = self else {return}
//
let filePathUrl:URL = URL(fileURLWithPath: task.filePath)
print("任务下载地址:\(filePathUrl.path)")
//
let downloadsURL = DocumentsURL.appendingPathComponent(downloadDocumentName)
//
let fileURL = downloadsURL.appendingPathComponent("\(nextVideoId).mp4")
//
do{
try fileManager.moveItem(at: filePathUrl, to: fileURL)
}catch{
//
delegate?.downloadResult(for: nextVideoId, result: .failure(error))
//
MP_AnalyticsManager.shared.player_b_downloadfailure_errorAction(nextVideoId, videoname: songHandlers[nextVideoId]?.title ?? "", artistname: songHandlers[nextVideoId]?.shortBylineText ?? "", error: error.localizedDescription)
//
if self.downloadTasks[nextVideoId]?.status == .downloading {
self.downloadTasks[nextVideoId]?.status = .failed
}
downloadURLs[nextVideoId] = nil
progressStorage[nextVideoId] = nil
songHandlers[nextVideoId] = nil
session.cancel(task) { _ in
self.downloadTasks[nextVideoId] = nil
print("移动\(self.songHandlers[nextVideoId]?.title ?? "")到真实下载地址失败,失败原因:\(error)")
}
//
self.executeVideoDownloadTask()
return
}
//
self.delegate?.downloadResult(for: nextVideoId, result: .success(self.songHandlers[nextVideoId]!))
saveLoadVideoItem(self.songHandlers[nextVideoId]!)
//
if self.downloadTasks[nextVideoId]?.status == .downloading {
self.downloadTasks[nextVideoId]?.status = .finished
}
self.downloadURLs[nextVideoId] = nil
self.downloadTasks[nextVideoId] = nil
self.progressStorage[nextVideoId] = nil
self.songHandlers[nextVideoId] = nil
//
MP_HUD.downloadText("Download Successfull", delay: 1.0, completion: nil)
//
if let bgTaskID = self.backgroundTaskIDs[nextVideoId] {
UIApplication.shared.endBackgroundTask(bgTaskID)
self.backgroundTaskIDs[nextVideoId] = .invalid
}
//
self.executeVideoDownloadTask()
}.failure { [weak self] task in
//
guard let self = self else {return}
//
if let error = task.error {
//,
delegate?.downloadResult(for: nextVideoId, result: .failure(error))
//
MP_AnalyticsManager.shared.player_b_downloadfailure_errorAction(nextVideoId, videoname: songHandlers[nextVideoId]?.title ?? "", artistname: songHandlers[nextVideoId]?.shortBylineText ?? "", error: error.localizedDescription)
//
if self.downloadTasks[nextVideoId]?.status == .downloading {
self.downloadTasks[nextVideoId]?.status = .failed
}
downloadURLs[nextVideoId] = nil
progressStorage[nextVideoId] = nil
songHandlers[nextVideoId] = nil
session.cancel(task) { _ in
self.downloadTasks[nextVideoId] = nil
print("\(self.songHandlers[nextVideoId]?.title ?? "")下载任务失败,失败原因:\(error)")
//
if let bgTaskID = self.backgroundTaskIDs[nextVideoId] {
UIApplication.shared.endBackgroundTask(bgTaskID)
self.backgroundTaskIDs[nextVideoId] = .invalid
}
}
//
self.executeVideoDownloadTask()
}
}
}
}
}
///
func isTasksQueue(for videoId:String) -> Bool {
if let song = songHandlers[videoId] {
return true
}
return false
}
///
func isActiveTask(for videoId:String) -> Bool {
if let taskData = downloadTasks[videoId] {
return taskData.status == .downloading
}
return false
}
///
func getProgress(for videoId: String) -> CGFloat?{
return progressStorage[videoId]
}
func cancelAllTasksIfNeeded() {
//
for key in progressStorage.keys {
session.cancel(key)
}
}
///
func isDownloadedFileDocuments(_ videoId:String) -> Bool {
//
guard MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", videoId)).count != 0 else {
//0
return false
}
//
guard let _ = getDocumentsFileURL(videoId) else {
return false
}
//
return true
}
///
func getDocumentsFileURL(_ videoID: String) -> String? {
// DocumentsURL
let documentsDirectoryURL = DocumentsURL.appendingPathComponent("Downloads")
// videoIdURL
let fileURL = documentsDirectoryURL.appendingPathComponent("\(videoID).mp4")
//
if FileManager.default.fileExists(atPath: fileURL.path) == true {
//
return fileURL.absoluteString
}else {
return nil
}
}
///
func cancelDownloadTask(_ videoId:String, completion:((String) -> Void)?) {
DispatchQueue.main.async {
[weak self] in
guard let self = self else {return}
//
MP_AnalyticsManager.shared.player_b_downloadfailure_errorAction(videoId, videoname: songHandlers[videoId]?.title ?? "", artistname: songHandlers[videoId]?.shortBylineText ?? "", error: "User Cancel Task")
queuedTaskVideoIds.removeAll(where: {$0 == videoId})
//
if self.downloadTasks[videoId]?.status == .downloading {
self.downloadTasks[videoId]?.status = .failed
}
downloadURLs[videoId] = nil
progressStorage[videoId] = nil
songHandlers[videoId] = nil
if let task = self.downloadTasks[videoId]?.task {
session.cancel(task) { _ in
self.downloadTasks[videoId] = nil
print("\(self.songHandlers[videoId]?.title ?? "")下载任务失败,失败原因:用户取消了")
}
}
//
if let bgTaskID = self.backgroundTaskIDs[videoId] {
UIApplication.shared.endBackgroundTask(bgTaskID)
self.backgroundTaskIDs[videoId] = .invalid
}
deleteFileDocuments(videoId, completion: nil)
if completion != nil {
completion!(videoId)
}
}
}
///
func deleteFileDocuments(_ videoId:String, completion:((String) -> Void)?) {
DispatchQueue.main.async {
[weak self] in
guard let self = self else {return}
//
MPPositive_DownloadItemModel.fetch(.init(format: "videoId == %@", videoId)).forEach { item in
if item.videoId == videoId {
MPPositive_DownloadItemModel.delete(item)
}
}
MPPositive_LoadCoreModel.shared.reloadLoadSongViewModel(nil)
let downloadsURL = DocumentsURL.appendingPathComponent("Downloads")
let fileURL = downloadsURL.appendingPathComponent("\(videoId).mp4")
if fileManager.fileExists(atPath: fileURL.path) {
do{
try FileManager.default.removeItem(at: fileURL)
//
if completion != nil {
completion!(videoId)
}
print("成功删除了\(videoId)文件")
}catch{
//
if completion != nil {
completion!(videoId)
}
print("删除文件时发生错误:\(error)")
}
}else {
//
if completion != nil {
completion!(videoId)
}
print("文件不存在")
}
}
}
}