Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/multi client #32

Merged
merged 5 commits into from
Dec 13, 2023
Merged
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
55 changes: 49 additions & 6 deletions Sources/YouTubeKit/Cipher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Cipher {

let js: String

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

private let jsFuncPatterns = [
Expand All @@ -30,16 +30,18 @@ class Cipher {

init(js: String) throws {
self.js = js
self.transformPlan = try Cipher.getTransformPlan(js: js)

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

let varRegex = NSRegularExpression(#"^\$*\w+\W"#)
guard let varMatch = varRegex.firstMatch(in: transformPlan[0], group: 0) else {
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)

self.throttlingPlan = try Cipher.getThrottlingPlan(js: js)
self.throttlingArray = try Cipher.getThrottlingFunctionArray(js: js)
Expand Down Expand Up @@ -83,10 +85,24 @@ class Cipher {
}

/// Decipher the signature
func getSignature(cipheredSignature: String) -> String {
func getSignature(cipheredSignature: String) -> String? {
var signature = Array(cipheredSignature)

// TODO: apply transform functions
guard !transformPlan.isEmpty else {
return nil
}

// apply transform functions
for (function, param) in transformPlan {
switch function {
case .reverse:
signature.reverse()
case .splice:
signature = Array(signature.dropFirst(param))
case .swap:
(signature[0], signature[param % signature.count]) = (signature[param % signature.count], signature[0])
}
}

return String(signature)
}
Expand Down Expand Up @@ -133,7 +149,7 @@ class Cipher {

/// Extract the "transform plan".
/// The "transform plan" is the functions that the ciphered signature is cycled through to obtain the actual signature.
class func getTransformPlan(js: String) throws -> [String] {
class func getRawTransformPlan(js: String) throws -> [String] {
let name = try getInitialFunctionName(js: js)
let pattern = NSRegularExpression(NSRegularExpression.escapedPattern(for: name) + #"=function\(\w\)\{[a-z=\.\(\"\)]*;(.*);(?:.+)\}"#)
os_log("getting transform plan", log: log, type: .debug)
Expand All @@ -143,6 +159,33 @@ class Cipher {
throw YouTubeKitError.regexMatchError
}

/// Transforms raw transform plan in to a decoded transform plan with functions and parameters
/// - Note: returns empty array if transformation failed
class func getDecodedTransformPlan(rawPlan: [String], variable: String, transformMap: [String: JSFunction]) throws -> [(func: JSFunction, param: Int)] {
let pattern = try NSRegularExpression(pattern: NSRegularExpression.escapedPattern(for: variable) + #"\.(.+)\(.+,(\d+)\)"#) // expecting e.g. "wP.Nl(a,65)"

var result: [(func: JSFunction, param: Int)] = []

for functionCall in rawPlan {
guard let (_, matchGroups) = pattern.allMatches(in: functionCall, includingGroups: [1, 2]).first,
let functionName = matchGroups[1]?.content,
let parameter = matchGroups[2]?.content
else {
os_log("failed to decode function call %{public}@", log: log, type: .error, functionCall)
return []
}

guard let decodedParameter = Int(parameter) else { return [] }
guard let function = transformMap[functionName] else {
os_log("failed to find function %{public}@", log: log, type: .error, functionName)
return []
}

result.append((func: function, param: decodedParameter))
}
return result
}

/// Extract the "transform object".
/// The "transform object" contains the function definitions referenced in the transform plan". The ``variable`` argument is the obfuscated variable name
/// which contains these functions, for example, given the function call ``DE.AJ(a,15)`` returned by the transform plan, "DE" would be the var.
Expand Down
35 changes: 35 additions & 0 deletions Sources/YouTubeKit/Extensions/Concurrency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// Concurrency.swift
// YouTubeKit
//
// Created by Alexander Eichhorn on 13.12.23.
//

import Foundation

@available(iOS 13.0, watchOS 6.0, tvOS 13.0, macOS 10.15, *)
extension Sequence {

func asyncMap<T>(_ transform: (Element) async throws -> T) async rethrows -> [T] {
var values = [T]()

for element in self {
try await values.append(transform(element))
}

return values
}

func concurrentMap<T>(_ transform: @escaping (Element) async -> T) async -> [T] {
let tasks = map { element in
Task {
await transform(element)
}
}

return await tasks.asyncMap { task in
await task.value
}
}

}
17 changes: 17 additions & 0 deletions Sources/YouTubeKit/Extraction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ class Extraction {
throw YouTubeKitError.regexMatchError
}

/// Tries to find video info in watch html directly
class func getVideoInfo(fromHTML html: String) throws -> InnerTube.VideoInfo {
let pattern = NSRegularExpression(#"ytInitialPlayerResponse\s*=\s*"#)
return try parseForObject(InnerTube.VideoInfo.self, html: html, precedingRegex: pattern)
}

/// Return the playability status and status explanation of the video
/// For example, a video may have a status of LOGIN\_REQUIRED, and an explanation
/// of "This is a private video. Please sign in to verify that you may see it."
Expand Down Expand Up @@ -280,6 +286,8 @@ class Extraction {
class func applySignature(streamManifest: inout [InnerTube.StreamingData.Format], videoInfo: InnerTube.VideoInfo, js: String) throws {
var cipher = ThrowingLazy(try Cipher(js: js))

var invalidStreamIndices = [Int]()

for (i, stream) in streamManifest.enumerated() {
if let url = stream.url {
if url.contains("signature") || (stream.s == nil && (url.contains("&sig=") || url.contains("&lsig="))) {
Expand All @@ -288,6 +296,10 @@ class Extraction {
}

if let cipheredSignature = stream.s {
// Remove the stream from `streamManifest` for now, as signature extraction currently doesn't work most of time
invalidStreamIndices.append(i)
continue // Skip the rest of the code as we are removing this stream

let signature = try cipher.value.getSignature(cipheredSignature: cipheredSignature)

os_log("finished descrambling signature for itag=%{public}i", log: log, type: .debug, stream.itag)
Expand All @@ -311,6 +323,11 @@ class Extraction {
}
}
}

// Remove invalid streams
for index in invalidStreamIndices.reversed() {
streamManifest.remove(at: index)
}
}

/// Breaks up the data in the ``type`` key of the manifest, which contains the
Expand Down
Loading
Loading