Compare commits
3 commits
abd3317a10
...
127596687b
Author | SHA1 | Date | |
---|---|---|---|
127596687b | |||
1d1dcd622f | |||
ab599206c5 |
24 changed files with 156 additions and 73 deletions
|
@ -35,7 +35,6 @@ regex = "1.6.0"
|
||||||
fancy-regex = "0.11.0"
|
fancy-regex = "0.11.0"
|
||||||
thiserror = "1.0.36"
|
thiserror = "1.0.36"
|
||||||
url = "2.2.2"
|
url = "2.2.2"
|
||||||
log = "0.4.17"
|
|
||||||
reqwest = { version = "0.11.11", default-features = false, features = [
|
reqwest = { version = "0.11.11", default-features = false, features = [
|
||||||
"json",
|
"json",
|
||||||
"gzip",
|
"gzip",
|
||||||
|
@ -60,11 +59,10 @@ ress = "0.11.4"
|
||||||
phf = "0.11.1"
|
phf = "0.11.1"
|
||||||
base64 = "0.21.0"
|
base64 = "0.21.0"
|
||||||
urlencoding = "2.1.2"
|
urlencoding = "2.1.2"
|
||||||
quick-xml = { version = "0.29.0", features = ["serialize"], optional = true }
|
quick-xml = { version = "0.30.0", features = ["serialize"], optional = true }
|
||||||
|
tracing = { version = "0.1.37", features = ["log"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.10.0"
|
|
||||||
test-log = "0.2.11"
|
|
||||||
rstest = "0.18.1"
|
rstest = "0.18.1"
|
||||||
tokio-test = "0.4.2"
|
tokio-test = "0.4.2"
|
||||||
insta = { version = "1.17.1", features = ["ron", "redactions"] }
|
insta = { version = "1.17.1", features = ["ron", "redactions"] }
|
||||||
|
|
|
@ -47,7 +47,7 @@ indicatif = "0.17.0"
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
clap = { version = "4.0.29", features = ["derive"] }
|
clap = { version = "4.0.29", features = ["derive"] }
|
||||||
env_logger = "0.10.0"
|
tracing-subscriber = "0.3.17"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0.82"
|
serde_json = "1.0.82"
|
||||||
serde_yaml = "0.9.19"
|
serde_yaml = "0.9.19"
|
||||||
|
|
|
@ -390,7 +390,8 @@ async fn download_videos(
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::builder().format_timestamp_micros().init();
|
// env_logger::builder().format_timestamp_micros().init();
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::error;
|
use tracing::error;
|
||||||
|
|
||||||
pub(crate) const DEFAULT_CACHE_FILE: &str = "rustypipe_cache.json";
|
pub(crate) const DEFAULT_CACHE_FILE: &str = "rustypipe_cache.json";
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -78,7 +80,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the videos from a YouTube channel
|
/// Get the videos from a YouTube channel
|
||||||
pub async fn channel_videos<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn channel_videos<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
) -> Result<Channel<Paginator<VideoItem>>, Error> {
|
) -> Result<Channel<Paginator<VideoItem>>, Error> {
|
||||||
|
@ -89,7 +92,8 @@ impl RustyPipeQuery {
|
||||||
/// Get a ordered list of videos from a YouTube channel
|
/// Get a ordered list of videos from a YouTube channel
|
||||||
///
|
///
|
||||||
/// This function does not return channel metadata.
|
/// This function does not return channel metadata.
|
||||||
pub async fn channel_videos_order<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn channel_videos_order<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
order: ChannelOrder,
|
order: ChannelOrder,
|
||||||
|
@ -99,7 +103,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the videos of the given tab (Shorts, Livestreams) from a YouTube channel
|
/// Get the videos of the given tab (Shorts, Livestreams) from a YouTube channel
|
||||||
pub async fn channel_videos_tab<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn channel_videos_tab<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
tab: ChannelVideoTab,
|
tab: ChannelVideoTab,
|
||||||
|
@ -111,7 +116,8 @@ impl RustyPipeQuery {
|
||||||
/// Get a ordered list of videos from the given tab (Shorts, Livestreams) of a YouTube channel
|
/// Get a ordered list of videos from the given tab (Shorts, Livestreams) of a YouTube channel
|
||||||
///
|
///
|
||||||
/// This function does not return channel metadata.
|
/// This function does not return channel metadata.
|
||||||
pub async fn channel_videos_tab_order<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn channel_videos_tab_order<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
tab: ChannelVideoTab,
|
tab: ChannelVideoTab,
|
||||||
|
@ -128,7 +134,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search the videos of a channel
|
/// Search the videos of a channel
|
||||||
pub async fn channel_search<S: AsRef<str>, S2: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn channel_search<S: AsRef<str> + Debug, S2: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
query: S2,
|
query: S2,
|
||||||
|
@ -143,7 +150,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the playlists of a channel
|
/// Get the playlists of a channel
|
||||||
pub async fn channel_playlists<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn channel_playlists<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
) -> Result<Channel<Paginator<PlaylistItem>>, Error> {
|
) -> Result<Channel<Paginator<PlaylistItem>>, Error> {
|
||||||
|
@ -167,7 +175,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get additional metadata from the *About* tab of a channel
|
/// Get additional metadata from the *About* tab of a channel
|
||||||
pub async fn channel_info<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn channel_info<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
) -> Result<Channel<ChannelInfo>, Error> {
|
) -> Result<Channel<ChannelInfo>, Error> {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::ChannelRss,
|
model::ChannelRss,
|
||||||
|
@ -15,7 +17,11 @@ impl RustyPipeQuery {
|
||||||
/// for checking a lot of channels or implementing a subscription feed.
|
/// for checking a lot of channels or implementing a subscription feed.
|
||||||
///
|
///
|
||||||
/// The downside of using the RSS feed is that it does not provide video durations.
|
/// The downside of using the RSS feed is that it does not provide video durations.
|
||||||
pub async fn channel_rss<S: AsRef<str>>(&self, channel_id: S) -> Result<ChannelRss, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn channel_rss<S: AsRef<str> + Debug>(
|
||||||
|
&self,
|
||||||
|
channel_id: S,
|
||||||
|
) -> Result<ChannelRss, Error> {
|
||||||
let channel_id = channel_id.as_ref();
|
let channel_id = channel_id.as_ref();
|
||||||
let url = format!("https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}");
|
let url = format!("https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}");
|
||||||
let xml = self
|
let xml = self
|
||||||
|
|
|
@ -489,7 +489,7 @@ impl RustyPipeBuilder {
|
||||||
.and_then(|data| match serde_json::from_str::<CacheData>(&data) {
|
.and_then(|data| match serde_json::from_str::<CacheData>(&data) {
|
||||||
Ok(data) => Some(data),
|
Ok(data) => Some(data),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Could not deserialize cache. Error: {}", e);
|
tracing::error!("Could not deserialize cache. Error: {}", e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -745,7 +745,7 @@ impl RustyPipe {
|
||||||
// Retry in case of a recoverable status code (server err, too many requests)
|
// Retry in case of a recoverable status code (server err, too many requests)
|
||||||
if n != self.inner.n_http_retries {
|
if n != self.inner.n_http_retries {
|
||||||
let ms = util::retry_delay(n, 1000, 60000, 3);
|
let ms = util::retry_delay(n, 1000, 60000, 3);
|
||||||
log::warn!(
|
tracing::warn!(
|
||||||
"Retry attempt #{}. Error: {}. Waiting {} ms",
|
"Retry attempt #{}. Error: {}. Waiting {} ms",
|
||||||
n + 1,
|
n + 1,
|
||||||
status,
|
status,
|
||||||
|
@ -866,7 +866,7 @@ impl RustyPipe {
|
||||||
match desktop_client.get() {
|
match desktop_client.get() {
|
||||||
Some(cdata) => cdata.version.clone(),
|
Some(cdata) => cdata.version.clone(),
|
||||||
None => {
|
None => {
|
||||||
log::debug!("getting desktop client version");
|
tracing::debug!("getting desktop client version");
|
||||||
match self.extract_desktop_client_version().await {
|
match self.extract_desktop_client_version().await {
|
||||||
Ok(version) => {
|
Ok(version) => {
|
||||||
*desktop_client = CacheEntry::from(ClientData {
|
*desktop_client = CacheEntry::from(ClientData {
|
||||||
|
@ -877,7 +877,7 @@ impl RustyPipe {
|
||||||
version
|
version
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("{}, falling back to hardcoded desktop client version", e);
|
tracing::warn!("{}, falling back to hardcoded desktop client version", e);
|
||||||
DESKTOP_CLIENT_VERSION.to_owned()
|
DESKTOP_CLIENT_VERSION.to_owned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -898,7 +898,7 @@ impl RustyPipe {
|
||||||
match music_client.get() {
|
match music_client.get() {
|
||||||
Some(cdata) => cdata.version.clone(),
|
Some(cdata) => cdata.version.clone(),
|
||||||
None => {
|
None => {
|
||||||
log::debug!("getting music client version");
|
tracing::debug!("getting music client version");
|
||||||
match self.extract_music_client_version().await {
|
match self.extract_music_client_version().await {
|
||||||
Ok(version) => {
|
Ok(version) => {
|
||||||
*music_client = CacheEntry::from(ClientData {
|
*music_client = CacheEntry::from(ClientData {
|
||||||
|
@ -909,7 +909,7 @@ impl RustyPipe {
|
||||||
version
|
version
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("{}, falling back to hardcoded music client version", e);
|
tracing::warn!("{}, falling back to hardcoded music client version", e);
|
||||||
DESKTOP_MUSIC_CLIENT_VERSION.to_owned()
|
DESKTOP_MUSIC_CLIENT_VERSION.to_owned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -925,7 +925,7 @@ impl RustyPipe {
|
||||||
match deobf_data.get() {
|
match deobf_data.get() {
|
||||||
Some(deobf_data) => Ok(deobf_data.clone()),
|
Some(deobf_data) => Ok(deobf_data.clone()),
|
||||||
None => {
|
None => {
|
||||||
log::debug!("getting deobf data");
|
tracing::debug!("getting deobf data");
|
||||||
|
|
||||||
match DeobfData::extract(self.inner.http.clone(), self.inner.reporter.as_deref())
|
match DeobfData::extract(self.inner.http.clone(), self.inner.reporter.as_deref())
|
||||||
.await
|
.await
|
||||||
|
@ -941,7 +941,7 @@ impl RustyPipe {
|
||||||
// Try to fall back to expired cache data if available, otherwise return error
|
// Try to fall back to expired cache data if available, otherwise return error
|
||||||
match deobf_data.get_expired() {
|
match deobf_data.get_expired() {
|
||||||
Some(d) => {
|
Some(d) => {
|
||||||
log::warn!("could not get new deobf data ({e}), falling back to expired cache");
|
tracing::warn!("could not get new deobf data ({e}), falling back to expired cache");
|
||||||
Ok(d.clone())
|
Ok(d.clone())
|
||||||
}
|
}
|
||||||
None => Err(e),
|
None => Err(e),
|
||||||
|
@ -963,7 +963,7 @@ impl RustyPipe {
|
||||||
|
|
||||||
match serde_json::to_string(&cdata) {
|
match serde_json::to_string(&cdata) {
|
||||||
Ok(data) => storage.write(&data),
|
Ok(data) => storage.write(&data),
|
||||||
Err(e) => log::error!("Could not serialize cache. Error: {}", e),
|
Err(e) => tracing::error!("Could not serialize cache. Error: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -976,7 +976,7 @@ impl RustyPipe {
|
||||||
/// Sometimes YouTube does not set the `__Secure-YEC` cookie. In this case, the
|
/// Sometimes YouTube does not set the `__Secure-YEC` cookie. In this case, the
|
||||||
/// visitor data is extracted from the html page.
|
/// visitor data is extracted from the html page.
|
||||||
async fn get_visitor_data(&self) -> Result<String, Error> {
|
async fn get_visitor_data(&self) -> Result<String, Error> {
|
||||||
log::debug!("getting YT visitor data");
|
tracing::debug!("getting YT visitor data");
|
||||||
let resp = self.inner.http.get(YOUTUBE_MUSIC_HOME_URL).send().await?;
|
let resp = self.inner.http.get(YOUTUBE_MUSIC_HOME_URL).send().await?;
|
||||||
|
|
||||||
let vdata = resp
|
let vdata = resp
|
||||||
|
@ -1284,6 +1284,7 @@ impl RustyPipeQuery {
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let body = response.text().await?;
|
let body = response.text().await?;
|
||||||
|
tracing::debug!("fetched {} bytes from YT", body.len());
|
||||||
|
|
||||||
let res = if status.is_client_error() || status.is_server_error() {
|
let res = if status.is_client_error() || status.is_server_error() {
|
||||||
let error_msg = serde_json::from_str::<response::ErrorResponse>(&body)
|
let error_msg = serde_json::from_str::<response::ErrorResponse>(&body)
|
||||||
|
@ -1314,9 +1315,11 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tracing::debug!("mapped response");
|
||||||
Ok(RequestResult { res, status, body })
|
Ok(RequestResult { res, status, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
async fn yt_request<R: DeserializeOwned + MapResponse<M> + Debug, M>(
|
async fn yt_request<R: DeserializeOwned + MapResponse<M> + Debug, M>(
|
||||||
&self,
|
&self,
|
||||||
request: &Request,
|
request: &Request,
|
||||||
|
@ -1339,7 +1342,7 @@ impl RustyPipeQuery {
|
||||||
|
|
||||||
if n != self.client.inner.n_http_retries {
|
if n != self.client.inner.n_http_retries {
|
||||||
let ms = util::retry_delay(n, 1000, 60000, 3);
|
let ms = util::retry_delay(n, 1000, 60000, 3);
|
||||||
log::warn!(
|
tracing::warn!(
|
||||||
"Retry attempt #{}. Error: {}. Waiting {} ms",
|
"Retry attempt #{}. Error: {}. Waiting {} ms",
|
||||||
n + 1,
|
n + 1,
|
||||||
err,
|
err,
|
||||||
|
@ -1380,7 +1383,7 @@ impl RustyPipeQuery {
|
||||||
body: &B,
|
body: &B,
|
||||||
deobf: Option<&DeobfData>,
|
deobf: Option<&DeobfData>,
|
||||||
) -> Result<M, Error> {
|
) -> Result<M, Error> {
|
||||||
log::debug!("getting {}({})", operation, id);
|
tracing::debug!("getting {}({})", operation, id);
|
||||||
|
|
||||||
let request = self
|
let request = self
|
||||||
.request_builder(ctype, endpoint)
|
.request_builder(ctype, endpoint)
|
||||||
|
@ -1528,7 +1531,7 @@ trait MapResponse<T> {
|
||||||
|
|
||||||
fn validate_country(country: Country) -> Country {
|
fn validate_country(country: Country) -> Country {
|
||||||
if country == Country::Zz {
|
if country == Country::Zz {
|
||||||
log::warn!("Country:Zz (Global) can only be used for fetching music charts, falling back to Country:Us");
|
tracing::warn!("Country:Zz (Global) can only be used for fetching music charts, falling back to Country:Us");
|
||||||
Country::Us
|
Country::Us
|
||||||
} else {
|
} else {
|
||||||
country
|
country
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::borrow::Cow;
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::response::url_endpoint::NavigationEndpoint,
|
client::response::url_endpoint::NavigationEndpoint,
|
||||||
|
@ -29,7 +30,7 @@ impl RustyPipeQuery {
|
||||||
let res = self._music_artist(artist_id, all_albums).await;
|
let res = self._music_artist(artist_id, all_albums).await;
|
||||||
|
|
||||||
if let Err(Error::Extraction(ExtractionError::Redirect(id))) = res {
|
if let Err(Error::Extraction(ExtractionError::Redirect(id))) = res {
|
||||||
log::debug!("music artist {} redirects to {}", artist_id, &id);
|
debug!("music artist {} redirects to {}", artist_id, &id);
|
||||||
self._music_artist(&id, all_albums).await
|
self._music_artist(&id, all_albums).await
|
||||||
} else {
|
} else {
|
||||||
res
|
res
|
||||||
|
|
|
@ -32,6 +32,7 @@ struct FormData {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the YouTube Music charts for a given country
|
/// Get the YouTube Music charts for a given country
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_charts(&self, country: Option<Country>) -> Result<MusicCharts, Error> {
|
pub async fn music_charts(&self, country: Option<Country>) -> Result<MusicCharts, Error> {
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QCharts {
|
let request_body = QCharts {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, fmt::Debug};
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
@ -38,7 +38,11 @@ struct QRadio<'a> {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the metadata of a YouTube music track
|
/// Get the metadata of a YouTube music track
|
||||||
pub async fn music_details<S: AsRef<str>>(&self, video_id: S) -> Result<TrackDetails, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_details<S: AsRef<str> + Debug>(
|
||||||
|
&self,
|
||||||
|
video_id: S,
|
||||||
|
) -> Result<TrackDetails, Error> {
|
||||||
let video_id = video_id.as_ref();
|
let video_id = video_id.as_ref();
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QMusicDetails {
|
let request_body = QMusicDetails {
|
||||||
|
@ -62,7 +66,8 @@ impl RustyPipeQuery {
|
||||||
/// Get the lyrics of a YouTube music track
|
/// Get the lyrics of a YouTube music track
|
||||||
///
|
///
|
||||||
/// The `lyrics_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
/// The `lyrics_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
||||||
pub async fn music_lyrics<S: AsRef<str>>(&self, lyrics_id: S) -> Result<Lyrics, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_lyrics<S: AsRef<str> + Debug>(&self, lyrics_id: S) -> Result<Lyrics, Error> {
|
||||||
let lyrics_id = lyrics_id.as_ref();
|
let lyrics_id = lyrics_id.as_ref();
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
@ -83,7 +88,11 @@ impl RustyPipeQuery {
|
||||||
/// Get related items (tracks, playlists, artists) to a YouTube Music track
|
/// Get related items (tracks, playlists, artists) to a YouTube Music track
|
||||||
///
|
///
|
||||||
/// The `related_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
/// The `related_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
||||||
pub async fn music_related<S: AsRef<str>>(&self, related_id: S) -> Result<MusicRelated, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_related<S: AsRef<str> + Debug>(
|
||||||
|
&self,
|
||||||
|
related_id: S,
|
||||||
|
) -> Result<MusicRelated, Error> {
|
||||||
let related_id = related_id.as_ref();
|
let related_id = related_id.as_ref();
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
@ -104,7 +113,8 @@ impl RustyPipeQuery {
|
||||||
/// Get a YouTube Music radio (a dynamically generated playlist)
|
/// Get a YouTube Music radio (a dynamically generated playlist)
|
||||||
///
|
///
|
||||||
/// The `radio_id` can be obtained using [`RustyPipeQuery::music_artist`] to get an artist's radio.
|
/// The `radio_id` can be obtained using [`RustyPipeQuery::music_artist`] to get an artist's radio.
|
||||||
pub async fn music_radio<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_radio<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
radio_id: S,
|
radio_id: S,
|
||||||
) -> Result<Paginator<TrackItem>, Error> {
|
) -> Result<Paginator<TrackItem>, Error> {
|
||||||
|
@ -133,7 +143,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a YouTube Music radio (a dynamically generated playlist) for a track
|
/// Get a YouTube Music radio (a dynamically generated playlist) for a track
|
||||||
pub async fn music_radio_track<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_radio_track<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
video_id: S,
|
video_id: S,
|
||||||
) -> Result<Paginator<TrackItem>, Error> {
|
) -> Result<Paginator<TrackItem>, Error> {
|
||||||
|
@ -142,7 +153,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a YouTube Music radio (a dynamically generated playlist) for a playlist
|
/// Get a YouTube Music radio (a dynamically generated playlist) for a playlist
|
||||||
pub async fn music_radio_playlist<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_radio_playlist<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
playlist_id: S,
|
playlist_id: S,
|
||||||
) -> Result<Paginator<TrackItem>, Error> {
|
) -> Result<Paginator<TrackItem>, Error> {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, fmt::Debug};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
|
@ -13,6 +13,7 @@ use super::{
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get a list of moods and genres from YouTube Music
|
/// Get a list of moods and genres from YouTube Music
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_genres(&self) -> Result<Vec<MusicGenreItem>, Error> {
|
pub async fn music_genres(&self) -> Result<Vec<MusicGenreItem>, Error> {
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
@ -31,7 +32,11 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the playlists from a YouTube Music genre
|
/// Get the playlists from a YouTube Music genre
|
||||||
pub async fn music_genre<S: AsRef<str>>(&self, genre_id: S) -> Result<MusicGenre, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_genre<S: AsRef<str> + Debug>(
|
||||||
|
&self,
|
||||||
|
genre_id: S,
|
||||||
|
) -> Result<MusicGenre, Error> {
|
||||||
let genre_id = genre_id.as_ref();
|
let genre_id = genre_id.as_ref();
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QBrowseParams {
|
let request_body = QBrowseParams {
|
||||||
|
|
|
@ -10,6 +10,7 @@ use super::{response, ClientType, MapResponse, QBrowse, RustyPipeQuery};
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the new albums that were released on YouTube Music
|
/// Get the new albums that were released on YouTube Music
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_new_albums(&self) -> Result<Vec<AlbumItem>, Error> {
|
pub async fn music_new_albums(&self) -> Result<Vec<AlbumItem>, Error> {
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
@ -28,6 +29,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the new music videos that were released on YouTube Music
|
/// Get the new music videos that were released on YouTube Music
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_new_videos(&self) -> Result<Vec<TrackItem>, Error> {
|
pub async fn music_new_videos(&self) -> Result<Vec<TrackItem>, Error> {
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, fmt::Debug};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
|
@ -17,7 +17,8 @@ use super::{
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get a playlist from YouTube Music
|
/// Get a playlist from YouTube Music
|
||||||
pub async fn music_playlist<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_playlist<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
playlist_id: S,
|
playlist_id: S,
|
||||||
) -> Result<MusicPlaylist, Error> {
|
) -> Result<MusicPlaylist, Error> {
|
||||||
|
@ -39,7 +40,11 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an album from YouTube Music
|
/// Get an album from YouTube Music
|
||||||
pub async fn music_album<S: AsRef<str>>(&self, album_id: S) -> Result<MusicAlbum, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_album<S: AsRef<str> + Debug>(
|
||||||
|
&self,
|
||||||
|
album_id: S,
|
||||||
|
) -> Result<MusicAlbum, Error> {
|
||||||
let album_id = album_id.as_ref();
|
let album_id = album_id.as_ref();
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, fmt::Debug};
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
@ -48,7 +48,11 @@ enum Params {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Search YouTube Music. Returns items from any type.
|
/// Search YouTube Music. Returns items from any type.
|
||||||
pub async fn music_search<S: AsRef<str>>(&self, query: S) -> Result<MusicSearchResult, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_search<S: AsRef<str> + Debug>(
|
||||||
|
&self,
|
||||||
|
query: S,
|
||||||
|
) -> Result<MusicSearchResult, Error> {
|
||||||
let query = query.as_ref();
|
let query = query.as_ref();
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QSearch {
|
let request_body = QSearch {
|
||||||
|
@ -68,7 +72,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search YouTube Music tracks
|
/// Search YouTube Music tracks
|
||||||
pub async fn music_search_tracks<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_search_tracks<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: S,
|
query: S,
|
||||||
) -> Result<MusicSearchFiltered<TrackItem>, Error> {
|
) -> Result<MusicSearchFiltered<TrackItem>, Error> {
|
||||||
|
@ -76,7 +81,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search YouTube Music videos
|
/// Search YouTube Music videos
|
||||||
pub async fn music_search_videos<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_search_videos<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: S,
|
query: S,
|
||||||
) -> Result<MusicSearchFiltered<TrackItem>, Error> {
|
) -> Result<MusicSearchFiltered<TrackItem>, Error> {
|
||||||
|
@ -107,7 +113,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search YouTube Music albums
|
/// Search YouTube Music albums
|
||||||
pub async fn music_search_albums<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_search_albums<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: S,
|
query: S,
|
||||||
) -> Result<MusicSearchFiltered<AlbumItem>, Error> {
|
) -> Result<MusicSearchFiltered<AlbumItem>, Error> {
|
||||||
|
@ -130,10 +137,12 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search YouTube Music artists
|
/// Search YouTube Music artists
|
||||||
pub async fn music_search_artists(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_search_artists<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: &str,
|
query: S,
|
||||||
) -> Result<MusicSearchFiltered<ArtistItem>, Error> {
|
) -> Result<MusicSearchFiltered<ArtistItem>, Error> {
|
||||||
|
let query = query.as_ref();
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QSearch {
|
let request_body = QSearch {
|
||||||
context,
|
context,
|
||||||
|
@ -154,7 +163,8 @@ impl RustyPipeQuery {
|
||||||
///
|
///
|
||||||
/// Playlists are filtered whether they are created by users
|
/// Playlists are filtered whether they are created by users
|
||||||
/// (`community=true`) or by YouTube Music (`community=false`)
|
/// (`community=true`) or by YouTube Music (`community=false`)
|
||||||
pub async fn music_search_playlists<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_search_playlists<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: S,
|
query: S,
|
||||||
community: bool,
|
community: bool,
|
||||||
|
@ -182,7 +192,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get YouTube Music search suggestions
|
/// Get YouTube Music search suggestions
|
||||||
pub async fn music_search_suggestion<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn music_search_suggestion<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: S,
|
query: S,
|
||||||
) -> Result<MusicSearchSuggestion, Error> {
|
) -> Result<MusicSearchSuggestion, Error> {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use crate::error::{Error, ExtractionError};
|
use crate::error::{Error, ExtractionError};
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
paginator::{ContinuationEndpoint, Paginator},
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
|
@ -11,7 +13,8 @@ use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery};
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get more YouTube items from the given continuation token and endpoint
|
/// Get more YouTube items from the given continuation token and endpoint
|
||||||
pub async fn continuation<T: FromYtItem, S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn continuation<T: FromYtItem, S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
ctoken: S,
|
ctoken: S,
|
||||||
endpoint: ContinuationEndpoint,
|
endpoint: ContinuationEndpoint,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap},
|
||||||
|
fmt::Debug,
|
||||||
};
|
};
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
@ -61,7 +62,8 @@ struct QContentPlaybackContext<'a> {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get YouTube player data (video/audio streams + basic metadata)
|
/// Get YouTube player data (video/audio streams + basic metadata)
|
||||||
pub async fn player<S: AsRef<str>>(&self, video_id: S) -> Result<VideoPlayer, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn player<S: AsRef<str> + Debug>(&self, video_id: S) -> Result<VideoPlayer, Error> {
|
||||||
let video_id = video_id.as_ref();
|
let video_id = video_id.as_ref();
|
||||||
let desktop_res = self.player_from_client(video_id, ClientType::Desktop).await;
|
let desktop_res = self.player_from_client(video_id, ClientType::Desktop).await;
|
||||||
|
|
||||||
|
@ -90,7 +92,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get YouTube player data (video/audio streams + basic metadata) using the specified client
|
/// Get YouTube player data (video/audio streams + basic metadata) using the specified client
|
||||||
pub async fn player_from_client<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn player_from_client<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
video_id: S,
|
video_id: S,
|
||||||
client_type: ClientType,
|
client_type: ClientType,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{borrow::Cow, convert::TryFrom};
|
use std::{borrow::Cow, convert::TryFrom, fmt::Debug};
|
||||||
|
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
@ -12,7 +12,8 @@ use super::{response, ClientType, MapResponse, MapResult, QBrowse, RustyPipeQuer
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get a YouTube playlist
|
/// Get a YouTube playlist
|
||||||
pub async fn playlist<S: AsRef<str>>(&self, playlist_id: S) -> Result<Playlist, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn playlist<S: AsRef<str> + Debug>(&self, playlist_id: S) -> Result<Playlist, Error> {
|
||||||
let playlist_id = playlist_id.as_ref();
|
let playlist_id = playlist_id.as_ref();
|
||||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -19,7 +21,8 @@ struct QSearch<'a> {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Search YouTube
|
/// Search YouTube
|
||||||
pub async fn search<S: AsRef<str>>(&self, query: S) -> Result<SearchResult, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn search<S: AsRef<str> + Debug>(&self, query: S) -> Result<SearchResult, Error> {
|
||||||
let query = query.as_ref();
|
let query = query.as_ref();
|
||||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||||
let request_body = QSearch {
|
let request_body = QSearch {
|
||||||
|
@ -39,7 +42,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search YouTube using the given [`SearchFilter`]
|
/// Search YouTube using the given [`SearchFilter`]
|
||||||
pub async fn search_filter<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn search_filter<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: S,
|
query: S,
|
||||||
filter: &SearchFilter,
|
filter: &SearchFilter,
|
||||||
|
@ -63,7 +67,11 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get YouTube search suggestions
|
/// Get YouTube search suggestions
|
||||||
pub async fn search_suggestion<S: AsRef<str>>(&self, query: S) -> Result<Vec<String>, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn search_suggestion<S: AsRef<str> + Debug>(
|
||||||
|
&self,
|
||||||
|
query: S,
|
||||||
|
) -> Result<Vec<String>, Error> {
|
||||||
let url = url::Url::parse_with_params(
|
let url = url::Url::parse_with_params(
|
||||||
"https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&xhr=t",
|
"https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&xhr=t",
|
||||||
&[
|
&[
|
||||||
|
|
|
@ -11,6 +11,7 @@ use super::{response, ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipe
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the videos from the YouTube startpage
|
/// Get the videos from the YouTube startpage
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn startpage(&self) -> Result<Paginator<VideoItem>, Error> {
|
pub async fn startpage(&self) -> Result<Paginator<VideoItem>, Error> {
|
||||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
@ -29,6 +30,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the videos from the YouTube trending page
|
/// Get the videos from the YouTube trending page
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn trending(&self) -> Result<Vec<VideoItem>, Error> {
|
pub async fn trending(&self) -> Result<Vec<VideoItem>, Error> {
|
||||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||||
let request_body = QBrowseParams {
|
let request_body = QBrowseParams {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, fmt::Debug};
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
@ -59,7 +59,8 @@ impl RustyPipeQuery {
|
||||||
/// );
|
/// );
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn resolve_url<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn resolve_url<S: AsRef<str> + Debug>(
|
||||||
self,
|
self,
|
||||||
url: S,
|
url: S,
|
||||||
resolve_albums: bool,
|
resolve_albums: bool,
|
||||||
|
@ -236,7 +237,8 @@ impl RustyPipeQuery {
|
||||||
/// );
|
/// );
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn resolve_string<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn resolve_string<S: AsRef<str> + Debug>(
|
||||||
self,
|
self,
|
||||||
s: S,
|
s: S,
|
||||||
resolve_albums: bool,
|
resolve_albums: bool,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -26,7 +28,11 @@ struct QVideo<'a> {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the metadata for a video
|
/// Get the metadata for a video
|
||||||
pub async fn video_details<S: AsRef<str>>(&self, video_id: S) -> Result<VideoDetails, Error> {
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn video_details<S: AsRef<str> + Debug>(
|
||||||
|
&self,
|
||||||
|
video_id: S,
|
||||||
|
) -> Result<VideoDetails, Error> {
|
||||||
let video_id = video_id.as_ref();
|
let video_id = video_id.as_ref();
|
||||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||||
let request_body = QVideo {
|
let request_body = QVideo {
|
||||||
|
@ -47,7 +53,8 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the comments for a video using the continuation token obtained from `rusty_pipe_query.video_details()`
|
/// Get the comments for a video using the continuation token obtained from `rusty_pipe_query.video_details()`
|
||||||
pub async fn video_comments<S: AsRef<str>>(
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn video_comments<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
ctoken: S,
|
ctoken: S,
|
||||||
visitor_data: Option<&str>,
|
visitor_data: Option<&str>,
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl DeobfData {
|
||||||
pub async fn extract(http: Client, reporter: Option<&dyn Reporter>) -> Result<Self, Error> {
|
pub async fn extract(http: Client, reporter: Option<&dyn Reporter>) -> Result<Self, Error> {
|
||||||
let js_url = get_player_js_url(&http).await?;
|
let js_url = get_player_js_url(&http).await?;
|
||||||
let player_js = get_response(&http, &js_url).await?;
|
let player_js = get_response(&http, &js_url).await?;
|
||||||
log::debug!("downloaded player.js from {}", js_url);
|
tracing::debug!("downloaded player.js from {}", js_url);
|
||||||
|
|
||||||
let res = Self::extract_fns(&js_url, &player_js);
|
let res = Self::extract_fns(&js_url, &player_js);
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ impl Deobfuscator {
|
||||||
res.as_str().map_or(
|
res.as_str().map_or(
|
||||||
Err(DeobfError::Other("sig deobfuscation func returned null")),
|
Err(DeobfError::Other("sig deobfuscation func returned null")),
|
||||||
|res| {
|
|res| {
|
||||||
log::debug!("deobfuscated sig");
|
tracing::debug!("deobfuscated sig");
|
||||||
Ok(res.to_owned())
|
Ok(res.to_owned())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -102,7 +102,7 @@ impl Deobfuscator {
|
||||||
res.as_str().map_or(
|
res.as_str().map_or(
|
||||||
Err(DeobfError::Other("nsig deobfuscation func returned null")),
|
Err(DeobfError::Other("nsig deobfuscation func returned null")),
|
||||||
|res| {
|
|res| {
|
||||||
log::debug!("deobfuscated nsig");
|
tracing::debug!("deobfuscated nsig");
|
||||||
Ok(res.to_owned())
|
Ok(res.to_owned())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -23,9 +23,9 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::error;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::{macros::format_description, OffsetDateTime};
|
use time::{macros::format_description, OffsetDateTime};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{deobfuscate::DeobfData, util};
|
use crate::{deobfuscate::DeobfData, util};
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,6 @@ use rustypipe::param::{
|
||||||
#[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
#[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
||||||
#[case::android(ClientType::Android)]
|
#[case::android(ClientType::Android)]
|
||||||
#[case::ios(ClientType::Ios)]
|
#[case::ios(ClientType::Ios)]
|
||||||
#[test_log::test]
|
|
||||||
fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe) {
|
fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe) {
|
||||||
let player_data =
|
let player_data =
|
||||||
tokio_test::block_on(rp.query().player_from_client("n4tK7LYFxI0", client_type)).unwrap();
|
tokio_test::block_on(rp.query().player_from_client("n4tK7LYFxI0", client_type)).unwrap();
|
||||||
|
@ -1469,7 +1468,6 @@ fn music_album_not_found(rp: RustyPipe) {
|
||||||
#[case::no_artist("no_artist", "UCh8gHdtzO2tXd593_bjErWg", false, 0, 2)]
|
#[case::no_artist("no_artist", "UCh8gHdtzO2tXd593_bjErWg", false, 0, 2)]
|
||||||
// querying Trailerpark's secondary YouTube channel should result in the YTM channel being fetched
|
// querying Trailerpark's secondary YouTube channel should result in the YTM channel being fetched
|
||||||
#[case::secondary_channel("no_more_albums", "UCC9192yGQD25eBZgFZ84MPw", true, 15, 0)]
|
#[case::secondary_channel("no_more_albums", "UCC9192yGQD25eBZgFZ84MPw", true, 15, 0)]
|
||||||
#[test_log::test]
|
|
||||||
fn music_artist(
|
fn music_artist(
|
||||||
#[case] name: &str,
|
#[case] name: &str,
|
||||||
#[case] id: &str,
|
#[case] id: &str,
|
||||||
|
@ -1491,12 +1489,17 @@ fn music_artist(
|
||||||
assert_gte(artist.subscriber_count.unwrap(), 30000, "subscribers");
|
assert_gte(artist.subscriber_count.unwrap(), 30000, "subscribers");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
artist.tracks.iter().for_each(|t| {
|
||||||
|
assert!(!t.cover.is_empty());
|
||||||
|
if t.is_video {
|
||||||
|
assert!(t.view_count.is_some());
|
||||||
|
} else {
|
||||||
|
assert!(t.album.is_some());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Check images
|
// Check images
|
||||||
assert!(!artist.header_image.is_empty(), "got no header image");
|
assert!(!artist.header_image.is_empty(), "got no header image");
|
||||||
artist
|
|
||||||
.tracks
|
|
||||||
.iter()
|
|
||||||
.for_each(|t| assert!(!t.cover.is_empty()));
|
|
||||||
artist
|
artist
|
||||||
.albums
|
.albums
|
||||||
.iter()
|
.iter()
|
||||||
|
|
Loading…
Reference in a new issue