Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 33 additions & 29 deletions Sources/YouTubeKit/Cipher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,28 @@ import JavaScriptCore
import os.log

@available(iOS 13.0, watchOS 6.0, tvOS 13.0, macOS 10.15, *)
/// Handles YouTube signature deciphering and throttling parameter calculation.
///
/// This class is responsible for:
/// 1. Extracting and decoding the signature transformation functions from YouTube's JavaScript
/// 2. Applying these transformations to decipher video signatures
/// 3. Calculating the 'n' parameter to prevent throttling
///
/// The implementation has been updated to dynamically detect the global variable used for
/// signature algorithms, based on the fix from YouTube.js PR #953, which improves compatibility
/// with YouTube's frequently changing obfuscation techniques.
class Cipher {

let js: String

private let transformPlan: [(func: JSFunction, param: Int)]
private let transformMap: [String: JSFunction]

private let jsFuncPatterns = [
private let jsFuncPatterns = [
NSRegularExpression(#"\w+\.(\w+)\(\w,(\d+)\)"#),
NSRegularExpression(#"\w+\[(\"\w+\")\]\(\w,(\d+)\)"#)
NSRegularExpression(#"\w+\[(\"\w+\")\]\(\w,(\d+)\)"#),
NSRegularExpression(#"function\(\w\)\{[\w=.]*;(.*);"#),
NSRegularExpression(#"\b([a-zA-Z0-9_$]+)\s*=\s*function\(\s*([a-zA-Z0-9_$]+)\s*\)"#)
]

private let nParameterFunction: String
Expand All @@ -30,52 +42,48 @@ class Cipher {

private static let log = OSLog(Cipher.self)

/// Initializes the Cipher with YouTube player JavaScript.
///
/// This implementation dynamically detects the global variable used for signature algorithms
/// based on the fix from YouTube.js PR #953. Instead of using a hardcoded variable name ("DE"),
/// it now attempts to extract the variable name from the JavaScript code, falling back to "DE"
/// only if extraction fails.
///
/// - Parameter js: The JavaScript content from YouTube player
/// - Throws: YouTubeKitError if parsing fails
init(js: String) throws {
self.js = js
// Extract the global variable used for signature algorithm
let globalVariable = Extraction.extractGlobalVariable(js: js) ?? "DE"
os_log("Using global variable: %{public}@", log: Cipher.log, type: .debug, globalVariable)

/*let rawTransformPlan = try Cipher.getRawTransformPlan(js: js)

let varRegex = NSRegularExpression(#"^\$*\w+\W"#)
guard let varMatch = varRegex.firstMatch(in: rawTransformPlan[0], group: 0) else {
throw YouTubeKitError.regexMatchError
}
var variable = varMatch.content
_ = variable.popLast()

self.transformMap = try Cipher.getTransformMap(js: js, variable: variable)
self.transformPlan = try Cipher.getDecodedTransformPlan(rawPlan: rawTransformPlan, variable: variable, transformMap: transformMap)*/
// -> temporarily disabled (as mostly unused)
self.transformMap = [:]
self.transformPlan = []

self.nParameterFunction = try Cipher.getThrottlingFunctionCode(js: js) //try Cipher.getNParameterFunction(js: js)
// Implement transformPlan and transformMap logic based on TypeScript fixes
let transformObject = try Cipher.getTransformObject(js: js, variable: globalVariable)
self.transformMap = try Cipher.getTransformMap(js: js, variable: globalVariable)
let rawPlan = try Cipher.getRawTransformPlan(js: js)
self.transformPlan = try Cipher.getDecodedTransformPlan(rawPlan: rawPlan, variable: globalVariable, transformMap: transformMap)
// Extract the n parameter function code (throttling)
self.nParameterFunction = try Cipher.getThrottlingFunctionCode(js: js)
}

/// Converts n to the correct value to prevent throttling.
func calculateN(initialN: String) throws -> String {
if let newN = calculatedN[initialN] {
return newN
}

#if canImport(JavaScriptCore)
guard let context = JSContext() else {
os_log("failed to create JSContext", log: Cipher.log, type: .error)
return ""
}

context.evaluateScript(nParameterFunction)

let function = context.objectForKeyedSubscript("processNSignature")
let result = function?.call(withArguments: [initialN])

guard let result, result.isString, let newN = result.toString() else {
os_log("failed to calculate n", log: Cipher.log, type: .error)
return ""
}

// cache the result
calculatedN[initialN] = newN

return newN
#else
return ""
Expand All @@ -85,12 +93,9 @@ class Cipher {
/// Decipher the signature
func getSignature(cipheredSignature: String) -> String? {
var signature = Array(cipheredSignature)

guard !transformPlan.isEmpty else {
return nil
}

// apply transform functions
for (function, param) in transformPlan {
switch function {
case .reverse:
Expand All @@ -101,7 +106,6 @@ class Cipher {
(signature[0], signature[param % signature.count]) = (signature[param % signature.count], signature[0])
}
}

return String(signature)
}

Expand Down
53 changes: 49 additions & 4 deletions Sources/YouTubeKit/Extraction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ class Extraction {
class func getYTPlayerConfig(html: String) throws -> PlayerConfig {
os_log("finding initial function name", log: log, type: .debug)
let configPatterns = [
NSRegularExpression(#"ytplayer\.config\s*=\s*"#),
NSRegularExpression(#"ytInitialPlayerResponse\s*=\s*"#)
// Outdated Regex
// NSRegularExpression(#"ytplayer\.config\s*=\s*(\{)"#),
NSRegularExpression(#"ytInitialPlayerResponse\s*=\s*(\{)"#)
]

for pattern in configPatterns {
Expand Down Expand Up @@ -112,17 +113,61 @@ class Extraction {
return (nil, [nil])
}

/// Extracts the signature timestamp (sts) from javascript.
/// Extracts the signature timestamp (sts) from javascript.
/// Used to pass into InnerTube to tell API what sig/player is in use.
/// - parameter js: The javascript contents of the watch page
/// - returns: The signature timestamp (sts) or nil if not found
class func extractSignatureTimestamp(fromJS js: String) -> Int? {
let pattern = NSRegularExpression(#"(?:signatureTimestamp|sts)\s*:\s*([0-9]{5})"#)
// Improved regex to match signatureTimestamp extraction as in TS
let pattern = NSRegularExpression(#"signatureTimestamp\s*:\s*(\d+)"#)
if let match = pattern.firstMatch(in: js, group: 1) {
return Int(match.content)
}
return nil
}

/// Extracts the global variable used for deciphering signatures from JS.
/// This implementation is based on the fix from YouTube.js PR #953 which uses a global variable
/// to find the signature algorithm instead of relying on hardcoded variable names.
/// YouTube.js PR #953 - https://github.com/LuanRT/YouTube.js/pull/953
///
/// - Parameter js: The JavaScript content from YouTube player
/// - Returns: The name of the global variable used for signature deciphering, or nil if not found
class func extractGlobalVariable(js: String) -> String? {
// Try to match common variable patterns as in YouTube.js
let patterns = [
// Match variable declarations that contain signature-related code
NSRegularExpression(#"var (\w+)=\{[^}]+\}"#),
// Match function declarations that handle signature splitting
NSRegularExpression(#"function\((\w+)\)\{\w+=\w+\.split\(""\)"#),
// Match variables that might contain signature transformation functions
NSRegularExpression(#"var (\w+)=\{[\s\S]*?\};"#)
]

for pattern in patterns {
if let match = pattern.firstMatch(in: js, group: 1) {
os_log("Found global variable for signature algorithm: %{public}@", log: log, type: .debug, match.content)
return match.content
}
}

// If no variable is found, the default "DE" will be used as fallback in Cipher.swift
os_log("Could not extract global variable for signature algorithm, using default", log: log, type: .debug)
return nil
}

/// Extracts the signature deciphering source code from JS.
class func extractSigSourceCode(js: String, globalVariable: String?) -> String? {
guard let globalVariable else { return nil }
// Try to extract the function body for signature deciphering
guard let pattern = try? NSRegularExpression(pattern: "function\\((\\w+)\\)\\{(\\w+=\\w+\\.split\\(\\\"\\\"\\)(.+?)\\.join\\(\\\"\\\"\\))\\}", options: []) else {
return nil
}
if let match = pattern.firstMatch(in: js, group: 2) {
return "function descramble_sig(sig) { var \(globalVariable) = {...}; \(match.content) } descramble_sig(sig);"
}
return nil
}

struct YtCfg: Decodable {
let VISITOR_DATA: String?
Expand Down
14 changes: 14 additions & 0 deletions Sources/YouTubeKit/YouTube.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ public class YouTube {

public let videoID: String

public static func extractStreams(forVideoID videoID: String, method: ExtractionMethod) async throws -> [Stream] {
let youtube = YouTube(videoID: videoID, methods: [method])
return try await youtube.streams
}

var watchURL: URL {
URL(string: "https://youtube.com/watch?v=\(videoID)")!
}
Expand Down Expand Up @@ -176,6 +181,15 @@ public class YouTube {
}
}

/// Retrieves the YouTube player JavaScript code.
///
/// This JavaScript contains the signature deciphering algorithms needed to generate valid video URLs.
/// The implementation now supports dynamic detection of the global variable used for signature algorithms
/// based on the fix from YouTube.js PR #953, which improves compatibility with YouTube's
/// frequently changing obfuscation techniques.
///
/// - Returns: The JavaScript content from YouTube player
/// - Throws: Error if JavaScript cannot be retrieved
var js: String {
get async throws {
if let cached = _js {
Expand Down