344 lines
16 KiB
Swift
344 lines
16 KiB
Swift
//
|
||
// 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? {
|
||
// 获取Documents目录的URL
|
||
let documentsDirectoryURL = DocumentsURL.appendingPathComponent("Downloads")
|
||
// 根据videoId构建文件完整URL
|
||
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("文件不存在")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|