// // MP_CacheManager.swift // MusicPlayer // // Created by Mr.Zhou on 2024/5/23. // import Foundation ///缓存/归档管理工具 class MP_CacheAndArchiverManager { // 单例模式,提供全局访问点 static let shared = MP_CacheAndArchiverManager() //文件管理器 let fileManager = FileManager.default ///归档指定数据字典 // private var archiverDic:[String: MP_PlayerRequestModel]! ///缓存状态更新 private var isCompleteds:[String:Bool] = [:] //缓存过期时间 private var zhoujunfeng_cacheOutTimes:TimeInterval = 172800 private init() { } ///检查缓存文件夹总大小 func zhoujunfeng_checkAndCleanCacheFolder() { //存放所有的缓存文件URL let contents:[URL]! guard let cacheDirectory = createCachePath() else {return} do{ contents = try fileManager.contentsOfDirectory(at: URL(fileURLWithPath: cacheDirectory), includingPropertiesForKeys: [.creationDateKey], options: .skipsHiddenFiles) }catch { print("获取缓存文件夹内容失败:\(error.localizedDescription)") return } //计算缓存文件夹所有的文件总大小 var folderSize: UInt64 = 0 for content in contents { if let fileSize = try? content.resourceValues(forKeys: [.fileSizeKey]).fileSize { folderSize += UInt64(fileSize) } } //超过规定值,就清理最旧的文件 if folderSize > 1024 * 1024 * 1024 * 2 { let sortedFiles = contents.sorted { (url1, url2) -> Bool in let creationDate1 = try? url1.resourceValues(forKeys: [.creationDateKey]).creationDate let creationDate2 = try? url2.resourceValues(forKeys: [.creationDateKey]).creationDate return creationDate1 ?? Date() < creationDate2 ?? Date() } for file in sortedFiles { print("清理\(file.absoluteString)的缓存文件") try? fileManager.removeItem(at: file) folderSize -= UInt64((try? file.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0) if folderSize <= 1024 * 1024 * 1024 * 2 { break } } } } ///创建播放器缓存文件 func createCachePath() -> String? { // 所有缓存文件都放在了沙盒Cache文件夹下MP_PlayerCache文件夹里 let cacheDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! let path = cacheDirectory+"/"+"MP_PlayerCache" //检索这个缓存文件夹是否存在 guard fileManager.fileExists(atPath: path) == false else { //存在,直接返回这个文件夹 return path } //不存在 do { //创建这个缓存文件夹 try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) return path } catch { //创建失败 print("创建播放器缓存文件夹失败,失败原因:\(error)") return nil } } ///创建一个临时文件 func createTempFile(_ videoID:String) -> Bool { //设置临时文件 let path = tempPath(videoID) if fileManager.fileExists(atPath: path) { //存在这个临时文件,将其删除 do{ try fileManager.removeItem(atPath: path) }catch { print("删除临时文件失败,error: \(error)") } } //生成临时文件 return fileManager.createFile(atPath: path, contents: nil, attributes: nil) } ///往临时文件中写入缓存数据 func writeDataToAudioFileTempPathWithData(_ data:Data, videoId:String) { guard let handle = FileHandle(forWritingAtPath: tempPath(videoId)) else {return} if #available(iOS 13.4, *) { do{ try handle.seekToEnd() }catch { print("Seek到末尾失败,失败原因:\(error)") } } else { handle.seekToEndOfFile() } //在最后写入数据 if #available(iOS 13.4, *) { do{ try handle.write(contentsOf: data) }catch{ print("写入数据失败,失败原因:\(error)") } } else { handle.write(data) } do { if #available(iOS 13.4, *) { try? handle.close() // 尝试关闭文件句柄 } else { handle.closeFile() } } } ///读取临时文件数据 func readTempFileDataWithOffset(_ offset:UInt64, length:Int, videoId:String) -> Data? { var allhandle:FileHandle! //检索当前数据是否下载完毕了 if isCompleteds[videoId] == true { //临时文件已经销毁,当前下载内容已经移动到缓存文件中,从缓存文件中读取 let path = audioCachedPath() ?? "" let audioName = "/"+videoId+".mp4" let cachePath = path+audioName guard let handle = FileHandle(forReadingAtPath: cachePath) else {return nil} allhandle = handle }else { let path = tempPath(videoId) guard let handle = FileHandle(forReadingAtPath: path) else {return nil} allhandle = handle } if #available(iOS 13.0, *) { do{ try allhandle.seek(toOffset: offset) }catch{ print("Seek到指定位置失败,失败原因:\(error)") try? allhandle.close() } } else { allhandle.seek(toFileOffset: offset) } if #available(iOS 13.4, *) { do{ let data = try allhandle.read(upToCount: length) try allhandle.close() return data }catch{ print("读取到指定位置失败,失败原因:\(error)") return nil } } else { let data = allhandle.readData(ofLength: length) allhandle.closeFile() return data } } /// 保存临时文件到缓存文件夹 /// - Parameter videoId: 使用videoId作为文件名 func moveAudioFileFromTempPathToCachePath(_ videoId: String) -> Bool { guard let path = audioCachedPath() else {return false } // 1. 判断文件夹是否存在 if !(fileManager.fileExists(atPath: path)) { //不存在 do{ try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true) }catch { print("创建缓存夹失败,失败原因:\(error)") return false } } let audioName = "/"+videoId+".mp4" let cachePath = path+audioName // 检查目标文件是否存在,如果存在,则删除 if fileManager.fileExists(atPath: cachePath) { // do { // print("缓存文件已经存在") // try fileManager.removeItem(atPath: cachePath) // } catch { // print("删除现有文件失败,失败原因: \(error)") // return false // } return true } // 2. 确保源文件存在 let tempFilePath = tempPath(videoId) guard fileManager.fileExists(atPath: tempFilePath) else { // print("源文件不存在于临时路径: \(tempFilePath)") return false } // 3. 尝试移动文件 do { try fileManager.moveItem(atPath: tempFilePath, toPath: cachePath) // print("\(videoId)下载完毕,移动到:\(cachePath)") //当前加载器加载完成 isCompleteds[videoId] = true return true } catch { print("移动 \(videoId) 缓存文件失败,失败原因: \(error)") // deleteKeyValueIfHaveArchivedWithVideoId(videoId) // 执行额外的清理操作 return false } } ///生成缓存文件 private func audioCachedPath() -> String? { return (createCachePath() ?? "") } ///创建一个固定的临时文件路径 private func tempPath(_ videoID:String) -> String { return (NSTemporaryDirectory())+"/"+"\(videoID).mp4" } ///根据VideoID获取缓存文件 func zhoujunfeng_getCachePath(_ videoId:String) -> String? { //获取缓存文件夹 guard let path = MP_CacheAndArchiverManager.shared.createCachePath() else { return nil } //根据videoId获取缓存文件 let audioName = "/"+videoId+".mp4" //缓存文件地址 let audioPath = path+audioName //检查文件是否存在 if fileManager.fileExists(atPath: audioPath) { var attributes:[FileAttributeKey : Any] //存在,检查是否缓存过期 do{ // 获取指定文件的属性 attributes = try fileManager.attributesOfItem(atPath: audioPath) }catch { //获取文件属性失败 print("获取\(videoId)的属性失败,失败原因:\(error.localizedDescription)") return nil } // 从属性字典中提取创建时间 if let creationDate = attributes[.creationDate] as? Date { // print("\(videoId)的创建时间是:",creationDate) //检索文件是否过期 if Date().timeIntervalSince(creationDate) < zhoujunfeng_cacheOutTimes { //没过期 return audioPath }else { //过期了 zhoujunfeng_deleteCachePath(audioPath, videoId: videoId) return nil } } else { print("\(videoId)无法获取文件的创建时间。") return nil } }else { //不存在 return nil } } //删除缓存过期文件 func zhoujunfeng_deleteCachePath(_ path:String, videoId:String) { do{ try fileManager.removeItem(atPath: path) print("\(videoId)的缓存文件已经删除") }catch{ print("\(videoId)的缓存文件删除失败,失败原因:\(error.localizedDescription)") } } }