Music_Player3/relax.offline.mp3.music/MP/Common/Tool(工具封装)/MP_DownloadManager.swift
2024-10-08 17:34:04 +08:00

449 lines
21 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
//
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(song.playListID ?? "", 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 couldnt 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}
// downloadTasks
guard let tasks = self.downloadTasks else {
print("No download tasks available.")
completion?(false)
return
}
// videoId
if let taskData = tasks[videoIdString] {
completion?(taskData.status == .downloading)
} else {
print("No active task found for videoId: \(videoIdString)")
completion?(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 {
// DocumentsURL
let documentsDirectoryURL = DocumentsURL.appendingPathComponent("Downloads")
// videoIdURL
let fileURL = documentsDirectoryURL.appendingPathComponent("\(videoID).mp4")
//
return FileManager.default.fileExists(atPath: fileURL.path)
}
///
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(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("文件不存在")
}
}
}
}
}