// // MPNetWorkManager.swift // MusicPlayer // // Created by Mr.Zhou on 2024/4/11. // import UIKit import Network import Alamofire import AVFoundation ///预览闭包(传递一个预览模块数据组和完成状态) typealias BrowseRequestStateBlock = (_ browse:[MPPositive_BrowseModuleListViewModel], _ isCompeleted:Bool) -> Void ///预览数据失败闭包 typealias BrowseRequestErrorBlock = () -> Void ///列表/专辑闭包(传递一个列表数据) typealias ListRequestResultBlock = (_ list:MPPositive_ListAlbumListViewModel) -> Void ///网络状况管理器 class MP_NetWorkManager: NSObject { //单例工具 static let shared = MP_NetWorkManager() //MARK: - 网络请求会话 ///会话实例 private lazy var MPSession:Session = { let configuration = URLSessionConfiguration.af.default //最多同时执行4条 configuration.timeoutIntervalForRequest = 30 configuration.timeoutIntervalForResource = 30 return Alamofire.Session(configuration: configuration, interceptor: MP_CustomRetrier()) }() ///播放资源会话实例 private lazy var PlayerSeesion:Session = { let configuration = URLSessionConfiguration.af.default configuration.timeoutIntervalForRequest = 20 configuration.timeoutIntervalForResource = 20 //最多同时执行4条 configuration.httpMaximumConnectionsPerHost = 4 return Alamofire.Session(configuration: configuration, interceptor: MP_CustomRetrier()) }() ///播放相关内容ID和歌词ID记录组 private var relatedRequests: [String:DataRequest] = [:] ///播放资源请求记录组 private var playerRequests: [String:DataRequest] = [:] //MARK: - API接口 ///IP获取 private let iPInfo:String = "https://api.tikustok.com/app/common/getIPInfo" ///域名链接 private let header:String = "https://music.youtube.com" ///端点 private let point:String = "/youtubei/v1" ///预览接口 private let browse:String = "/browse" ///相关接口 private let next:String = "/next" ///播放器接口 private let player:String = "/player" ///搜索建议接口 private let suggestions:String = "/music/get_search_suggestions" ///搜索接口 private let search = "/search" ///YouTuBe资源键值 private let youTubeKeys:[String] = ["MUSIC_VIDEO_TYPE_ATV","MUSIC_VIDEO_TYPE_OMV","MUSIC_PAGE_TYPE_ALBUM","MUSIC_PAGE_TYPE_ARTIST","MUSIC_PAGE_TYPE_PLAYLIST","MUSIC_PAGE_TYPE_TRACK_LYRICS","MUSIC_PAGE_TYPE_TRACK_RELATED"] ///接口版本默认值 private var clientVersion:String! ///播放接口版本默认值 private var playerVersion:String! ///禁止接入IP信息组 private let banIPs:[String] = [ "CN", "HK", "TW", "JP", "KR" ] ///允许访问的区域Code(对部分内容进行塞选) private let codes:[String] = [ "CL","GB","ID","IN","IT","IL","HU","NZ","ES","UY","UA","UG","GT","TR","TZ","SA","RS","SV","CH","SE","JP","PT","NO","NG","NI","ZA","MX","PE","US","RO","LU","KE","ZW","CZ","CA","HN","NL","KR","CR","CO","FI","FR","EC","RU","DO","DK","BO","PL","IS","BE","BR","PA","PY","AU","AT","EE","IE","EG","AE","AR","ZZ" ] //网络状态 enum NetWorkStatus: String { case notReachable = "网络不可用" case unknown = "网络未知" case reachable = "网络可用" } ///网络监听器 private var monitor:NWPathMonitor ///对监听器的判断 private var isReach:Bool = false ///当前网络状态 var netWorkStatu:NetWorkStatus!{ willSet{ guard newValue != netWorkStatu else { //重复情况 return } switch newValue { case .reachable://网络可用 print("网络可用") NotificationCenter.notificationKey.post(notificationName: .net_switch_reachable) case .notReachable://不可用 print("网络不可用") NotificationCenter.notificationKey.post(notificationName: .net_switch_notReachable) case .unknown://位置情况 print("网络状况未知") case .none: break } } } //MARK: - 固定参数 //访问数据(首次首页预览时获得) private var visitorData:String? //固定时间点(同一天的上一周) private lazy var currTimeDate:String = (Date().timeZone() - 7.days).toString(.custom("YYYYMMdd")) ///地址 private var locaton:String? = "HK" //预览下一阶段参数(网络请求获取) private var continuationAndItct:(String?,String?){ willSet{ //当获取新值后,判断新值是否存在,存在则继续串行异步请求 guard let continuation = newValue.0, let itct = newValue.1, browseQueque != nil, let url = URL(string: header+point+browse) else { //首页基础数据已经获取完毕 //调用艺术家排行榜 requestArtistsRank() return } //生成新参数 let parameters:[String:Any] = [ "ctoken":continuation, "continuation":continuation, "type":"next", "itct":itct, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", "visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "HK" ] ] ] //执行异步请求 browseQueque?.sync { [weak self] in guard let self = self else { return } requestPostHomeBrowse(url, parameters: parameters) } } } //MARK: - GCD队列任务 ///网络检测队列 private let MPNetWorkqueue = DispatchQueue(label: "MPNetWorkManager") ///A/B任务处理队伍 private var executionQueue: [() -> Void] = [] ///串行队列-预览 private var browseQueque:DispatchQueue? ///并发队列-单曲资源预加载 var playerItemLoadingGroup:DispatchGroup = DispatchGroup() //MARK: - 闭包 ///预览闭包(传递一个预览模块数据和完成状态) var browseRequestStateBlock:BrowseRequestStateBlock? ///预览失败闭包 var browseRequestErrorBlock:BrowseRequestErrorBlock? //私有初始化 private override init() { self.monitor = NWPathMonitor() super.init() reloadVersion() } //更新版本默认值 func reloadVersion() { if let cv = UserDefaults.standard.string(forKey: "ClientVersion") { print("成功更新数据版本") clientVersion = cv } if let pv = UserDefaults.standard.string(forKey: "PlayerVersion") { playerVersion = pv } } //MARK: - 网络情况 ///检查网络状况 func requestNetworkPermission(oberve:UIViewController, completeHanlder:ActionBlock?) { switch netWorkStatu { case .reachable: DispatchQueue.main.async { guard completeHanlder != nil else { return } completeHanlder!() } default: DispatchQueue.main.async { //次要处理 let alertController = UIAlertController(title: "Access network request", message: "”Musiclax“ needs to be loaded via a network request. Please click “Settings” to allow this application to gain access to the network.", preferredStyle: .alert) let CancelAction = UIAlertAction(title:"Cancel", style: .cancel) { (Cancel) in } let OKAction = UIAlertAction(title: "Settings", style: .default) { (action) in let url = URL(string: UIApplication.openSettingsURLString) if let url = url,UIApplication.shared.canOpenURL(url){ if #available(iOS 10, *) { UIApplication.shared.open(url, options: [:]) { (success) in } }else{ UIApplication.shared.canOpenURL(url) } } } alertController.addAction(CancelAction) alertController.addAction(OKAction) oberve.present(alertController, animated: true, completion: nil) } } } ///网络请求检测 func requestStatusToYouTube() { guard isReach == false else { return } isReach = true monitor.start(queue: MPNetWorkqueue) monitor.pathUpdateHandler = { [weak self] path in self?.updateNetworkStatus(path) } } private func updateNetworkStatus(_ path: NWPath) { if path.status == .satisfied { netWorkStatu = .reachable executePendingTasks() } else { netWorkStatu = .notReachable } } //AB面检测队列任务 func performTaskNetWrokAvailable(_ task: @escaping() -> Void) { //根据当前网络情况决定是执行任务,还是将任务放置到队列 if netWorkStatu == .reachable { //网络可以执行任务 task() }else { //网络不可执行任务,放入队列 executionQueue.append(task) } } //当网络恢复可用后立刻处理队列任务 private func executePendingTasks() { while !executionQueue.isEmpty { let task = executionQueue.removeFirst() task() } } } //MARK: - API请求 extension MP_NetWorkManager { //MARK: - 请求iP信息 ///请求IP信息 func requestIPInfo(_ completion:@escaping((Bool) -> Void)) { //拼接出browse路径 let path = iPInfo //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } requestPostIPInfo(url) { open in completion(open) } } private func requestPostIPInfo(_ url:URL, completion:@escaping((Bool) -> Void)) { MPSession.request(url, method: .get, encoding: JSONEncoding.default).responseDecodable(of: JsonIPInfo.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): guard let data = value.data, let code = data.isoCode else { return } locaton = code if banIPs.contains(code) == true { //包含,是禁止区域 completion(false) }else { //不包含,是通行区域 completion(true) } case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) completion(true) } } } //MARK: - 请求首页预览 ///向YouTubemusic请求预览/首页数据 func requestBrowseDatas() { //实行串行异步队列,进行多次请求。由于第一次之后的请求都必须携带对应的continuation编码,所以串行队列。直到最后一次请求的continuation值为空,销毁队列 // 实例化串行队列 browseQueque = DispatchQueue(label: "com.request.browseQueque") visitorData = nil //拼接出browse路径 let path = header+point+browse //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //异步执行任务 browseQueque?.sync { [weak self] in //进行第一次请求,有预览ID,无continuation编码 guard let self = self else { return } let parameters:[String:Any] = [ "browseId": "FEmusic_home", "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostHomeBrowse(url, parameters: parameters) } } //请求首页预览内容(执行多次) private func requestPostHomeBrowse(_ url:URL, parameters:Parameters) { //发送post请求,并将结果转为RootBrowses MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonBrowses.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): if value.responseContext?.visitorData != nil { self.visitorData = value.responseContext?.visitorData } //解析结构体 let tab = value.contents?.singleColumnBrowseResultsRenderer?.tabs?[0] if let content = tab?.tabRenderer?.content { parsingBrowseContents(content) }else if let continuationContents = value.continuationContents { parsingBrowseContinuationContents(continuationContents) }else { //不在服务范围内 print("Failed to parse browses content") // 请求失败,处理错误 homeError(error: nil) browseQueque = nil } case .failure(let error): print("Failed to parse browses content") // 请求失败,处理错误 homeError(error: error) browseQueque = nil } } } ///执行艺术家排行请求 private func requestArtistsRank() { //拼接出browse路径 let path = header+point+browse //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //异步执行任务 browseQueque?.sync { [weak self] in guard let self = self else { return } var code = locaton ?? "" if codes.contains(code) == false { //没有包含在内,默认替换为US美国国家码 code = "US" } let parameters:[String:Any] = [ "browseId": "FEmusic_charts", "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ], "formData":[ "selectedValues":[code] ] ] requestPostArtistsRank(url, parameters: parameters) } } ///请求艺术家排行 private func requestPostArtistsRank(_ url:URL, parameters:Parameters) { MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonCharts.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let charts): if let content = charts.contents?.singleColumnBrowseResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents { parsingCharts(content) } case .failure(let error): print("Failed to parse browses content") // 请求失败,处理错误 homeError(error: error) browseQueque = nil } } } //MARK: - 请求列表专辑预览 /// 向YouTubemusic请求列表/专辑数据,该接口调用的同样是browse预览接口 /// - Parameters: /// - browseId: 专辑列表的Id /// - params: 专辑列表的访问编码 func requestAlbumOrListDatas(_ browseId: String, params: String, comletion:@escaping (MPPositive_ListAlbumListViewModel) -> Void) { //拼接出browse路径 let path = header+point+browse //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数,browseId与params参数是必定携带内容 let parameters:[String:Any] = [ "browseId":browseId, "params":params, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", // "visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] // //guard netWorkStatu != .notReachable else {return} requestPostAlbumOrList(url, parameters: parameters) { results in comletion(results) } } //请求列表/专辑数据 private func requestPostAlbumOrList(_ url:URL, parameters:Parameters, comletion:@escaping (MPPositive_ListAlbumListViewModel) -> Void) { //发送post请求,并将结果转为RootBrowses MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonListOrAlbum.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): let contents = value.contents //创建一个列表ViewModel let list = MPPositive_ListAlbumListViewModel() if let content = contents?.singleColumnBrowseResultsRenderer?.tabs?.first?.tabRenderer?.content {//使用一号版本 list.items = parsingListSingleContents(content) if let header = value.header { list.header = .init(parsingListSingleHeaders(header)) } //列表数据完善,回调 comletion(list) }else if let content = contents?.twoColumnBrowseResultsRenderer?.secondaryContents {//使用二号版本 list.items = parsingListTwoContents(content) if let musicResponsiveHeaderRenderer = contents?.twoColumnBrowseResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents?.first?.musicResponsiveHeaderRenderer { list.header = .init(parsingListTwoHeaders(musicResponsiveHeaderRenderer)) } //列表数据完善,回调 comletion(list) } case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } ///请求艺术家 func requestArtist(_ browseId:String, comletion:@escaping (MPPositive_ArtistViewModel) -> Void) { //拼接出browse路径 let path = header+point+browse //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数,browseId与params参数是必定携带内容 let parameters:[String:Any] = [ "browseId":browseId, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostArtist(url, parameters: parameters) { result in comletion(result) } } //请求艺术家信息 private func requestPostArtist(_ url:URL, parameters:Parameters, comletion:@escaping (MPPositive_ArtistViewModel) -> Void) { //发送post请求,并将结果转为RootBrowses MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonArtist.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): //解析value let artist = MPPositive_ArtistViewModel() if let header = value.header { artist.header = parsingArtistHeaders(header) } if let contents = value.contents?.singleColumnBrowseResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents { artist.lists = parsingArtistContents(contents) } comletion(artist) case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } ///请求艺术家更多数据 func requestArtistMore(_ browseId:String, params: String, comletion:@escaping (([MPPositive_BrowseItemViewModel], String?, String?)) -> Void) { //拼接出browse路径 let path = header+point+browse //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数,browseId与params参数是必定携带内容 let parameters:[String:Any] = [ "browseId":browseId, "params":params, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostArtistMore(url, parameters: parameters) { result in comletion(result) } } ///请求艺术家更多数据 private func requestPostArtistMore(_ url:URL, parameters:Parameters, comletion:@escaping (([MPPositive_BrowseItemViewModel], String?, String?)) -> Void) { //发送post请求,并将结果转为RootBrowses MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonArtistMore.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): if let content = value.contents?.singleColumnBrowseResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents?.first { let result = parsingArtistMore(content) comletion(result) } case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } ///请求艺术家更多数据继续 func requestArtistMoreContinuation(_ continuation:String, itct:String, comletion:@escaping (([MPPositive_BrowseItemViewModel], String?, String?)) -> Void) { //拼接出browse路径 let path = header+point+browse //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数,browseId与params参数是必定携带内容 let parameters:[String:Any] = [ "continuation":continuation, "ctoken":continuation, "type":"next", "itct":itct, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostArtistMoreContinuation(url, parameters: parameters) { result in comletion(result) } } ///请求艺术家更多数据继续 private func requestPostArtistMoreContinuation(_ url:URL, parameters:Parameters, comletion:@escaping (([MPPositive_BrowseItemViewModel], String?, String?)) -> Void) { //发送post请求,并将结果转为RootBrowses MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonArtistMore.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): if let musicPlaylistShelfContinuation = value.continuationContents?.musicPlaylistShelfContinuation { let result = parsingArtistMoreContinuation(musicPlaylistShelfContinuation) comletion(result) } case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } ///请求模块数据 func requestGenres(_ completion:@escaping (([MPPositive_GridViewModel]) -> Void)) { //拼接出browse路径 let path = header+point+browse //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数 let parameters:[String:Any] = [ "browseId": "FEmusic_moods_and_genres", "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] requestPostGenres(url, parameters: parameters) { array in completion(array) } } ///请求模块数据 private func requestPostGenres(_ url:URL, parameters:Parameters, completion:@escaping (([MPPositive_GridViewModel]) -> Void)) { //发送post请求 MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonGenres.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): if let contents = value.contents?.singleColumnBrowseResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents { let array = parsingGenres(contents) completion(array) } case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } ///请求模块详情数据 func requestMoodDetails(_ browseId:String, params:String, completion:@escaping (([MPPositive_BrowseModuleListViewModel]) -> Void)) { //拼接出browse路径 let path = header+point+browse //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数 let parameters:[String:Any] = [ "browseId": browseId, "params":params, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] requestPostMoodDetails(url, parameters: parameters) { array in completion(array) } } ///请求模块详情数据 private func requestPostMoodDetails(_ url:URL, parameters:Parameters, completion:@escaping (([MPPositive_BrowseModuleListViewModel]) -> Void)) { MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonBrowses.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): //解析结构体 let tab = value.contents?.singleColumnBrowseResultsRenderer?.tabs?[0] if let content = tab?.tabRenderer?.content { let array = parsingMoodDetails(content) completion(array) } case .failure(let error): print("Failed to parse browses content") // 请求失败,处理错误 handleError(url, error: error) } } } //MARK: - 请求列表专辑下一部分 ///请求Next列表(优先于Player) /// - Parameter item: 请求的预览实体 func requestNextList(_ browseId:String, videoId:String, completion:@escaping(([MPPositive_SongItemModel]) -> Void)) { //拼接出next路径 let path = header+point+next //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数,videoId与params参数是必定携带内容 let parameters:[String:Any] = [ "playlistId":browseId, "videoId":videoId, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostNextList(url, parameters: parameters) { listSongs in //成功拿到列表所有歌曲(内容尚不完善) completion(listSongs) } } //请求next列表 private func requestPostNextList(_ url:URL, parameters:Parameters, completion:@escaping (([MPPositive_SongItemModel]) -> Void)) { //发送post请求 MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonNext.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): //转化为list值 let listSongs:[MPPositive_SongItemModel] = parsingNextList(value) completion(listSongs) case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } ///请求Next歌词/相关内容 /// - Parameter item: 请求的预览实体 func requestNextLyricsAndRelated(_ item: MPPositive_SongItemModel, completion:@escaping(((String?,String?)) -> Void)) { guard netWorkStatu != .notReachable else { completion((nil,nil)) return } //拼接出next路径 let path = header+point+next //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数,videoId与params参数是必定携带内容 let parameters:[String:Any] = [ "videoId":(item.videoId ?? ""), "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostNextLyricsAndRelated(url, videoId: item.videoId, parameters: parameters) { result in completion(result) } } //请求请求Next歌词/相关内容 private func requestPostNextLyricsAndRelated(_ url:URL, videoId:String, parameters:Parameters, completion:@escaping(((String?,String?)) -> Void)) { //发送post请求 let request = MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonNext.self) { [weak self] (response) in guard let self = self else {return} relatedRequests[videoId] = nil switch response.result { case .success(let value): let result = parsingNextLyricsAndRelated(value) //回掉数据 completion(result) case .failure(let error): //当前无数据 completion((nil,nil)) // 请求失败,处理错误 handleError(url, error: error, status: false) } } relatedRequests[videoId] = request } //MARK: - 请求player播放资源 /// 请求Player(单曲/视频)播放资源 /// - Parameter item: 请求的预览实体 func requestAndroidPlayer(_ videoId: String, playlistId: String, completion:@escaping ((([String],[Int],[String])?, [String]?) -> Void)){ guard netWorkStatu != .notReachable else { completion(nil,nil) return } //拼接出player路径 let path = header+point+player //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数,videoId与params参数是必定携带内容 let parameters:[String:Any] = [ "videoId":videoId, "prettyPrint":"false", "context":[ "client":[ "clientName": "ANDROID_MUSIC", "clientVersion": playerVersion, "platform":"MOBILE", "browserVersion":"125.0.0.0", ] ] ] //guard netWorkStatu != .notReachable else {return} requestAndroidPostPlayer(url, videoId: videoId, parameters: parameters){ resourceUlrs, coverUrls in completion(resourceUlrs, coverUrls) } } private func requestAndroidPostPlayer(_ url:URL, videoId:String, parameters:Parameters, completion:@escaping((([String],[Int],[String])?, [String]?) -> Void)) { //发送post请求 let request = PlayerSeesion.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonAndroidPlayer.self) { [weak self] (response) in guard let self = self else {return} self.playerRequests[videoId] = nil // 清除请求 switch response.result { case .success(let value): parsingAndroidPlayer(value) { resourceUlrs, coverUrls in completion(resourceUlrs, coverUrls) } case .failure(let error): //当前无数据 completion(nil,nil) // 请求失败,处理错误 handleError(url, error: error, status: false) } } //根据当前videoId添加request playerRequests[videoId] = request } ///移除补全任务请求任务 func removeVideoResource(_ videoId: String) { // playerRequests[videoId]?.cancel() playerRequests.removeValue(forKey: videoId) // relatedRequests[videoId]?.cancel() relatedRequests.removeValue(forKey: videoId) } // func requestPlayer(_ videoId: String, playlistId: String, completion:@escaping ((([String],[Float],[String]), [String]?) -> Void)){ // //拼接出player路径 // let path = header+point+player // //设置url // guard let url = URL(string: path) else { // print("Url is Incorrect") // return // } // //设置参数,videoId与params参数是必定携带内容 // let parameters:[String:Any] = [ // "videoId":videoId, // "prettyPrint":"false", // "context":[ // "client":[ // "clientName": "WEB_REMIX", //// //"visitorData":visitorData, //// "originalUrl":"https://music.youtube.com/watch?v=\(videoId)", // //当前访问版本(日期值) // "clientVersion": clientVersion, // "platform":"MOBILE", // //语言 // "hl":Language_first_local, // //地址 // "gl":locaton ?? "" // ] // ], // "playbackContext": [ // "contentPlaybackContext": [ // "signatureTimestamp": MP_WebWork.shared.signatureTimestamp ?? 0 // ] // ] // ] // //guard netWorkStatu != .notReachable else {return} // requestPostPlayer(url, parameters: parameters){ resourceUlrs, coverUrls in // completion(resourceUlrs, coverUrls) // } // } //请求单曲/视频 // private func requestPostPlayer(_ url:URL, parameters:Parameters, completion:@escaping((([String],[Float],[String]), [String]?) -> Void)) { // //发送post请求 // MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonPlayer.self) { [weak self] (response) in // guard let self = self else {return} // // switch response.result { // case .success(let value): // parsingPlayer(value) { resourceUlrs, coverUrls in // completion(resourceUlrs, coverUrls) // } // case .failure(let error): // // 请求失败,处理错误 // print("Request failed: \(error)") // } // } // } //MARK: - 请求歌词 /// 请求歌词 /// - Parameter lyricId: 歌词id func requestLyric(_ lyricId:String, completion:@escaping((String) -> Void)) { //拼接出browse路径 let path = header+point+browse //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数,browseId与params参数是必定携带内容 let parameters:[String:Any] = [ "browseId":lyricId, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostLyric(url, parameters: parameters) { lyrics in completion(lyrics) } } //请求歌词 private func requestPostLyric(_ url:URL, parameters:Parameters, completion:@escaping((String) -> Void)) { //发送post请求 MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonLyrics.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): completion(parsingLyrics(value) ?? "") case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } ///请求相关内容 func requestRecommend(_ browseId:String, completion: @escaping ([MPPositive_RecommendListViewModel]) -> Void) { //拼接出browse路径 let path = header+point+browse //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数,browseId与params参数是必定携带内容 let parameters:[String:Any] = [ "browseId":browseId, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostRecommend(url, parameters: parameters) { results in completion(results) } } ///请求相关内容 private func requestPostRecommend(_ url:URL, parameters:Parameters, completion: @escaping ([MPPositive_RecommendListViewModel]) -> Void) { //发送post请求 MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonRecommend.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): if let contents = value.contents?.sectionListRenderer?.contents { let section = parsingRecommend(contents) completion(section) } case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } //MARK: - 请求搜索建议 /// 请求搜索建议 /// - Parameter content: 用户输入的文本 func requestSearchSuggestions(_ content:String, completion:@escaping(([[MPPositive_SearchSuggestionItemModel]]) -> Void)) { //拼接路径 let path = header+point+suggestions //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数 let parameters:[String:Any] = [ "input":content, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostSearchSuggestions(url, parameters: parameters) { result in completion(result) } } //请求搜索建议 private func requestPostSearchSuggestions(_ url:URL, parameters:Parameters, completion:@escaping(([[MPPositive_SearchSuggestionItemModel]]) -> Void)) { //发送post请求 MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchSuggestions.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): parsingSearchSuggestions(value) { results in completion(results) } case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } /// 请求搜索预览结果 /// - Parameter text: 用户请求文本 func requestSearchPreviewResults(_ text:String, completion:@escaping (([MPPositive_SearchResultListViewModel]) -> Void)) { //拼接路径 let path = header+point+search //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数 let parameters:[String:Any] = [ "query":text, "prettyPrint":"false", "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostSearchPreviewResults(url, parameters: parameters) { result in completion(result) } } //请求搜索预览结果 private func requestPostSearchPreviewResults(_ url:URL, parameters:Parameters, completion:@escaping (([MPPositive_SearchResultListViewModel]) -> Void)) { //发送post请求 MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchPreviewResults.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): if let contents = value.contents?.tabbedSearchResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents { let result = parsingSearchPreviewResults(contents) print("一共搜索到\(result.count)相关内容模块") completion(result) } case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } /// 请求搜索分类结果 /// - Parameters: /// - query: 搜索文本 /// - params: 事件参数码 func requestSearchTypeResults(_ query:String, params:String, completion:@escaping (([MPPositive_SearchResultItemViewModel], String?, String?))->Void) { //拼接路径 let path = header+point+search //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数 let parameters:[String:Any] = [ "query":query, "prettyPrint":"false", "params":params, "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostSearchTypeResults(url, parameters: parameters) { result in completion(result) } } //请求搜索分类结果 private func requestPostSearchTypeResults(_ url:URL, parameters:Parameters, completion:@escaping (([MPPositive_SearchResultItemViewModel], String?, String?))->Void) { //发送post请求 MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchTypeResults.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): if let musicShelfRenderer = value.contents?.tabbedSearchResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents?.first?.musicShelfRenderer { completion(parsingSearchTypeResults(musicShelfRenderer)) } case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } /// 请求搜索分类继续结果 /// - Parameters: /// - continuation: 继续编码 /// - itct: 继续事件码 func requestSearchTypeContinuation(_ continuation:String, itct:String, completion:@escaping (([MPPositive_SearchResultItemViewModel], String?, String?))->Void) { //拼接路径 let path = header+point+search //设置url guard let url = URL(string: path) else { print("Url is Incorrect") return } //设置参数 let parameters:[String:Any] = [ "ctoken":continuation, "continuation":continuation, "type":"next", "itct":itct, "prettyPrint":false, "context":[ "client":[ //web端 "clientName": "WEB_REMIX", //"visitorData":visitorData, //当前访问版本(日期值) "clientVersion": clientVersion, "platform":"MOBILE", //语言 "hl":Language_first_local, //地址 "gl":locaton ?? "" ] ] ] //guard netWorkStatu != .notReachable else {return} requestPostSearchTypeContinuation(url, parameters: parameters) { result in completion(result) } } //请求搜索分类继续结果 private func requestPostSearchTypeContinuation(_ url:URL, parameters:Parameters, completion:@escaping (([MPPositive_SearchResultItemViewModel], String?, String?))->Void) { //发送post请求 MPSession.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchTypeContinuation.self) { [weak self] (response) in guard let self = self else {return} switch response.result { case .success(let value): if let musicShelfContinuation = value.continuationContents?.musicShelfContinuation { completion(parsingSearchTypeContinuation(musicShelfContinuation)) } case .failure(let error): // 请求失败,处理错误 handleError(url, error: error) } } } ///首页错误处理 private func homeError(error:AFError?) { // 根据错误类型处理 if let statusCode = error?.responseCode { switch statusCode { case 400...499: print("请求错误,错误码: \(statusCode)。") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("Request Error, Code:\(statusCode)") MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists() return case 500...599: print("服务器错误,错误码: \(statusCode)。") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("Server Error, Code:\(statusCode)") default: print("其他 HTTP 错误,错误码: \(statusCode)。") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("HTTP Error, Code:\(statusCode)") } } else if let underlyingError = error?.underlyingError as? URLError { switch underlyingError.code { case .notConnectedToInternet: print("网络连接不可用,请检查你的网络设置") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("Network Unavailable, Error:\(underlyingError.localizedDescription)") case .timedOut: print("请求超时,即将重启请求") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("Timed Out, Error:\(underlyingError.localizedDescription)") MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists() return case .networkConnectionLost: print("网络权限开启,但网络本身不可用,请检查你的网络设置") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("International comics not yet launched, Error:\(underlyingError.localizedDescription)") case .cannotLoadFromNetwork: print("请求证书失效,即将重启请求") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("SSL Error, Error:\(underlyingError.localizedDescription)") MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists() return case .cannotConnectToHost: print("服务器响应失败,请待会儿调用") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("Server not present, Error:\(underlyingError.localizedDescription)") case .badURL: print("链接失败,即将重启请求") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("Bad URL, Error:\(underlyingError.localizedDescription)") MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists() return case .cannotDecodeContentData: print("解析响应体失败,即将重启请求") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("Can't Decode Content Data, Error:\(underlyingError.localizedDescription)") MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists() return default: print("NSURL 错误,错误码: \(underlyingError.code.rawValue)。") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("Request Error, Error:\(underlyingError.localizedDescription)") } } else { print("未知错误: \(error?.localizedDescription ?? "")") MP_AnalyticsManager.shared.home_b_module_showfailure_errorAction("No Area") } if browseRequestErrorBlock != nil { browseRequestErrorBlock!() } } ///通用错误处理方法 private func handleError(_ url: URL, error:AFError?, status:Bool = true) { // 根据错误类型处理 if let statusCode = error?.responseCode { switch statusCode { case 400...499: print("\(url)请求错误,错误码: \(statusCode)。") case 500...599: print("\(url)服务器错误,错误码: \(statusCode)。") default: print("\(url)其他 HTTP 错误,错误码: \(statusCode)。") } } else if let underlyingError = error?.underlyingError as? URLError { switch underlyingError.code { case .notConnectedToInternet: print("\(url)网络连接不可用,请检查你的网络设置。") case .timedOut: print("\(url)请求超时,请稍后重试。") default: print("\(url)NSURL 错误,错误码: \(underlyingError.code.rawValue)。") } } else { print("\(url)未知错误: \(error?.localizedDescription ?? "")") } if status { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { //统一发一个报错通知 NotificationCenter.notificationKey.post(notificationName: .netWork_error_deal) } } } } //MARK: - 数据解析 extension MP_NetWorkManager { //MARK: - 浅层解析 ///解析预览内容_Contents private func parsingBrowseContents(_ content:JsonBrowses.Contents.SingleColumnBrowseResultsRenderer.Tab.TabRenderer.Content) { //获取预览结构体中需要的数据(模块标题;歌曲信息:封面路径,音乐标题,歌手姓名,列表id和视频id) var browses:[MPPositive_BrowseModuleListViewModel] = [] var continuation:String? var itct:String? //循环获取音乐内容 content.sectionListRenderer?.contents?.forEach({ content in //该循环获取预览模块内容,生成一个预览模型接收数据 let browse = MPPositive_BrowseModuleListViewModel() browse.title = content.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.first?.text //循环音乐内容组 content.musicCarouselShelfRenderer?.contents?.forEach({ content in //设置标题、歌手、专辑/列表 if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { if let item = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { browse.items.append(.init(item)) } }else if let musicTwoRowItemRenderer = content.musicTwoRowItemRenderer { browse.items.append(.init(parsingMusicTwoRowItemRenderer(musicTwoRowItemRenderer))) } }) browses.append(browse) }) if let nextContinuationData = content.sectionListRenderer?.continuations?.first { continuation = nextContinuationData.nextContinuationData?.continuation itct = nextContinuationData.nextContinuationData?.clickTrackingParams } guard browseRequestStateBlock != nil else { return } //传递解析数据 self.browseRequestStateBlock!(browses, false) //重新赋值,直到没有下一阶段数据为止 self.continuationAndItct = (continuation,itct) } ///排行榜艺术家数据解析 private func parsingCharts(_ contents:[JsonCharts.Contents.SingleColumnBrowseResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content]) { var browses:[MPPositive_BrowseModuleListViewModel] = [] contents.forEach { content in //检索content是否存在二阶段数据 if let musicCarouselShelfRenderer = content.musicCarouselShelfRenderer { //存在,继续操作 let browse = MPPositive_BrowseModuleListViewModel() browse.title = musicCarouselShelfRenderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.first?.text //获取数据 musicCarouselShelfRenderer.contents?.forEach({ content in if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { let item = MPPositive_BrowseItemModel() //设置封面 item.coverUrls = musicResponsiveListItemRenderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.compactMap({$0.url}) //设置标题 for (index, flexColumn) in (musicResponsiveListItemRenderer.flexColumns ?? []).enumerated() { if index == 0 { //添加主标题 item.title = flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) }else { item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "") } } //设置ID item.itemType = .artist item.browseId = musicResponsiveListItemRenderer.navigationEndpoint?.browseEndpoint?.browseId item.artistId = musicResponsiveListItemRenderer.navigationEndpoint?.browseEndpoint?.browseId item.pageType = musicResponsiveListItemRenderer.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType browse.items.append(.init(item)) } }) browses.append(browse) } } //清理为空的数据 browses = browses.filter({$0.items.contains(where: {$0.browseItem.pageType == "MUSIC_PAGE_TYPE_ARTIST"})}) guard browseRequestStateBlock != nil else { return } //传递解析数据 self.browseRequestStateBlock!(browses, true) //移除队列 browseQueque = nil } ///解析预览内容_ContinuationContents private func parsingBrowseContinuationContents(_ continuationContents:JsonBrowses.ContinuationContents) { //获取预览结构体中需要的数据(模块标题;歌曲信息:封面路径,音乐标题,歌手姓名,列表id和视频id) var browses:[MPPositive_BrowseModuleListViewModel] = [] var continuation:String? var itct:String? //循环获取音乐内容 continuationContents.sectionListContinuation?.contents?.forEach({ content in //该循环获取预览模块内容,生成一个预览模型接收数据 let browse = MPPositive_BrowseModuleListViewModel() browse.title = content.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.first?.text //循环音乐内容组 content.musicCarouselShelfRenderer?.contents?.forEach({ content in //设置标题、歌手、专辑/列表 if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { if let item = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { browse.items.append(.init(item)) } }else if let musicTwoRowItemRenderer = content.musicTwoRowItemRenderer { browse.items.append(.init(parsingMusicTwoRowItemRenderer(musicTwoRowItemRenderer))) } }) browses.append(browse) }) if let nextContinuationData = continuationContents.sectionListContinuation?.continuations?.first { continuation = nextContinuationData.nextContinuationData?.continuation itct = nextContinuationData.nextContinuationData?.clickTrackingParams } guard browseRequestStateBlock != nil else { return } //传递解析数据 self.browseRequestStateBlock!(browses, false) //重新赋值,直到没有下一阶段数据为止 self.continuationAndItct = (continuation,itct) } ///解析列表_Contents_一号版本 private func parsingListSingleContents(_ content:JsonListOrAlbum.Contents.SingleColumnBrowseResultsRenderer.Tab.TabRenderer.Content) -> [MPPositive_BrowseItemViewModel] { var items:[MPPositive_BrowseItemViewModel] = [] //循环获取音乐内容 content.sectionListRenderer?.contents?.forEach({ content in //循环音乐内容组 if let musicPlaylistShelfRenderer = content.musicPlaylistShelfRenderer { musicPlaylistShelfRenderer.contents?.forEach({ content in //设置标题、歌手、专辑/列表 if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { if let item = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { items.append(.init(item)) } }else if let musicTwoRowItemRenderer = content.musicTwoRowItemRenderer { items.append(.init(parsingMusicTwoRowItemRenderer(musicTwoRowItemRenderer))) } }) }else if let musicShelfRenderer = content.musicShelfRenderer{ musicShelfRenderer.contents?.forEach({ content in //设置标题、歌手、专辑/列表 if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { if let item = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { items.append(.init(item)) } }else if let musicTwoRowItemRenderer = content.musicTwoRowItemRenderer { items.append(.init(parsingMusicTwoRowItemRenderer(musicTwoRowItemRenderer))) } }) } }) return items } ///解析列表_Contents_二号版本 private func parsingListTwoContents(_ content:JsonListOrAlbum.Contents.TwoColumnBrowseResultsRenderer.SecondaryContents) -> [MPPositive_BrowseItemViewModel] { var items:[MPPositive_BrowseItemViewModel] = [] //循环获取音乐内容 content.sectionListRenderer?.contents?.forEach({ content in //循环音乐内容组 if let musicPlaylistShelfRenderer = content.musicPlaylistShelfRenderer { musicPlaylistShelfRenderer.contents?.forEach({ content in //设置标题、歌手、专辑/列表 if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { if let item = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { items.append(.init(item)) } }else if let musicTwoRowItemRenderer = content.musicTwoRowItemRenderer { items.append(.init(parsingMusicTwoRowItemRenderer(musicTwoRowItemRenderer))) } }) }else if let musicShelfRenderer = content.musicShelfRenderer{ musicShelfRenderer.contents?.forEach({ content in //设置标题、歌手、专辑/列表 if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { if let item = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { items.append(.init(item)) } }else if let musicTwoRowItemRenderer = content.musicTwoRowItemRenderer { items.append(.init(parsingMusicTwoRowItemRenderer(musicTwoRowItemRenderer))) } }) } }) return items } ///解析列表_Header_一号版本 private func parsingListSingleHeaders(_ header:JsonListOrAlbum.Header) -> MPPositive_ListHeaderModel { let listHeader = MPPositive_ListHeaderModel() if let musicDetailHeaderRenderer = header.musicDetailHeaderRenderer { //主标题 listHeader.maintitle = musicDetailHeaderRenderer.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) //次标题 listHeader.subtitle = musicDetailHeaderRenderer.subtitle?.runs?.reduce("", { $0 + ($1.text ?? "")}) //三级标题 listHeader.thirdtitle = musicDetailHeaderRenderer.secondSubtitle?.runs?.reduce("", { $0 + ($1.text ?? "")}) //说明/介绍 listHeader.forDescription = musicDetailHeaderRenderer.description?.runs?.reduce("", { $0 + ($1.text ?? "")}) //图片(默认获取最后/最大的) listHeader.coverUrl = musicDetailHeaderRenderer.thumbnail?.croppedSquareThumbnailRenderer?.thumbnail?.thumbnails?.last?.url }else if let musicImmersiveHeaderRenderer = header.musicImmersiveHeaderRenderer { listHeader.maintitle = musicImmersiveHeaderRenderer.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) listHeader.forDescription = musicImmersiveHeaderRenderer.description?.runs?.reduce("", { $0 + ($1.text ?? "")}) listHeader.coverUrl = musicImmersiveHeaderRenderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.last?.url } return listHeader } ///解析列表_Header_二号版本 private func parsingListTwoHeaders(_ header:JsonListOrAlbum.Contents.TwoColumnBrowseResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content.MusicResponsiveHeaderRenderer) -> MPPositive_ListHeaderModel { let listHeader = MPPositive_ListHeaderModel() //封面 if let thumbnail = header.thumbnail { listHeader.coverUrl = thumbnail.musicThumbnailRenderer?.thumbnail?.thumbnails?.last?.url } //标题 if let title = header.title { listHeader.maintitle = title.runs?.reduce("", { $0 + ($1.text ?? "")}) } //副标题 if let subtitle = header.subtitle { listHeader.subtitle = subtitle.runs?.reduce("", { $0 + ($1.text ?? "")}) } //说明 if let description = header.description?.musicDescriptionShelfRenderer?.description?.runs?.first?.text { //对说明文本裁剪 let _d = truncateTextAfterTwoNewlines(from: description) listHeader.forDescription = _d } return listHeader } ///解析艺术家_Header private func parsingArtistHeaders(_ header:JsonArtist.Header) -> MPPositive_ArtistHeaderModel { let model = MPPositive_ArtistHeaderModel() model.title = header.musicImmersiveHeaderRenderer?.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) model.subscriptions = header.musicImmersiveHeaderRenderer?.subscriptionButton?.subscribeButtonRenderer?.subscriberCountText?.runs?.reduce("", { $0 + ($1.text ?? "")}) model.subscriptionedText = header.musicImmersiveHeaderRenderer?.subscriptionButton?.subscribeButtonRenderer?.subscribedButtonText?.runs?.reduce("", { $0 + ($1.text ?? "")}) model.fordescription = header.musicImmersiveHeaderRenderer?.description?.runs?.reduce("", { $0 + ($1.text ?? "")}) model.fordescription = truncateTextAfterTwoNewlines(from: model.fordescription ?? "") model.thumbnails = header.musicImmersiveHeaderRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.compactMap({$0.url}) return model } ///解析艺术家_Contents private func parsingArtistContents(_ contents:[JsonArtist.Contents.SingleColumnBrowseResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content]) -> [MPPositive_ArtistContentListViewModel] { var listArray:[MPPositive_ArtistContentListViewModel] = [] ///遍历解析模块内容 contents.forEach { content in var title:String? var itemViews:[MPPositive_BrowseItemViewModel] = [] var id:String? var params:String? if let musicShelfRenderer = content.musicShelfRenderer { //这是首选模块 title = musicShelfRenderer.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) //获取模块的事件值 id = musicShelfRenderer.title?.runs?.first?.navigationEndpoint?.browseEndpoint?.browseId params = musicShelfRenderer.title?.runs?.first?.navigationEndpoint?.browseEndpoint?.params //解析模块内容 musicShelfRenderer.contents?.forEach({ item in if let musicResponsiveListItemRenderer = item.musicResponsiveListItemRenderer, let asd = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { itemViews.append(.init(asd)) } }) } if let musicCarouselShelfRenderer = content.musicCarouselShelfRenderer { //这是次选模块 title = musicCarouselShelfRenderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) id = musicCarouselShelfRenderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.first?.navigationEndpoint?.browseEndpoint?.browseId params = musicCarouselShelfRenderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.first?.navigationEndpoint?.browseEndpoint?.params //解析模块内容 musicCarouselShelfRenderer.contents?.forEach({ item in if item.musicTwoRowItemRenderer != nil { itemViews.append(.init(parsingMusicTwoRowItemRenderer(item.musicTwoRowItemRenderer!))) } }) } let list = MPPositive_ArtistContentListViewModel(title, itemViews: itemViews, browseId: id, params: params) listArray.append(list) } return listArray } ///解析艺术家更多_More private func parsingArtistMore(_ content:JsonArtistMore.Contents.SingleColumnBrowseResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content) -> ([MPPositive_BrowseItemViewModel], String?, String?) { //返回一组预览实体和一对继续编码 var continuation:String? var clickTrackingParams:String? var array:[MPPositive_BrowseItemViewModel] = [] //判断返回内容是音视频还是专辑列表 if let musicPlaylistShelfRenderer = content.musicPlaylistShelfRenderer { //是音视频 musicPlaylistShelfRenderer.contents?.forEach({ item in if let musicResponsiveListItemRenderer = item.musicResponsiveListItemRenderer { if let asd = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { array.append(.init(asd)) } } }) continuation = musicPlaylistShelfRenderer.continuations?.first?.nextContinuationData?.continuation clickTrackingParams = musicPlaylistShelfRenderer.continuations?.first?.nextContinuationData?.clickTrackingParams } if let gridRenderer = content.gridRenderer { //是专辑列表 gridRenderer.items?.forEach({ item in if let musicTwoRowItemRenderer = item.musicTwoRowItemRenderer { array.append(.init(parsingMusicTwoRowItemRenderer(musicTwoRowItemRenderer))) } continuation = gridRenderer.continuations?.first?.nextContinuationData?.continuation clickTrackingParams = gridRenderer.continuations?.first?.nextContinuationData?.clickTrackingParams }) } return (array, continuation, clickTrackingParams) } ///解析艺术家更多继续_Continuation private func parsingArtistMoreContinuation(_ musicPlaylistShelfContinuation: JsonArtistMore.ContinuationContents.MusicPlaylistShelfContinuation) -> ([MPPositive_BrowseItemViewModel], String?, String?) { //返回一组预览实体和一对继续编码 var continuation:String? var clickTrackingParams:String? var array:[MPPositive_BrowseItemViewModel] = [] //判断返回内容是音视频还是专辑列表 if let contents = musicPlaylistShelfContinuation.contents { contents.forEach { item in if let musicResponsiveListItemRenderer = item.musicResponsiveListItemRenderer { if let asd = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { array.append(.init(asd)) } } } } continuation = musicPlaylistShelfContinuation.continuations?.first?.nextContinuationData?.continuation clickTrackingParams = musicPlaylistShelfContinuation.continuations?.first?.nextContinuationData?.clickTrackingParams return (array,continuation,clickTrackingParams) } ///解析相关内容_Next_中的接下来播放列表 private func parsingNextList(_ next:JsonNext) -> [MPPositive_SongItemModel] { var array:[MPPositive_SongItemModel] = [] if let tabs = next.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs { if let tab = tabs.first { //获取一张播放列表 for (index, content) in (tab.tabRenderer?.content?.musicQueueRenderer?.content?.playlistPanelRenderer?.contents ?? []).enumerated() { if let playlistPanelVideoRenderer = content.playlistPanelVideoRenderer { //生成一个音乐实体,用来装填部分数据 let song = MPPositive_SongItemModel() song.index = index song.title = playlistPanelVideoRenderer.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) song.longBylineText = playlistPanelVideoRenderer.longBylineText?.runs?.reduce("", { $0 + ($1.text ?? "")}) song.lengthText = playlistPanelVideoRenderer.lengthText?.runs?.reduce("", { $0 + ($1.text ?? "")}) song.shortBylineText = playlistPanelVideoRenderer.shortBylineText?.runs?.reduce("", { $0 + ($1.text ?? "")}) song.reviewUrls = playlistPanelVideoRenderer.thumbnail?.thumbnails?.map({$0.url ?? ""}) song.videoId = playlistPanelVideoRenderer.videoId array.append(song) } } } } return array } /// 解析相关内容_Next_中的歌词ID和相关内容ID(两个ID都适用于用brwose接口) /// - Parameter next: 需要解析的next组 /// - Returns: 0位是歌词ID,1位相关内容ID private func parsingNextLyricsAndRelated(_ next:JsonNext) -> (String?,String?){ if let tabs = next.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs { if tabs.count == 3 { //歌词ID let lyrcisId = tabs[1].tabRenderer?.endpoint?.browseEndpoint?.browseId //相关内容ID let relatedID = tabs[2].tabRenderer?.endpoint?.browseEndpoint?.browseId return (lyrcisId, relatedID) }else if tabs.count == 2 { //歌词ID let lyrcisId = tabs[1].tabRenderer?.endpoint?.browseEndpoint?.browseId return (lyrcisId, nil) }else { return (nil,nil) } }else { return (nil,nil) } } ///解析模块数据_Genres private func parsingGenres(_ contents:[JsonGenres.Contents.SingleColumnBrowseResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content]) -> [MPPositive_GridViewModel] { var array:[MPPositive_GridViewModel] = [] contents.forEach { content in if let items = content.gridRenderer?.items { items.forEach { item in if let text = item.musicNavigationButtonRenderer?.buttonText?.runs?.first?.text, let leftStripeColor = item.musicNavigationButtonRenderer?.solid?.leftStripeColor, let browseId = item.musicNavigationButtonRenderer?.clickCommand?.browseEndpoint?.browseId, let params = item.musicNavigationButtonRenderer?.clickCommand?.browseEndpoint?.params { let gride = MPPositive_GridModel(title: text, stringColor: leftStripeColor, browseId: browseId, params: params) array.append(.init(gride)) } } } } return array } ///解析模块详情数据_Mood private func parsingMoodDetails(_ content:JsonBrowses.Contents.SingleColumnBrowseResultsRenderer.Tab.TabRenderer.Content) -> [MPPositive_BrowseModuleListViewModel] { //获取预览结构体中需要的数据(模块标题;歌曲信息:封面路径,音乐标题,歌手姓名,列表id和视频id) var browses:[MPPositive_BrowseModuleListViewModel] = [] //循环获取音乐内容 content.sectionListRenderer?.contents?.forEach({ content in //该循环获取预览模块内容,生成一个预览模型接收数据 let browse = MPPositive_BrowseModuleListViewModel() if let musicCarouselShelfRenderer = content.musicCarouselShelfRenderer { browse.title = musicCarouselShelfRenderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.first?.text //循环音乐内容组 musicCarouselShelfRenderer.contents?.forEach({ content in //设置标题、歌手、专辑/列表 if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { if let item = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { browse.items.append(.init(item)) } }else if let musicTwoRowItemRenderer = content.musicTwoRowItemRenderer { browse.items.append(.init(parsingMusicTwoRowItemRenderer(musicTwoRowItemRenderer))) } }) if browse.items.isEmpty != true { browses.append(browse) } }else if let gridRenderer = content.gridRenderer { browse.title = gridRenderer.header?.gridHeaderRenderer?.title?.runs?.first?.text gridRenderer.items?.forEach({ content in //设置标题、歌手、专辑/列表 if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { if let item = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { browse.items.append(.init(item)) } }else if let musicTwoRowItemRenderer = content.musicTwoRowItemRenderer { browse.items.append(.init(parsingMusicTwoRowItemRenderer(musicTwoRowItemRenderer))) } }) if browse.items.isEmpty != true { browses.append(browse) } } }) return browses } /// 解析播放器_Player /// - Parameters: /// - player: player库 /// - completion: 传递两个字符串数组,第一个资源路径组,第二个是封面路径组 // private func parsingPlayer(_ player:JsonPlayer, completion:@escaping((([String],[Float],[String]), [String]?) -> Void)){ // var infos:[String]? // //解析player,获取资源库和信息库 // if let videoDetails = player.videoDetails { // infos = parsingPlayerVideoDetails(videoDetails) // } // if let streamingData = player.streamingData { // parsingPlayerStreamingData(streamingData){ videos,floats,approxDurationMs in // completion((videos,floats,approxDurationMs),infos) // } // } // } private func parsingAndroidPlayer(_ player:JsonAndroidPlayer,completion:@escaping((([String],[Int],[String]), [String]?) -> Void)){ var infos:[String]? //解析player,获取资源库和信息库 if let videoDetails = player.videoDetails { infos = parsingAndroidPlayerVideoDetails(videoDetails) } if let streamingData = player.streamingData { parsingAndroidPlayerStreamingData(streamingData){ videos,itags,mimeType in completion((videos,itags,mimeType),infos) } } } /// 解析播放器_StreamingData /// - Parameters: /// - streamingData: 资源库 /// - completion: 第一位是音频资源,第二位是视频资源 private func parsingAndroidPlayerStreamingData(_ streamingData:JsonAndroidPlayer.StreamingData, completion:@escaping(([String],[Int],[String]) -> Void)) { var videos:[String] = [] var itags:[Int] = [] var mimeType:[String] = [] var allFormats = (streamingData.formats ?? []) + (streamingData.adaptiveFormats ?? []) //判断第一个itag值是否大于100,当第一条itag大于100,那么第一条资源将不可用,将itag140移动到首位 if let first = allFormats.first, (first.itag ?? 0) >= 100 { //第一条资源不可用,将音轨itag140资源移动到第一条 if let index = allFormats.firstIndex(where: {$0.itag == 140 || $0.itag == 251}) { let itag = allFormats.remove(at: index) allFormats.insert(itag, at: 0) } } for format in allFormats { videos.append(format.url ?? "") itags.append(format.itag ?? 0) mimeType.append(format.mimeType ?? "") } completion(videos,itags,mimeType) } // private func parsingPlayerStreamingData(_ streamingData:JsonPlayer.StreamingData, completion:@escaping(([String],[Float],[String]) -> Void)) { // var group:DispatchGroup? = DispatchGroup() // var videos:[String] = [] // var floats:[Float] = [] // var approxDurationMs:[String] = [] // let allFormats = (streamingData.formats ?? []) + (streamingData.adaptiveFormats ?? []) // for format in allFormats { // if let signatureCipher = format.signatureCipher { // // 进入DispatchGroup,表示开始一个异步任务 // group?.enter() // //获得资源签名,开始解密签名内容 // parsingPlayerSignatureCipher(signatureCipher) { result in // //这是条视频资源 // videos.append(result) // floats.append(format.bitrate ?? 0) // approxDurationMs.append(format.approxDurationMs ?? "") // // 离开DispatchGroup,表示异步任务完成 // group?.leave() // } // } // } // group?.notify(queue: .main) { // completion(videos, floats, approxDurationMs) // group = nil // } // } ///解析加密签名_SignatureCipher private func parsingPlayerSignatureCipher(_ signatureCipher:String, completion:@escaping((String) -> Void)) { // print("Resources-SignatureCipher:\(signatureCipher)") //该加密资源有两段式加密,先进行百分比加密解码 let originalURLString = seperatorOff(String(signatureCipher)) //第一段加密资源为权限资源 guard let sRange = originalURLString.range(of: "s=") else { return } guard let spSigRange = originalURLString.range(of: "&sp=sig", range: sRange.upperBound.. [MPPositive_RecommendListViewModel] { var sectionLists:[MPPositive_RecommendListViewModel] = [] contents.forEach { section in let list = MPPositive_RecommendListViewModel() list.title = section.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) section.musicCarouselShelfRenderer?.contents?.forEach({ content in if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer{ if let item = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { list.items.append(.init(item)) } }else if content.musicTwoRowItemRenderer != nil { let item = parsingMusicTwoRowItemRenderer(content.musicTwoRowItemRenderer!) list.items.append(.init(item)) } }) sectionLists.append(list) } return sectionLists } /// 解析搜索建议_SearchSuggestions /// - Parameters: /// - searchSuggestions: 需要解析搜索建议 /// - completion: 回掉两组搜索建议组 private func parsingSearchSuggestions(_ searchSuggestions:JsonSearchSuggestions, completion:@escaping([[MPPositive_SearchSuggestionItemModel]]) -> Void) { if let contents = searchSuggestions.contents { var sections:[[MPPositive_SearchSuggestionItemModel]] = [] contents.forEach { section in var suggestions:[MPPositive_SearchSuggestionItemModel] = [] section.searchSuggestionsSectionRenderer?.contents?.forEach({ content in //生成搜索建议模型 let item = MPPositive_SearchSuggestionItemModel() if let searchSuggestionRenderer = content.searchSuggestionRenderer { //这里是搜索词条建议 item.title = searchSuggestionRenderer.suggestion?.runs?.reduce("", { $0 + ($1.text ?? "")}) }else if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer { var reviewUrls:[String] = [] //这里是搜索音乐建议 musicResponsiveListItemRenderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.forEach({ thumbnail in reviewUrls.append(thumbnail.url ?? "") }) item.reviewUrls = reviewUrls if let flexColumns = musicResponsiveListItemRenderer.flexColumns { for (index,flexColumn) in flexColumns.enumerated() { if index == 0 { //主标题 item.title = flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) }else { //副标题 item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "") } } } } suggestions.append(item) }) sections.append(suggestions) } completion(sections) } } ///解析搜索预览结果_SearchPreviewResults private func parsingSearchPreviewResults(_ contents:[JsonSearchPreviewResults.Contents.TabbedSearchResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content]) -> [MPPositive_SearchResultListViewModel]{ var resultListSections:[MPPositive_SearchResultListViewModel] = [] contents.forEach { content in let resultList = MPPositive_SearchResultListViewModel() //判断当前模块是最佳结果还是其它模块 if let musicCardShelfRenderer = content.musicCardShelfRenderer { //当前是最佳结果 //设置模块标题 resultList.title = musicCardShelfRenderer.header?.musicCardShelfHeaderBasicRenderer?.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) //生成第一个内容块(根据内容不同,可能是单曲/歌手/专辑) let item = MPPositive_BrowseItemModel() //设置第一个内容块的预览图 item.coverUrls = musicCardShelfRenderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.map({$0.url ?? ""}) item.playListId = musicCardShelfRenderer.menu?.menuRenderer?.items?.first?.menuNavigationItemRenderer?.navigationEndpoint?.watchEndpoint?.playlistId //设置第一个内容块的一级标题和类型和ID if let title = musicCardShelfRenderer.title { title.runs?.forEach({ run in item.title = run.text if run.navigationEndpoint?.watchEndpoint != nil { //这个内容块是单曲/视频 item.videoId = run.navigationEndpoint?.watchEndpoint?.videoId item.itemType = .single }else { //这个内容块是艺术家/列表/专辑 item.browseId = run.navigationEndpoint?.browseEndpoint?.browseId let pageType = run.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType if youTubeKeys.contains(pageType ?? "") == true { //判断细分 if pageType == "MUSIC_PAGE_TYPE_ARTIST" { item.artistId = run.navigationEndpoint?.browseEndpoint?.browseId //是艺术家 item.itemType = .artist }else { //是列表/专辑 item.itemType = .list } } } }) } //设置第一个内容块的二级标题 if let subtitle = musicCardShelfRenderer.subtitle { item.subtitle = subtitle.runs?.reduce("", { $0 + ($1.text ?? "")}) } //第一个内容块填充完毕 if item.itemType != nil { resultList.previewItemViews.append(.init(item)) } //设置最佳结果的其他内容块 if let contents = musicCardShelfRenderer.contents { contents.forEach { content in //生成更多内容块 let item = MPPositive_BrowseItemModel() if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer, (musicResponsiveListItemRenderer.playlistItemData != nil || musicResponsiveListItemRenderer.navigationEndpoint != nil) && (musicResponsiveListItemRenderer.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType != "MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE"){ //设置预览图 item.coverUrls = musicResponsiveListItemRenderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.map({$0.url ?? ""}) //设置一级标题与二级标题 for (index, flexColumn) in (musicResponsiveListItemRenderer.flexColumns ?? []).enumerated() { if index == 0 { //一级标题 item.title = flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) }else { //二级标题 item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "") } } item.playListId = musicResponsiveListItemRenderer.menu?.menuRenderer?.items?.first?.menuNavigationItemRenderer?.navigationEndpoint?.watchEndpoint?.playlistId //设置id和类型 if musicResponsiveListItemRenderer.playlistItemData != nil { //是单曲 item.itemType = .single item.videoId = musicResponsiveListItemRenderer.playlistItemData?.videoId }else { //是专辑列表/艺术家 item.browseId = musicResponsiveListItemRenderer.navigationEndpoint?.browseEndpoint?.browseId let pageType = musicResponsiveListItemRenderer.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType if youTubeKeys.contains(pageType ?? "") == true && pageType != "MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE" { //判断细分 if pageType == "MUSIC_PAGE_TYPE_ARTIST" { item.artistId = musicResponsiveListItemRenderer.navigationEndpoint?.browseEndpoint?.browseId //是艺术家 item.itemType = .artist }else { //是列表/专辑 item.itemType = .list } } } } if item.title != nil && item.itemType != nil, item.pageType != "MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE" { resultList.previewItemViews.append(.init(item)) } } } //由于是最佳结果模块,所以没有query和params值 resultList.query = nil resultList.params = nil }else if let musicShelfRenderer = content.musicShelfRenderer { //当前是其他结果,不存在首选内容块 //设置模块标题 resultList.title = musicShelfRenderer.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) //设置内容块 musicShelfRenderer.contents?.forEach({ content in //专辑/列表/单曲视频/艺术家 if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer, let item = parsingMusicResponsiveListItemRenderer(musicResponsiveListItemRenderer) { resultList.previewItemViews.append(.init(item)) } }) //设置query和params值 if let searchEndpoint = musicShelfRenderer.bottomEndpoint?.searchEndpoint { resultList.query = searchEndpoint.query resultList.params = searchEndpoint.params } } resultListSections.append(resultList) } resultListSections.forEach { list in list.previewItemViews = list.previewItemViews.filter({ item in return item.item.pageType != "MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE" }) } resultListSections = resultListSections.filter({$0.previewItemViews.count != 0}) return resultListSections } ///解析搜索分页结果 private func parsingSearchTypeResults(_ musicShelfRenderer:JsonSearchTypeResults.Contents.TabbedSearchResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content.MusicShelfRenderer) -> ([MPPositive_SearchResultItemViewModel], String?, String?) { var array:[MPPositive_SearchResultItemViewModel] = [] if let contents = musicShelfRenderer.contents { ///解析内容块 contents.forEach({ content in //专辑/列表/单曲视频/艺术家 let item = MPPositive_BrowseItemModel() if (content.musicResponsiveListItemRenderer?.playlistItemData != nil || content.musicResponsiveListItemRenderer?.navigationEndpoint != nil) && (content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType != "MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE") { //设置内容块的封面 item.coverUrls = content.musicResponsiveListItemRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.map({$0.url ?? ""}) //设置一级标题与二级标题 for (index, flexColumn) in (content.musicResponsiveListItemRenderer?.flexColumns ?? []).enumerated() { if index == 0 { //一级标题 item.title = flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) }else { //二级标题 item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "") } } item.playListId = content.musicResponsiveListItemRenderer?.menu?.menuRenderer?.items?.first?.menuNavigationItemRenderer?.navigationEndpoint?.watchEndpoint?.playlistId //设置id和类型 if content.musicResponsiveListItemRenderer?.playlistItemData != nil { //是单曲 item.itemType = .single item.videoId = content.musicResponsiveListItemRenderer?.playlistItemData?.videoId }else { //是专辑列表/艺术家 item.browseId = content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId let pageType = content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType if youTubeKeys.contains(pageType ?? "") == true && pageType != "MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE" { //判断细分 if pageType == "MUSIC_PAGE_TYPE_ARTIST" { item.artistId = content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId //是艺术家 item.itemType = .artist }else { //是列表/专辑 item.itemType = .list } } } //添加内容块 if item.itemType != nil { array.append(.init(item)) } } }) } var continuation:String? var clickTrackingParams:String? if let continuations = musicShelfRenderer.continuations?.first { ///解析继续编码 continuation = continuations.nextContinuationData?.continuation clickTrackingParams = continuations.nextContinuationData?.clickTrackingParams } array = array.filter({$0.item.itemType != nil}) return (array, continuation, clickTrackingParams) } ///解析搜索分页继续 private func parsingSearchTypeContinuation(_ musicShelfContinuation:JsonSearchTypeContinuation.ContinuationContents.MusicShelfContinuation) -> ([MPPositive_SearchResultItemViewModel], String?, String?) { var array:[MPPositive_SearchResultItemViewModel] = [] if let contents = musicShelfContinuation.contents { ///解析内容块 contents.forEach({ content in //专辑/列表/单曲视频/艺术家 let item = MPPositive_BrowseItemModel() //设置内容块的封面 item.coverUrls = content.musicResponsiveListItemRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.map({$0.url ?? ""}) //设置一级标题与二级标题 for (index, flexColumn) in (content.musicResponsiveListItemRenderer?.flexColumns ?? []).enumerated() { if index == 0 { //一级标题 item.title = flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) }else { //二级标题 item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "") } } item.playListId = content.musicResponsiveListItemRenderer?.menu?.menuRenderer?.items?.first?.menuNavigationItemRenderer?.navigationEndpoint?.watchEndpoint?.playlistId //设置id和类型 if content.musicResponsiveListItemRenderer?.playlistItemData != nil { //是单曲 item.itemType = .single item.videoId = content.musicResponsiveListItemRenderer?.playlistItemData?.videoId }else { //是专辑列表/艺术家 item.browseId = content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId let pageType = content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType if youTubeKeys.contains(pageType ?? "") == true && pageType != "MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE" { //判断细分 if pageType == "MUSIC_PAGE_TYPE_ARTIST" { item.artistId = content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId //是艺术家 item.itemType = .artist }else { //是列表/专辑 item.itemType = .list } } } //添加内容块 if item.itemType != nil { array.append(.init(item)) } }) } var continuation:String? var clickTrackingParams:String? if let continuations = musicShelfContinuation.continuations?.first { ///解析继续编码 continuation = continuations.nextContinuationData?.continuation clickTrackingParams = continuations.nextContinuationData?.clickTrackingParams } array = array.filter({$0.item.itemType != nil}) return (array, continuation, clickTrackingParams) } //MARK: - 解析具体内容形式 //解析musicResponsiveListItemRenderer(四组件内容) private func parsingMusicResponsiveListItemRenderer(_ musicResponsiveListItemRenderer: RootMusicResponsiveListItemRenderer) -> MPPositive_BrowseItemModel? { //生成一个音乐模型接收数据 let item = MPPositive_BrowseItemModel() //预览图片路径 item.coverUrls = musicResponsiveListItemRenderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.compactMap({$0.url}) //判断预览项性质,并继续补完数据 if let playlistItemData = musicResponsiveListItemRenderer.playlistItemData { //当前预览项是视频/单曲 item.itemType = .single item.videoId = playlistItemData.videoId //设置主副标题 guard var flexColumns = musicResponsiveListItemRenderer.flexColumns, flexColumns.isEmpty != true else { return nil } //主标题为第一位,同时VideoId,PlayListId,pageType都以第一个flexColumn为主 let first = flexColumns.removeFirst() item.title = first.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) //判断watchEndpoint(资源导向)是否存在 guard let watchEndpoint = first.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.first?.navigationEndpoint?.watchEndpoint else {return nil} //watchEndpoint内容赋值给item item.playListId = watchEndpoint.playlistId item.pageType = watchEndpoint.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType //设置副标题,通常为作者/专辑 flexColumns.forEach { flexColumn in item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "") } //继续补完videoId,检索是否具备playListId,当playListId为空时,说明并没有成功补完,需要使用菜单结构补全内容 guard (item.playListId ?? "").isEmpty == true else { //不为空,说明需要的数据已经补完了 return item } //未补完,继续 if let watchItem = musicResponsiveListItemRenderer.menu?.menuRenderer?.items?.first(where: {$0.menuNavigationItemRenderer?.navigationEndpoint?.watchEndpoint != nil}) { item.playListId = watchItem.menuNavigationItemRenderer?.navigationEndpoint?.watchEndpoint?.playlistId return item }else { //补完失败 return nil } }else if let navigationEndpoint = musicResponsiveListItemRenderer.navigationEndpoint { //是专辑/艺术家/歌单 //设置主副标题 guard var flexColumns = musicResponsiveListItemRenderer.flexColumns, flexColumns.isEmpty != true else { return nil } //主标题为第一位,其余内容都是副标题 let first = flexColumns.removeFirst() item.title = first.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) //设置副标题,通常为作者/专辑 flexColumns.forEach { flexColumn in item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "") } //设置预览Id guard let browseEndpoint = navigationEndpoint.browseEndpoint else { return nil } item.browseId = browseEndpoint.browseId //根据pageType确定导航至何处 guard let pageType = browseEndpoint.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType else {return nil} item.pageType = pageType if pageType == "MUSIC_PAGE_TYPE_ALBUM" { //专辑 item.itemType = .list return item }else if pageType == "MUSIC_PAGE_TYPE_PLAYLIST" { //列表 item.itemType = .list return item }else if pageType == "MUSIC_PAGE_TYPE_ARTIST" { //艺术家 item.itemType = .artist return item }else { return nil } }else { return nil } } //解析musicResponsiveListItemRenderer(三组件结构) private func parsingMusicTwoRowItemRenderer(_ musicTwoRowItemRenderer: RootMusicTwoRowItemRenderer) -> MPPositive_BrowseItemModel { //生成一个音乐模型接收数据 let item = MPPositive_BrowseItemModel() //封面路径 item.coverUrls = musicTwoRowItemRenderer.thumbnailRenderer?.musicThumbnailRenderer?.thumbnail?.thumbnails?.compactMap({$0.url}) //标题 item.title = musicTwoRowItemRenderer.title?.runs?.reduce("", { $0 + ($1.text ?? "")}) //副标题 item.subtitle = musicTwoRowItemRenderer.subtitle?.runs?.reduce("", { $0 + ($1.text ?? "")}) //判断这个内容是音视频还是专辑列表艺术家 if let watchEndpoint = musicTwoRowItemRenderer.navigationEndpoint?.watchEndpoint { //是音视频 item.itemType = .single item.videoId = watchEndpoint.videoId item.playListId = watchEndpoint.playlistId item.pageType = watchEndpoint.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType } if let browseEndpoint = musicTwoRowItemRenderer.navigationEndpoint?.browseEndpoint { //是列表专辑艺术家 if browseEndpoint.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == "MUSIC_PAGE_TYPE_ARTIST" { //是艺术家 item.itemType = .artist item.artistId = browseEndpoint.browseId item.pageType = "MUSIC_PAGE_TYPE_ARTIST" }else { //是专辑列表 item.itemType = .list item.pageType = browseEndpoint.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType item.browseId = browseEndpoint.browseId } } return item } } //MARK: - 解析资源加密 extension MP_NetWorkManager { ///百分比解码 private func seperatorOff(_ encodedString: String) -> String { guard let decodedString = encodedString.removingPercentEncoding else { print("百分比解码失败") return "" } return decodedString } } //MARK: - 自动重试 ///重试策略类 class MP_CustomRetrier: RequestInterceptor { // 追踪重试次数的字典 private var retryCounts: [String: Int] = [:] // 最大重试次数 private let maxRetryCount: Int = 2 // 重试间隔时间 private let retryInterval: TimeInterval = 1.0 //重试策略 func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping (RetryResult) -> Void) { // 检查请求是否具有URL以便于在字典中进行追踪 guard let url = request.request?.url?.absoluteString else { completion(.doNotRetry) return } // 获取这个 URL 当前的重试次数 let currentRetryCount = retryCounts[url] ?? 0 // 如果未超过最大重试次数,那么决定重试 if currentRetryCount < maxRetryCount { // 更新重试次数 retryCounts[url] = currentRetryCount + 1 print("进行对\(url)访问的第\(currentRetryCount)次重试") // 告诉 Alamofire 重试并包含间隔时间 completion(.retryWithDelay(retryInterval)) } else { // 如果达到最大重试次数,则不再重试 completion(.doNotRetry) print("访问\(url)失败,不在进行重试") // // 移除追踪记录 // retryCounts[url] = nil } } }