fix: fetch YTM playlists with visitor data
feat: add lang/country options to cli
This commit is contained in:
parent
b25e9ebbb7
commit
d6de428549
3 changed files with 41 additions and 7 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
#![warn(clippy::todo, clippy::dbg_macro)]
|
#![warn(clippy::todo, clippy::dbg_macro)]
|
||||||
|
|
||||||
use std::{path::PathBuf, time::Duration};
|
use std::{path::PathBuf, str::FromStr, time::Duration};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::{Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
|
|
@ -10,7 +10,7 @@ use reqwest::{Client, ClientBuilder};
|
||||||
use rustypipe::{
|
use rustypipe::{
|
||||||
client::RustyPipe,
|
client::RustyPipe,
|
||||||
model::{UrlTarget, VideoId},
|
model::{UrlTarget, VideoId},
|
||||||
param::{search_filter, ChannelVideoTab, StreamFilter},
|
param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter},
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
|
@ -25,6 +25,12 @@ struct Cli {
|
||||||
/// YouTube visitor data cookie
|
/// YouTube visitor data cookie
|
||||||
#[clap(long, global = true)]
|
#[clap(long, global = true)]
|
||||||
vdata: Option<String>,
|
vdata: Option<String>,
|
||||||
|
/// YouTube content language
|
||||||
|
#[clap(long, global = true)]
|
||||||
|
lang: Option<String>,
|
||||||
|
/// YouTube content country
|
||||||
|
#[clap(long, global = true)]
|
||||||
|
country: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
|
|
@ -404,6 +410,12 @@ async fn main() {
|
||||||
std::fs::create_dir_all(&storage_dir).expect("could not create data dir");
|
std::fs::create_dir_all(&storage_dir).expect("could not create data dir");
|
||||||
rp = rp.storage_dir(storage_dir);
|
rp = rp.storage_dir(storage_dir);
|
||||||
}
|
}
|
||||||
|
if let Some(lang) = cli.lang {
|
||||||
|
rp = rp.lang(Language::from_str(&lang.to_ascii_lowercase()).expect("invalid language"));
|
||||||
|
}
|
||||||
|
if let Some(country) = cli.country {
|
||||||
|
rp = rp.country(Country::from_str(&country.to_ascii_uppercase()).expect("invalid country"));
|
||||||
|
}
|
||||||
let rp = rp.build().unwrap();
|
let rp = rp.build().unwrap();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{paginator::Paginator, ChannelId, Playlist, VideoItem},
|
model::{
|
||||||
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
|
ChannelId, Playlist, VideoItem,
|
||||||
|
},
|
||||||
util::{self, timeago, TryRemove},
|
util::{self, timeago, TryRemove},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -15,18 +18,27 @@ impl RustyPipeQuery {
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn playlist<S: AsRef<str> + Debug>(&self, playlist_id: S) -> Result<Playlist, Error> {
|
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;
|
// YTM playlists require visitor data for continuations to work
|
||||||
|
let visitor_data: Option<String> = if playlist_id.starts_with("RD") {
|
||||||
|
Some(self.get_visitor_data().await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let context = self
|
||||||
|
.get_context(ClientType::Desktop, true, visitor_data.as_deref())
|
||||||
|
.await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
context,
|
context,
|
||||||
browse_id: &format!("VL{playlist_id}"),
|
browse_id: &format!("VL{playlist_id}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.execute_request::<response::Playlist, _, _>(
|
self.execute_request_vdata::<response::Playlist, _, _>(
|
||||||
ClientType::Desktop,
|
ClientType::Desktop,
|
||||||
"playlist",
|
"playlist",
|
||||||
playlist_id,
|
playlist_id,
|
||||||
"browse",
|
"browse",
|
||||||
&request_body,
|
&request_body,
|
||||||
|
visitor_data.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +159,13 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
c: Playlist {
|
c: Playlist {
|
||||||
id: playlist_id,
|
id: playlist_id,
|
||||||
name,
|
name,
|
||||||
videos: Paginator::new(Some(n_videos), mapper.items, mapper.ctoken),
|
videos: Paginator::new_ext(
|
||||||
|
Some(n_videos),
|
||||||
|
mapper.items,
|
||||||
|
mapper.ctoken,
|
||||||
|
vdata.map(str::to_owned),
|
||||||
|
ContinuationEndpoint::Browse,
|
||||||
|
),
|
||||||
video_count: n_videos,
|
video_count: n_videos,
|
||||||
thumbnail: thumbnails.into(),
|
thumbnail: thumbnails.into(),
|
||||||
description,
|
description,
|
||||||
|
|
|
||||||
|
|
@ -844,7 +844,11 @@ impl FromStr for Language {
|
||||||
Some(pos) => {
|
Some(pos) => {
|
||||||
sub = &sub[..pos];
|
sub = &sub[..pos];
|
||||||
}
|
}
|
||||||
None => return Err(Error::Other("could not parse language `{s}`".into())),
|
None => {
|
||||||
|
return Err(Error::Other(
|
||||||
|
format!("could not parse language `{s}`").into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Reference in a new issue