388 lines
19 KiB
Swift
388 lines
19 KiB
Swift
//
|
||
// MP_DownloadManager.swift
|
||
// MusicPlayer
|
||
//
|
||
// Created by 忆海16 on 2024/5/14.
|
||
//
|
||
|
||
import Foundation
|
||
import Tiercel
|
||
import DownloadButton
|
||
|
||
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)
|
||
//下载任务会话
|
||
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
|
||
}
|
||
if url.scheme == "file" {
|
||
print("用户对同一首歌删除又下载")
|
||
//出现这种情况是用户在播放器界面移除了下载歌曲,又继续点了下载同一首歌,当前歌曲资源的并未刷新,提供的路径资源仍旧是下载后的本地资源,需要重新调用网络请求获取
|
||
MP_NetWorkManager.shared.requestNextList("", videoId: videoId){
|
||
[weak self] listSongs in
|
||
guard let self = self, let first = listSongs.first else {
|
||
return
|
||
}
|
||
let group = DispatchGroup()
|
||
group.enter()
|
||
improveDataforLycirsAndRelated(first) {(result) in
|
||
first.lyricsID = result.0
|
||
first.relatedID = result.1
|
||
group.leave()
|
||
}
|
||
group.enter()
|
||
//补全资源路径组和封面路径组
|
||
improveDataforResouceAndCover(first) {(resourceUrls, coverUrls) in
|
||
if let resourceUrls = resourceUrls {
|
||
first.resourceUrls = resourceUrls.0
|
||
first.itags = resourceUrls.1
|
||
first.mimeTypes = resourceUrls.2
|
||
}
|
||
first.coverUrls = coverUrls
|
||
group.leave()
|
||
}
|
||
group.notify(queue: .main, execute: {
|
||
[weak self] in
|
||
guard let self = self else {
|
||
return
|
||
}
|
||
//补全了数据,执行下载
|
||
prepareVideoDownloadTask(from: first)
|
||
})
|
||
}
|
||
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
|
||
//执行下载进度更新代理
|
||
NotificationCenter.notificationKey.post(notificationName: .download_progress_source, object: ["videoId":nextVideoId])
|
||
}
|
||
//下载任务完成状态回调
|
||
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")
|
||
//判断真实下载地址是否存在对应文件
|
||
if fileManager.fileExists(atPath: fileURL.path) {
|
||
//确实存在一个文件,将它删除
|
||
try? fileManager.removeItem(at: fileURL)
|
||
}
|
||
//将任务缓存文件移动到真实下载地址
|
||
do{
|
||
try fileManager.moveItem(at: filePathUrl, to: fileURL)
|
||
}catch{
|
||
//移动失败,终止任务
|
||
NotificationCenter.notificationKey.post(notificationName: .dowload_end_source, object: ["videoId":nextVideoId])
|
||
//发布报错埋点
|
||
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
|
||
MP_HUD.text("An error occurred while downloading. Please download again.", delay: 1.5, completion: nil)
|
||
session.cancel(task) { _ in
|
||
self.downloadTasks[nextVideoId] = nil
|
||
print("移动\(self.songHandlers[nextVideoId]?.title ?? "")到真实下载地址失败,失败原因:\(error)")
|
||
}
|
||
//启动新的任务
|
||
self.executeVideoDownloadTask()
|
||
return
|
||
}
|
||
//文件移动完成后,执行任务队列清理
|
||
saveLoadVideoItem(self.songHandlers[nextVideoId]!){
|
||
[weak self] in
|
||
NotificationCenter.notificationKey.post(notificationName: .dowload_end_source, object: ["videoId":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
|
||
}
|
||
session.remove(task)
|
||
//启动新的任务
|
||
self.executeVideoDownloadTask()
|
||
}.failure { [weak self] task in
|
||
//下载失败
|
||
guard let self = self else {return}
|
||
//确定任务报错是否存在
|
||
if let error = task.error {
|
||
//任务报错存在,处理完成状态回调
|
||
NotificationCenter.notificationKey.post(notificationName: .dowload_end_source, object: ["videoId":nextVideoId])
|
||
//发布报错埋点
|
||
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
|
||
MP_HUD.text("An error occurred while downloading. Please download again.", delay: 1.5, completion: 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 _ = 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, completion: @escaping (Bool) -> Void){
|
||
//优先检索数据库模型是否存在
|
||
MPPositive_DownloadItemModel.fetch(predicate: .init(format: "videoId == %@", videoId)) { [weak self] results in
|
||
if results.isEmpty {
|
||
// 数据库中没有记录,直接返回 false
|
||
DispatchQueue.main.async {
|
||
completion(false)
|
||
}
|
||
return
|
||
}
|
||
//数据库中存在数据
|
||
DispatchQueue.global(qos: .userInitiated).async {
|
||
let fileURL = self?.getDocumentsFileURL(videoId)
|
||
// 回到主线程调用回调闭包
|
||
DispatchQueue.main.async {
|
||
completion(fileURL != nil)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
///获取沙盒下载路径
|
||
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(predicate: .init(format: "videoId == %@", videoId)) { results in
|
||
results.forEach { item in
|
||
if item.videoId == videoId {
|
||
MPPositive_DownloadItemModel.delete(item)
|
||
}
|
||
}
|
||
MPPositive_LoadCoreModel.shared.reloadLoadSongViewModel(nil)
|
||
let downloadsURL = self.DocumentsURL.appendingPathComponent("Downloads")
|
||
let fileURL = downloadsURL.appendingPathComponent("\(videoId).mp4")
|
||
if self.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("文件不存在")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|