445 lines
21 KiB
Swift
445 lines
21 KiB
Swift
//
|
||
// MP_DownloadManager.swift
|
||
// MusicPlayer
|
||
//
|
||
// Created by 忆海16 on 2024/5/14.
|
||
//
|
||
|
||
import Foundation
|
||
import Tiercel
|
||
import DownloadButton
|
||
//下载构造体
|
||
struct TaskStatu{
|
||
var task:DownloadTask
|
||
var status:MP_DownloadTaskStatus
|
||
}
|
||
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)
|
||
///数据库保存线程
|
||
lazy var sharedBackgroundQueue: DispatchQueue = {
|
||
return DispatchQueue(label: "com.example.saveLoadVideoItemQueue", qos: .background, attributes: .concurrent)
|
||
}()
|
||
//下载任务会话
|
||
var session: SessionManager!
|
||
//下载链接组
|
||
private var downloadURLs:[String: URL]? = [:]
|
||
//待下载任务队列组
|
||
private var downloadTasks: [String: TaskStatu]? = [:]
|
||
//活跃的下载任务队列组
|
||
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("Failed to obtain resource, please try again later".localizableString(), 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, clickTrackingParams: nil){
|
||
[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()
|
||
}failure: { statu in
|
||
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], let downloadTask = session.download(url, headers: ["Accept-Encoding": "gzip, deflate"], onMainQueue: false) {
|
||
queuedTaskVideoIds?.removeFirst()
|
||
//将下载任务加入任务队列
|
||
downloadTasks?[nextVideoId] = .init(task: downloadTask, status: .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)的进度值
|
||
loadQueue.async {
|
||
self.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
|
||
loadQueue.async {
|
||
self.progressStorage?[nextVideoId] = nil
|
||
}
|
||
songHandlers?[nextVideoId] = nil
|
||
MP_HUD.text("An error occurred while downloading. Please download again.".localizableString(), 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
|
||
guard let self = self else {return}
|
||
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
|
||
loadQueue.async {
|
||
self.progressStorage?[nextVideoId] = nil
|
||
}
|
||
self.songHandlers?[nextVideoId] = nil
|
||
//告知用户下载成功
|
||
MP_HUD.downloadText("Download Successfull".localizableString(), 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
|
||
loadQueue.async {
|
||
self.progressStorage?[nextVideoId] = nil
|
||
}
|
||
songHandlers?[nextVideoId] = nil
|
||
switch error.localizedDescription {
|
||
case "The operation couldn’t be completed. No space left on device":
|
||
MP_HUD.text("Insufficient storage space, download failed".localizableString(), delay: 1.5, completion: nil)
|
||
default:
|
||
MP_HUD.text("An error occurred while downloading. Please download again.".localizableString(), delay: 1.5, completion: nil)
|
||
}
|
||
session.cancel(task) { [weak self] _ in
|
||
guard let self = self else {return}
|
||
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:Any, completion: ((Bool) -> Void)?) {
|
||
guard let videoIdString = videoId as? String else {
|
||
print("Error: Expected a String for videoId but got--\(videoId)")
|
||
completion?(false)
|
||
return
|
||
}
|
||
loadQueue.async {
|
||
[weak self] in
|
||
guard let self = self else {return}
|
||
if let tasks = downloadTasks, let taskData = tasks[videoIdString] {
|
||
completion?(taskData.status == .downloading)
|
||
}else {
|
||
completion?(false)
|
||
}
|
||
}
|
||
// if let task = downloadTasks as? [String: TaskStatu], let taskData = task[videoIdString] {
|
||
// return taskData.status == .downloading
|
||
// }
|
||
// return false
|
||
}
|
||
///获取当前音乐任务下载进度
|
||
func getProgress(for videoId: String?, completion:((CGFloat?) -> Void)?){
|
||
guard let videoIdString = videoId else {
|
||
print("Error: Expected a String for videoId but got--\(videoId ?? "")")
|
||
completion?(nil)
|
||
return
|
||
}
|
||
loadQueue.async {
|
||
[weak self] in
|
||
guard let self = self else {return}
|
||
if let progress = progressStorage?[videoIdString] {
|
||
completion?(progress)
|
||
}else {
|
||
completion?(nil)
|
||
}
|
||
}
|
||
}
|
||
func cancelAllTasksIfNeeded() {
|
||
if let storages = progressStorage {
|
||
// 根据需求,取消所有任务,或者根据任务状态进行过滤
|
||
for key in (storages.keys) {
|
||
session.cancel(key)
|
||
}
|
||
}
|
||
}
|
||
///检索文件是否下载
|
||
func isDownloadedFileDocuments(_ videoId:String, completion: @escaping (Bool) -> Void){
|
||
//优先检索数据库模型是否存在
|
||
MPPositive_DownloadItemModel.fetch(predicate: .init(format: "videoId == %@", videoId)) { [weak self] results in
|
||
DispatchQueue.main.async {
|
||
if results.isEmpty {
|
||
// 数据库中没有记录,直接返回 false
|
||
completion(false)
|
||
} else {
|
||
// 直接在主队列检查文件是否存在,无需单独的全局队列
|
||
completion(self?.getDocumentsExistence(videoId) ?? false)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
///确认该音乐是否下载
|
||
private func getDocumentsExistence(_ videoID: String) -> Bool {
|
||
// 获取Documents目录的URL
|
||
let documentsDirectoryURL = DocumentsURL.appendingPathComponent("Downloads")
|
||
// 根据videoId构建文件完整URL
|
||
let fileURL = documentsDirectoryURL.appendingPathComponent("\(videoID).mp4")
|
||
|
||
// 检查文件是否存在
|
||
return FileManager.default.fileExists(atPath: fileURL.path)
|
||
}
|
||
|
||
///获取沙盒下载路径
|
||
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("文件不存在")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|