210 lines
9.1 KiB
Swift
210 lines
9.1 KiB
Swift
//
|
||
// MP_WebWork.swift
|
||
// MusicPlayer
|
||
//
|
||
// Created by Mr.Zhou on 2024/4/30.
|
||
//
|
||
|
||
import Foundation
|
||
import WebKit
|
||
import Alamofire
|
||
///WebView管理器,通过拉取base.js文件来获取解密函数,完成对请求权限的解密
|
||
class MP_WebWork:NSObject {
|
||
//单例工具
|
||
static let shared = MP_WebWork()
|
||
///加载web(调用之后记得销毁)
|
||
private var webView:WKWebView?
|
||
///油管首页
|
||
private lazy var homePath = "https://music.youtube.com/"
|
||
private var jsPath:String = "https://music.youtube.com/"
|
||
///解码方法名
|
||
private var codeFunctionName:String!
|
||
///签名时间缀
|
||
var signatureTimestamp:Int!
|
||
private override init() {
|
||
super.init()
|
||
//实例化webView
|
||
webView = .init(frame: .zero)
|
||
webView?.navigationDelegate = self
|
||
webView?.uiDelegate = self
|
||
// webView?.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||
}
|
||
deinit {
|
||
webView = nil
|
||
}
|
||
/// 访问youtube主页
|
||
func pingYoutubeHome() {
|
||
//实现一个请求
|
||
let request = URLRequest(url: .init(string: homePath)!)
|
||
//加载web
|
||
webView?.load(request)
|
||
}
|
||
///获取base.js文件地址
|
||
private func getBasePath(_ jsContent:String) {
|
||
let pattern = "s/player/[0-9a-fA-F]{8}/player_ias.vflset/[a-zA-Z_]+/base\\.js"
|
||
guard let regex = try? NSRegularExpression(pattern: pattern, options: .dotMatchesLineSeparators) else {
|
||
print("Regular expression compilation failed")
|
||
return
|
||
}
|
||
// 在整个字符串范围内搜索正则表达式匹配项
|
||
let nsRange = NSRange(jsContent.startIndex..<jsContent.endIndex, in: jsContent)
|
||
let matches = regex.matches(in: jsContent, options: [], range: nsRange)
|
||
// 检查是否找到匹配项
|
||
if let first = matches.first {
|
||
// 从匹配结果中提取整个匹配的字符串
|
||
if let swiftRange = Range(first.range, in: jsContent) {
|
||
let matchedString = String(jsContent[swiftRange])
|
||
//成功获得base.js地址,进行拼接更改
|
||
self.jsPath = self.jsPath + matchedString
|
||
print("Current base.JavaScript path:\(self.jsPath)")
|
||
//开始获取首页内容
|
||
MPPositive_BrowseLoadViewModel.shared.reloadBrowseLists()
|
||
//切换地址
|
||
if let baseUrl = URL(string: self.jsPath) {
|
||
let request = URLRequest(url: baseUrl)
|
||
webView?.load(request)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/// 调用JSweb并解密相关内容
|
||
/// - Parameter signatureString: 需要解密的权限字符串
|
||
func excuteJavaScript(_ signatureString:String, completion:@escaping((String) -> Void)){
|
||
//检索webView是否存在
|
||
guard webView != nil, codeFunctionName != nil else {
|
||
return
|
||
}
|
||
//当webView和解码方法名同时存在时
|
||
let function = "_yt_player.SNa('\(signatureString)');"
|
||
//调用解密方法
|
||
webView?.evaluateJavaScript(function){result,error in
|
||
if let error = error {
|
||
print("Code Error:\(error)")
|
||
}
|
||
if let result = result as? String {
|
||
//该方法返回结果是一个字符串
|
||
completion(result)
|
||
}
|
||
}
|
||
}
|
||
///获取方法名的正则表达
|
||
private func regexFunction(_ jsContent:String) -> String? {
|
||
var result:String?
|
||
//唯一性的方法字符串
|
||
let pattern = "\\w+=function\\(a\\)\\{[^}]*?a=a\\.split\\(\\\"\\\"\\);"
|
||
// 使用NSRegularExpression进行正则匹配
|
||
guard let regex = try? NSRegularExpression(pattern: pattern, options: .dotMatchesLineSeparators) else {
|
||
print("Regular expression compilation failed")
|
||
return result
|
||
}
|
||
// 执行正则匹配
|
||
let matches = regex.matches(in: jsContent, options: [], range: NSRange(location: 0, length: jsContent.utf16.count))
|
||
// 遍历匹配项并打印函数名
|
||
for match in matches {
|
||
if let range = Range(match.range(at: 0), in: jsContent) {
|
||
// 提取函数体字符串
|
||
let functionbody = String(jsContent[range])
|
||
//将函数体字符串进行截取(以=为基准)
|
||
let results = functionbody.split(separator: "=").map(String.init)
|
||
//第一个元素就是方法名
|
||
result = results[0]
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
///获取签名时间值
|
||
private func getSignatureTimestamp(_ jsContent:String) -> Int?{
|
||
let regexPattern = "signatureTimestamp:(\\d+)"
|
||
guard let regex = try? NSRegularExpression(pattern: regexPattern, options: .dotMatchesLineSeparators) else {
|
||
print("Regular expression compilation failed")
|
||
return nil
|
||
}
|
||
let range = NSRange(jsContent.startIndex..<jsContent.endIndex, in: jsContent)
|
||
let matches = regex.matches(in: jsContent, options: [], range: range)
|
||
if let first = matches.first {
|
||
if let matchRange = Range(first.range(at: 1), in: jsContent) {
|
||
let matchedNumber = String(jsContent[matchRange])
|
||
print(matchedNumber)
|
||
return Int(matchedNumber)
|
||
}
|
||
return nil
|
||
}
|
||
return nil
|
||
}
|
||
}
|
||
//MARK: - navigationDelegate
|
||
extension MP_WebWork: WKNavigationDelegate, WKUIDelegate {
|
||
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void){
|
||
guard let url = navigationResponse.response.url, url.lastPathComponent == "base.js", codeFunctionName == nil else {
|
||
decisionHandler(.allow) // 允许非JavaScript文件的加载
|
||
return
|
||
}
|
||
//调用的是base.js文件,拦截该请求
|
||
decisionHandler(.cancel)
|
||
// 使用URLSession重新请求base.js文件
|
||
let task = URLSession.shared.dataTask(with: url){ [weak self] data, response, error in
|
||
guard let self = self, let data = data, error == nil else {
|
||
print("Request base.JavaScript error: \(error?.localizedDescription ?? "")")
|
||
decisionHandler(.allow)
|
||
return
|
||
}
|
||
// 修改JavaScript文件内容
|
||
guard let jsContent = String(data: data, encoding: .utf8) else {
|
||
decisionHandler(.allow)
|
||
return
|
||
}
|
||
guard let function = regexFunction(jsContent), let timeValue = getSignatureTimestamp(jsContent) else {
|
||
decisionHandler(.allow)
|
||
return
|
||
}
|
||
//获取需要的方法名,和签名时间缀
|
||
signatureTimestamp = timeValue
|
||
codeFunctionName = function
|
||
//由于该解密函数方法是私有的,因此创建一句暴露代码
|
||
let code = "g.SNa=\(codeFunctionName!);"
|
||
let pattern = "\(codeFunctionName!)=function\\(a\\)\\{[^\\}]*\\};"
|
||
let regex = try? NSRegularExpression(pattern: pattern, options: .dotMatchesLineSeparators)
|
||
let nsRange = NSRange(jsContent.startIndex..<jsContent.endIndex, in: jsContent)
|
||
let matches = regex?.matches(in: jsContent, options: [], range: nsRange)
|
||
if let match = matches?.first {
|
||
let replacementString = "\(jsContent[Range(match.range, in: jsContent)!])\n\(code)"
|
||
let newJavaScript = jsContent.replacingOccurrences(of: jsContent[Range(match.range, in: jsContent)!], with: replacementString)
|
||
DispatchQueue.main.async {
|
||
// 在这里处理新的JavaScript代码,例如注入到webView
|
||
webView.evaluateJavaScript(newJavaScript) { result,error in
|
||
if let error = error {
|
||
print("注入代码失败:\(error)")
|
||
}else {
|
||
print("注入代码完成")
|
||
//发布切换启动页
|
||
NotificationCenter.notificationKey.post(notificationName: .js_edit_completion)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
task.resume()
|
||
}
|
||
|
||
|
||
|
||
// WKNavigationDelegate 方法,用于监听加载完成事件
|
||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||
//未加载,查找解码方法
|
||
webView.evaluateJavaScript("document.documentElement.outerHTML", completionHandler: { [weak self] (result, error) in
|
||
//成功抓取了js内容
|
||
if let content = result as? String {
|
||
if self?.jsPath != "https://music.youtube.com/" {
|
||
//当前base.js文件获取成功,获取解密方法名
|
||
}else {
|
||
//获取base.js地址
|
||
self?.getBasePath(content)
|
||
}
|
||
} else if let error = error {
|
||
print("Error getting JavaScript content: \(error.localizedDescription)")
|
||
}
|
||
})
|
||
}
|
||
|
||
}
|