add client module
This commit is contained in:
parent
7447d2394b
commit
b85b9893a8
7 changed files with 550 additions and 29 deletions
|
|
@ -8,7 +8,6 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
quick-js = "0.4.1"
|
quick-js = "0.4.1"
|
||||||
once_cell = "1.12.0"
|
once_cell = "1.12.0"
|
||||||
regex = "1.6.0"
|
|
||||||
fancy-regex = "0.10.0"
|
fancy-regex = "0.10.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
thiserror = "1.0.31"
|
thiserror = "1.0.31"
|
||||||
|
|
@ -16,3 +15,10 @@ url = "2.2.2"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
reqwest = "0.11.11"
|
reqwest = "0.11.11"
|
||||||
tokio = {version = "1.20.0", features = ["macros"]}
|
tokio = {version = "1.20.0", features = ["macros"]}
|
||||||
|
serde_json = "1.0.82"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.9.0"
|
||||||
|
test-log = "0.2.11"
|
||||||
|
|
|
||||||
86
notes/desktopMusic-request.json
Normal file
86
notes/desktopMusic-request.json
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
{
|
||||||
|
"videoId": "a1IuJLebHgM",
|
||||||
|
"context": {
|
||||||
|
"client": {
|
||||||
|
"hl": "de",
|
||||||
|
"gl": "DE",
|
||||||
|
"remoteHost": "2003:de:af47:2200:1f01:5e28:c1a1:55b9",
|
||||||
|
"deviceMake": "",
|
||||||
|
"deviceModel": "",
|
||||||
|
"visitorData": "CgtfZGdNenp4Z1JVRSjItfSWBg%3D%3D",
|
||||||
|
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36,gzip(gfe)",
|
||||||
|
"clientName": "WEB_REMIX",
|
||||||
|
"clientVersion": "1.20220715.04.00",
|
||||||
|
"osName": "X11",
|
||||||
|
"osVersion": "",
|
||||||
|
"originalUrl": "https://music.youtube.com/?cbrd=1",
|
||||||
|
"platform": "DESKTOP",
|
||||||
|
"clientFormFactor": "UNKNOWN_FORM_FACTOR",
|
||||||
|
"configInfo": {
|
||||||
|
"appInstallData": "CMi19JYGENSDrgUQxPb9EhCurK4FEP24_RIQlK-uBRC3y60FENCtrgUQ5vj9EhDLoq4FELiLrgUQpvP9EhDYvq0F"
|
||||||
|
},
|
||||||
|
"browserName": "Chrome",
|
||||||
|
"browserVersion": "103.0.5060.134",
|
||||||
|
"screenWidthPoints": 540,
|
||||||
|
"screenHeightPoints": 1003,
|
||||||
|
"screenPixelDensity": 1,
|
||||||
|
"screenDensityFloat": 1,
|
||||||
|
"utcOffsetMinutes": 120,
|
||||||
|
"userInterfaceTheme": "USER_INTERFACE_THEME_DARK",
|
||||||
|
"timeZone": "Europe/Berlin",
|
||||||
|
"playerType": "UNIPLAYER",
|
||||||
|
"tvAppInfo": { "livingRoomAppMode": "LIVING_ROOM_APP_MODE_UNSPECIFIED" },
|
||||||
|
"clientScreen": "WATCH_FULL_SCREEN"
|
||||||
|
},
|
||||||
|
"user": { "lockedSafetyMode": false },
|
||||||
|
"request": {
|
||||||
|
"useSsl": true,
|
||||||
|
"internalExperimentFlags": [],
|
||||||
|
"consistencyTokenJars": []
|
||||||
|
},
|
||||||
|
"clientScreenNonce": "MC41MjQzMjkwMzQ5NzQ3ODE4",
|
||||||
|
"adSignalsInfo": {
|
||||||
|
"params": [
|
||||||
|
{ "key": "dt", "value": "1658657482350" },
|
||||||
|
{ "key": "flash", "value": "0" },
|
||||||
|
{ "key": "frm", "value": "0" },
|
||||||
|
{ "key": "u_tz", "value": "120" },
|
||||||
|
{ "key": "u_his", "value": "3" },
|
||||||
|
{ "key": "u_h", "value": "1080" },
|
||||||
|
{ "key": "u_w", "value": "2560" },
|
||||||
|
{ "key": "u_ah", "value": "1080" },
|
||||||
|
{ "key": "u_aw", "value": "2560" },
|
||||||
|
{ "key": "u_cd", "value": "24" },
|
||||||
|
{ "key": "bc", "value": "31" },
|
||||||
|
{ "key": "bih", "value": "1003" },
|
||||||
|
{ "key": "biw", "value": "528" },
|
||||||
|
{
|
||||||
|
"key": "brdim",
|
||||||
|
"value": "1219,-39,1219,-39,2560,0,1336,1158,540,1003"
|
||||||
|
},
|
||||||
|
{ "key": "vis", "value": "1" },
|
||||||
|
{ "key": "wgl", "value": "true" },
|
||||||
|
{ "key": "ca_type", "value": "image" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"clickTracking": {
|
||||||
|
"clickTrackingParams": "CIwFEMjeAiITCKTV48-kkfkCFfrfEQgdbKQFeQ=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"playbackContext": {
|
||||||
|
"contentPlaybackContext": {
|
||||||
|
"html5Preference": "HTML5_PREF_WANTS",
|
||||||
|
"lactMilliseconds": "11",
|
||||||
|
"referer": "https://music.youtube.com/",
|
||||||
|
"signatureTimestamp": 19194,
|
||||||
|
"autoCaptionsDefaultOn": false,
|
||||||
|
"mdxContext": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cpn": "Hhji0Eo5WqdxXyj8",
|
||||||
|
"playlistId": "RDAMVMa1IuJLebHgM",
|
||||||
|
"captionParams": {},
|
||||||
|
"serviceIntegrityDimensions": {
|
||||||
|
"poToken": "GpsBCm41_Q8Gdg5ubTfFjg5iom_5EMgKAKggyajvdSFL8McTI6M-exELW1WeGmPtwON1sqmJItk-wHF1WobX8rZRx49vefmv5NprWecvSx3LPl8ESu-QQZOK9VI-q9R3OdcTpJWLkcY7eEKuyItFf6k0ZxIpAX04kIiBQUQPO8JcQv_U5siKZaug6kUSg9OqLmFc7rRClBhbWq7yF1U="
|
||||||
|
}
|
||||||
|
}
|
||||||
90
notes/desktopMusic_search.json
Normal file
90
notes/desktopMusic_search.json
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
{
|
||||||
|
"context": {
|
||||||
|
"client": {
|
||||||
|
"hl": "de",
|
||||||
|
"gl": "DE",
|
||||||
|
"remoteHost": "2003:de:af47:2200:1f01:5e28:c1a1:55b9",
|
||||||
|
"deviceMake": "",
|
||||||
|
"deviceModel": "",
|
||||||
|
"visitorData": "CgtfZGdNenp4Z1JVRSjItfSWBg%3D%3D",
|
||||||
|
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36,gzip(gfe)",
|
||||||
|
"clientName": "WEB_REMIX",
|
||||||
|
"clientVersion": "1.20220715.04.00",
|
||||||
|
"osName": "X11",
|
||||||
|
"osVersion": "",
|
||||||
|
"originalUrl": "https://music.youtube.com/?cbrd=1",
|
||||||
|
"platform": "DESKTOP",
|
||||||
|
"clientFormFactor": "UNKNOWN_FORM_FACTOR",
|
||||||
|
"configInfo": {
|
||||||
|
"appInstallData": "CMi19JYGENSDrgUQxPb9EhCurK4FEP24_RIQlK-uBRC3y60FENCtrgUQ5vj9EhDLoq4FELiLrgUQpvP9EhDYvq0F"
|
||||||
|
},
|
||||||
|
"browserName": "Chrome",
|
||||||
|
"browserVersion": "103.0.5060.134",
|
||||||
|
"screenWidthPoints": 759,
|
||||||
|
"screenHeightPoints": 1010,
|
||||||
|
"screenPixelDensity": 1,
|
||||||
|
"screenDensityFloat": 1,
|
||||||
|
"utcOffsetMinutes": 120,
|
||||||
|
"userInterfaceTheme": "USER_INTERFACE_THEME_DARK",
|
||||||
|
"timeZone": "Europe/Berlin",
|
||||||
|
"musicAppInfo": {
|
||||||
|
"pwaInstallabilityStatus": "PWA_INSTALLABILITY_STATUS_UNKNOWN",
|
||||||
|
"webDisplayMode": "WEB_DISPLAY_MODE_BROWSER",
|
||||||
|
"storeDigitalGoodsApiSupportStatus": {
|
||||||
|
"playStoreDigitalGoodsApiSupportStatus": "DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED"
|
||||||
|
},
|
||||||
|
"musicActivityMasterSwitch": "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE",
|
||||||
|
"musicLocationMasterSwitch": "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user": { "lockedSafetyMode": false },
|
||||||
|
"request": {
|
||||||
|
"useSsl": true,
|
||||||
|
"internalExperimentFlags": [],
|
||||||
|
"consistencyTokenJars": []
|
||||||
|
},
|
||||||
|
"adSignalsInfo": {
|
||||||
|
"params": [
|
||||||
|
{ "key": "dt", "value": "1658657481372" },
|
||||||
|
{ "key": "flash", "value": "0" },
|
||||||
|
{ "key": "frm", "value": "0" },
|
||||||
|
{ "key": "u_tz", "value": "120" },
|
||||||
|
{ "key": "u_his", "value": "4" },
|
||||||
|
{ "key": "u_h", "value": "1080" },
|
||||||
|
{ "key": "u_w", "value": "1920" },
|
||||||
|
{ "key": "u_ah", "value": "1080" },
|
||||||
|
{ "key": "u_aw", "value": "1920" },
|
||||||
|
{ "key": "u_cd", "value": "24" },
|
||||||
|
{ "key": "bc", "value": "31" },
|
||||||
|
{ "key": "bih", "value": "1010" },
|
||||||
|
{ "key": "biw", "value": "759" },
|
||||||
|
{ "key": "brdim", "value": "2560,0,2560,0,1920,0,1920,1080,759,1010" },
|
||||||
|
{ "key": "vis", "value": "1" },
|
||||||
|
{ "key": "wgl", "value": "true" },
|
||||||
|
{ "key": "ca_type", "value": "image" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"activePlayers": [{ "playerContextParams": "Q0FFU0FnZ0I=" }]
|
||||||
|
},
|
||||||
|
"query": "test",
|
||||||
|
"suggestStats": {
|
||||||
|
"validationStatus": "VALID",
|
||||||
|
"parameterValidationStatus": "VALID_PARAMETERS",
|
||||||
|
"clientName": "youtube-music",
|
||||||
|
"searchMethod": "ENTER_KEY",
|
||||||
|
"inputMethod": "KEYBOARD",
|
||||||
|
"originalQuery": "test",
|
||||||
|
"availableSuggestions": [
|
||||||
|
{ "index": 0, "suggestionType": 0 },
|
||||||
|
{ "index": 1, "suggestionType": 0 },
|
||||||
|
{ "index": 2, "suggestionType": 0 },
|
||||||
|
{ "index": 3, "suggestionType": 0 },
|
||||||
|
{ "index": 4, "suggestionType": 0 },
|
||||||
|
{ "index": 5, "suggestionType": 0 },
|
||||||
|
{ "index": 6, "suggestionType": 0 }
|
||||||
|
],
|
||||||
|
"zeroPrefixEnabled": true,
|
||||||
|
"firstEditTimeMsec": 240405,
|
||||||
|
"lastEditTimeMsec": 243276
|
||||||
|
}
|
||||||
|
}
|
||||||
303
src/client/mod.rs
Normal file
303
src/client/mod.rs
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
|
use fancy_regex::Regex;
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use rand::Rng;
|
||||||
|
use reqwest::{header, Client, ClientBuilder, Request};
|
||||||
|
use serde::Serialize;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
pub enum ClientType {
|
||||||
|
Desktop,
|
||||||
|
DesktopMusic,
|
||||||
|
TvHtml5Embed,
|
||||||
|
Android,
|
||||||
|
Ios,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(rename = "camelCase")]
|
||||||
|
struct BaseRequest {
|
||||||
|
context: ContextYT,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(rename = "camelCase")]
|
||||||
|
struct ContextYT {
|
||||||
|
client: ClientInfo,
|
||||||
|
/// only used on desktop
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
request: Option<RequestYT>,
|
||||||
|
user: User,
|
||||||
|
/// only used for the embedded player
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
third_party: Option<ThirdParty>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(rename = "camelCase")]
|
||||||
|
struct ClientInfo {
|
||||||
|
client_name: String,
|
||||||
|
client_version: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
client_screen: Option<String>,
|
||||||
|
platform: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
original_url: Option<String>,
|
||||||
|
/// Language (`en`, `de`)
|
||||||
|
hl: String,
|
||||||
|
/// Country (`US`, `DE`)
|
||||||
|
gl: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(rename = "camelCase")]
|
||||||
|
struct RequestYT {
|
||||||
|
internal_experiment_flags: Vec<String>,
|
||||||
|
use_ssl: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RequestYT {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
internal_experiment_flags: vec![],
|
||||||
|
use_ssl: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Default)]
|
||||||
|
#[serde(rename = "camelCase")]
|
||||||
|
struct User {
|
||||||
|
// TO DO: provide a way to enable restricted mode with:
|
||||||
|
// "enableSafetyMode": true
|
||||||
|
locked_safety_mode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(rename = "camelCase")]
|
||||||
|
struct ThirdParty {
|
||||||
|
embed_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DesktopClientData {
|
||||||
|
last_update: Option<Instant>,
|
||||||
|
client_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DesktopClientData {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
last_update: None,
|
||||||
|
client_version: DESKTOP_CLIENT_VERSION.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DesktopClientData {
|
||||||
|
fn is_old(&self) -> bool {
|
||||||
|
self.last_update.is_none()
|
||||||
|
|| Instant::now()
|
||||||
|
.duration_since(self.last_update.unwrap())
|
||||||
|
.as_secs()
|
||||||
|
> 86400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_UA: &str =
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; rv:107.0) Gecko/20100101 Firefox/107.0";
|
||||||
|
|
||||||
|
const CONSENT_COOKIE: &str = "CONSENT";
|
||||||
|
const CONSENT_COOKIE_YES: &str = "YES+yt.462272069.de+FX+";
|
||||||
|
const CONSENT_COOKIE_NO: &str = "PENDING+";
|
||||||
|
|
||||||
|
const DESKTOP_CLIENT_VERSION: &str = "2.20220721.05.00_1";
|
||||||
|
const DESKTOP_API_KEY: &str = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
|
||||||
|
|
||||||
|
const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] =
|
||||||
|
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||||
|
|
||||||
|
pub struct RustyTube {
|
||||||
|
http: Client,
|
||||||
|
desktop_client_data: Mutex<DesktopClientData>,
|
||||||
|
|
||||||
|
lang: String,
|
||||||
|
country: String,
|
||||||
|
|
||||||
|
consent_cookie_yes: String,
|
||||||
|
consent_cookie_no: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustyTube {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::new_with_ua("en", "US", DEFAULT_UA)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_with_ua(lang: &str, country: &str, user_agent: &str) -> Self {
|
||||||
|
let http = ClientBuilder::new()
|
||||||
|
.user_agent(user_agent)
|
||||||
|
.build()
|
||||||
|
.expect("unable to build the HTTP client");
|
||||||
|
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
http,
|
||||||
|
desktop_client_data: Mutex::new(DesktopClientData::default()),
|
||||||
|
lang: lang.to_owned(),
|
||||||
|
country: country.to_owned(),
|
||||||
|
consent_cookie_yes: format!(
|
||||||
|
"{}={}{}",
|
||||||
|
CONSENT_COOKIE,
|
||||||
|
CONSENT_COOKIE_YES,
|
||||||
|
rng.gen_range(100..1000)
|
||||||
|
),
|
||||||
|
consent_cookie_no: format!(
|
||||||
|
"{}={}{}",
|
||||||
|
CONSENT_COOKIE,
|
||||||
|
CONSENT_COOKIE_NO,
|
||||||
|
rng.gen_range(100..1000)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_client_version(&self) -> String {
|
||||||
|
let mut client_data = self.desktop_client_data.lock().await;
|
||||||
|
|
||||||
|
if client_data.is_old() {
|
||||||
|
let client_version = self.extract_client_version_from_swjs().await;
|
||||||
|
let new_version = match client_version {
|
||||||
|
Ok(client_version) => match client_version {
|
||||||
|
Some(client_version) => {
|
||||||
|
debug!("Updated desktop client version to {}", client_version);
|
||||||
|
client_version
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("Could not find desktop client version in sw.js");
|
||||||
|
DESKTOP_CLIENT_VERSION.to_owned()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Could not extract desktop client version, Error: {}", e);
|
||||||
|
DESKTOP_CLIENT_VERSION.to_owned()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
*client_data = DesktopClientData {
|
||||||
|
client_version: new_version,
|
||||||
|
last_update: Some(Instant::now()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client_data.client_version.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_client_version_from_swjs(&self) -> Result<Option<String>> {
|
||||||
|
let swjs = self
|
||||||
|
.exec_request(
|
||||||
|
self.http
|
||||||
|
.get("https://www.youtube.com/sw.js")
|
||||||
|
.header(header::ORIGIN, "https://www.youtube.com")
|
||||||
|
.header(header::REFERER, "https://www.youtube.com")
|
||||||
|
.header(header::COOKIE, self.consent_cookie_yes.to_owned())
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to download sw.js")?;
|
||||||
|
|
||||||
|
static CLIENT_VERSION_PATTERNS: Lazy<[Regex; 3]> = Lazy::new(|| {
|
||||||
|
[
|
||||||
|
Regex::new("INNERTUBE_CONTEXT_CLIENT_VERSION\":\"([0-9\\.]+?)\"").unwrap(),
|
||||||
|
Regex::new("innertube_context_client_version\":\"([0-9\\.]+?)\"").unwrap(),
|
||||||
|
Regex::new("client.version=([0-9\\.]+)").unwrap(),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
static API_KEY_PATTERNS: Lazy<[Regex; 2]> = Lazy::new(|| {
|
||||||
|
[
|
||||||
|
Regex::new("INNERTUBE_API_KEY\":\"([0-9a-zA-Z_-]+?)\"").unwrap(),
|
||||||
|
Regex::new("innertubeApiKey\":\"([0-9a-zA-Z_-]+?)\"").unwrap(),
|
||||||
|
]
|
||||||
|
});*/
|
||||||
|
|
||||||
|
Ok(util::get_cg_from_regexes(
|
||||||
|
CLIENT_VERSION_PATTERNS.iter(),
|
||||||
|
&swjs,
|
||||||
|
1,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_content_playback_nonce() -> String {
|
||||||
|
util::random_string(CONTENT_PLAYBACK_NONCE_ALPHABET, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_t_parameter() -> String {
|
||||||
|
util::random_string(CONTENT_PLAYBACK_NONCE_ALPHABET, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exec_request(&self, request: Request) -> Result<String> {
|
||||||
|
let resp = self.http.execute(request).await?.error_for_status()?;
|
||||||
|
Ok(resp.text().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
|
#[test(tokio::test)]
|
||||||
|
async fn t_extract_client_version_from_swjs() {
|
||||||
|
let rt = RustyTube::new();
|
||||||
|
let version = rt.extract_client_version_from_swjs().await.unwrap();
|
||||||
|
|
||||||
|
let version = version.unwrap();
|
||||||
|
|
||||||
|
// Client version changes often, notify during test so the hardcoded version can be updated
|
||||||
|
if version != DESKTOP_CLIENT_VERSION {
|
||||||
|
println!(
|
||||||
|
"INFO: YT Desktop Client was updated, new version: {}",
|
||||||
|
version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test(tokio::test)]
|
||||||
|
async fn t_get_client_version() {
|
||||||
|
error!("Checking whether it still works...");
|
||||||
|
let rt = RustyTube::new();
|
||||||
|
let client_version = rt.get_client_version().await;
|
||||||
|
assert!(client_version.len() > 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_test() {
|
||||||
|
let request = BaseRequest {
|
||||||
|
context: ContextYT {
|
||||||
|
client: ClientInfo {
|
||||||
|
client_name: "WEB".to_owned(),
|
||||||
|
client_version: "x".to_owned(),
|
||||||
|
client_screen: None,
|
||||||
|
platform: "DESKTOP".to_owned(),
|
||||||
|
original_url: Some("https://www.youtube.com".to_owned()),
|
||||||
|
hl: "de".to_owned(),
|
||||||
|
gl: "DE".to_owned(),
|
||||||
|
},
|
||||||
|
request: Some(RequestYT::default()),
|
||||||
|
user: User::default(),
|
||||||
|
third_party: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_str = serde_json::to_string_pretty(&request).unwrap();
|
||||||
|
println!("{}", request_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use fancy_regex::Regex;
|
use fancy_regex::Regex;
|
||||||
|
use log::debug;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use std::result::Result::Ok;
|
use std::result::Result::Ok;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
pub struct Deobfuscator {
|
pub struct Deobfuscator {
|
||||||
client: Client,
|
http: Client,
|
||||||
cache: RwLock<JSCache>,
|
cache: RwLock<JSCache>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,9 +22,10 @@ struct JSCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deobfuscator {
|
impl Deobfuscator {
|
||||||
pub fn new(client: Client) -> Self {
|
#[must_use]
|
||||||
|
pub fn new(http: Client) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client: client,
|
http,
|
||||||
cache: RwLock::new(JSCache {
|
cache: RwLock::new(JSCache {
|
||||||
js_url: None,
|
js_url: None,
|
||||||
last_update: Instant::now(),
|
last_update: Instant::now(),
|
||||||
|
|
@ -35,15 +39,15 @@ impl Deobfuscator {
|
||||||
let mut cache = self.cache.write().await;
|
let mut cache = self.cache.write().await;
|
||||||
|
|
||||||
if cache.is_stale() {
|
if cache.is_stale() {
|
||||||
let url = get_player_js_url(&self.client)
|
let url = get_player_js_url(&self.http)
|
||||||
.await
|
.await
|
||||||
.context("Failed to retrieve player.js URL")?;
|
.context("Failed to retrieve player.js URL")?;
|
||||||
|
|
||||||
let player_js = get_website(&self.client, &url)
|
let player_js = get_response(&self.http, &url)
|
||||||
.await
|
.await
|
||||||
.context("Failed to download player.js")?;
|
.context("Failed to download player.js")?;
|
||||||
|
|
||||||
println!("Downloaded player.js from {}", url);
|
debug!("Downloaded player.js from {}", url);
|
||||||
|
|
||||||
let sig_fn = get_sig_fn(&player_js)?;
|
let sig_fn = get_sig_fn(&player_js)?;
|
||||||
let nsig_fn = get_nsig_fn(&player_js)?;
|
let nsig_fn = get_nsig_fn(&player_js)?;
|
||||||
|
|
@ -91,10 +95,7 @@ fn get_sig_fn_name(player_js: &str) -> Result<String> {
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
FUNCTION_PATTERNS
|
util::get_cg_from_regexes(FUNCTION_PATTERNS.iter(), player_js, 1)
|
||||||
.iter()
|
|
||||||
.find_map(|pattern| pattern.captures(player_js).ok().flatten())
|
|
||||||
.map(|c| c.get(1).unwrap().as_str().to_owned())
|
|
||||||
.ok_or_else(|| anyhow!("could not find deobf function name"))
|
.ok_or_else(|| anyhow!("could not find deobf function name"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,8 +271,8 @@ fn deobfuscate_nsig(sig: &str, nsig_fn: &str) -> Result<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_player_js_url(client: &Client) -> Result<String> {
|
async fn get_player_js_url(http: &Client) -> Result<String> {
|
||||||
let resp = client
|
let resp = http
|
||||||
.get("https://www.youtube.com/iframe_api")
|
.get("https://www.youtube.com/iframe_api")
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -295,8 +296,8 @@ async fn get_player_js_url(client: &Client) -> Result<String> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_website(client: &Client, url: &str) -> Result<String> {
|
async fn get_response(http: &Client, url: &str) -> Result<String> {
|
||||||
let resp = client.get(url).send().await?.error_for_status()?;
|
let resp = http.get(url).send().await?.error_for_status()?;
|
||||||
Ok(resp.text().await?)
|
Ok(resp.text().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,6 +305,7 @@ async fn get_website(client: &Client, url: &str) -> Result<String> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
const TEST_JS: &str = include_str!("../notes/base.js");
|
const TEST_JS: &str = include_str!("../notes/base.js");
|
||||||
const N_DEOBF_FUNC: &str = r#"Vo=function(a){var b=a.split(""),c=[function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(""))},
|
const N_DEOBF_FUNC: &str = r#"Vo=function(a){var b=a.split(""),c=[function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(""))},
|
||||||
|
|
@ -321,13 +323,13 @@ c[30]=c;c[40]=c;c[46]=c;try{c[43](c[34]),c[45](c[40],c[47]),c[46](c[51],c[33]),c
|
||||||
c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c[47],c[49]),c[1](c[44],c[28]),c[39](c[16]),c[32](c[42],c[22]),c[46](c[14],c[48]),c[26](c[29],c[10]),c[46](c[9],c[3]),c[32](c[45])}catch(d){return"enhanced_except_85UBjOr-_w8_"+a}return b.join("")};function deobfuscate(a){return Vo(a);}"#;
|
c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c[47],c[49]),c[1](c[44],c[28]),c[39](c[16]),c[32](c[42],c[22]),c[46](c[14],c[48]),c[26](c[29],c[10]),c[46](c[9],c[3]),c[32](c[45])}catch(d){return"enhanced_except_85UBjOr-_w8_"+a}return b.join("")};function deobfuscate(a){return Vo(a);}"#;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_sig_fn_name() {
|
fn t_get_sig_fn_name() {
|
||||||
let dfunc_name = get_sig_fn_name(TEST_JS).unwrap();
|
let dfunc_name = get_sig_fn_name(TEST_JS).unwrap();
|
||||||
assert_eq!(dfunc_name, "Rva");
|
assert_eq!(dfunc_name, "Rva");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_sig_fn() {
|
fn t_get_sig_fn() {
|
||||||
let dcode = get_sig_fn(TEST_JS).unwrap();
|
let dcode = get_sig_fn(TEST_JS).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dcode,
|
dcode,
|
||||||
|
|
@ -336,47 +338,47 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deobfuscate_sig() {
|
fn t_deobfuscate_sig() {
|
||||||
let dcode = get_sig_fn(TEST_JS).unwrap();
|
let dcode = get_sig_fn(TEST_JS).unwrap();
|
||||||
let deobf = deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i", &dcode).unwrap();
|
let deobf = deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i", &dcode).unwrap();
|
||||||
assert_eq!(deobf, "AOq0QJ8wRAIgaryQHmplJ9xJSKFywyaSMHuuwZYsoMTfvRviG51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5f");
|
assert_eq!(deobf, "AOq0QJ8wRAIgaryQHmplJ9xJSKFywyaSMHuuwZYsoMTfvRviG51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5f");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_nsig_fn_name() {
|
fn t_get_nsig_fn_name() {
|
||||||
let name = get_nsig_fn_name(TEST_JS).unwrap();
|
let name = get_nsig_fn_name(TEST_JS).unwrap();
|
||||||
assert_eq!(name, "Vo");
|
assert_eq!(name, "Vo");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_match_to_closing_parenthesis() {
|
fn t_match_to_closing_parenthesis() {
|
||||||
let res =
|
let res =
|
||||||
match_to_closing_parenthesis("Kx Hello { Thx { Bye } } Wut {Tst {}}", "Hello").unwrap();
|
match_to_closing_parenthesis("Kx Hello { Thx { Bye } } Wut {Tst {}}", "Hello").unwrap();
|
||||||
assert_eq!(res, " { Thx { Bye } }")
|
assert_eq!(res, " { Thx { Bye } }")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_nsig_fn() {
|
fn t_get_nsig_fn() {
|
||||||
let res = get_nsig_fn(TEST_JS).unwrap();
|
let res = get_nsig_fn(TEST_JS).unwrap();
|
||||||
assert_eq!(res, N_DEOBF_FUNC);
|
assert_eq!(res, N_DEOBF_FUNC);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deobfuscate_nsig() {
|
fn t_deobfuscate_nsig() {
|
||||||
let res = deobfuscate_nsig("BI_n4PxQ22is-KKajKUW", N_DEOBF_FUNC).unwrap();
|
let res = deobfuscate_nsig("BI_n4PxQ22is-KKajKUW", N_DEOBF_FUNC).unwrap();
|
||||||
assert_eq!(res, "nrkec0fwgTWolw");
|
assert_eq!(res, "nrkec0fwgTWolw");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test(tokio::test)]
|
||||||
async fn test_get_player_js_url() {
|
async fn t_get_player_js_url() {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let url = get_player_js_url(&client).await.unwrap();
|
let url = get_player_js_url(&client).await.unwrap();
|
||||||
assert!(url.starts_with("https://www.youtube.com/s/player"));
|
assert!(url.starts_with("https://www.youtube.com/s/player"));
|
||||||
assert_eq!(url.len(), 73);
|
assert_eq!(url.len(), 73);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test(tokio::test)]
|
||||||
async fn test_update() {
|
async fn t_update() {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let deobf = Deobfuscator::new(client);
|
let deobf = Deobfuscator::new(client);
|
||||||
|
|
||||||
|
|
@ -384,8 +386,8 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c
|
||||||
println!("{}", deobf_sig);
|
println!("{}", deobf_sig);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test(tokio::test)]
|
||||||
async fn test_parallel() {
|
async fn t_parallel() {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let deobf = Deobfuscator::new(client);
|
let deobf = Deobfuscator::new(client);
|
||||||
let deobf_arc = Arc::new(deobf);
|
let deobf_arc = Arc::new(deobf);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
#[macro_use] mod macros;
|
#[allow(dead_code)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
mod macros;
|
||||||
|
|
||||||
|
mod util;
|
||||||
mod deobfuscate;
|
mod deobfuscate;
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
|
|
||||||
28
src/util.rs
Normal file
28
src/util.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
use fancy_regex::Regex;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
/// Return the given capture group that matches first in a list of regexes
|
||||||
|
pub fn get_cg_from_regexes<'a, I>(mut regexes: I, text: &str, cg: usize) -> Option<String>
|
||||||
|
where
|
||||||
|
I: Iterator<Item = &'a Regex>,
|
||||||
|
{
|
||||||
|
regexes
|
||||||
|
.find_map(|pattern| pattern.captures(text).ok().flatten())
|
||||||
|
.map(|c| c.get(cg).unwrap().as_str().to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a random string with given length and byte charset.
|
||||||
|
pub fn random_string(charset: &[u8], length: usize) -> String {
|
||||||
|
let mut result = String::with_capacity(length);
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
for _ in 0..length {
|
||||||
|
result.push(
|
||||||
|
char::from(*charset.get_unchecked(rng.gen_range(0..charset.len())))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
Reference in a new issue