diff --git a/cli/src/main.rs b/cli/src/main.rs index b960851..d048820 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,6 +1,6 @@ #![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 clap::{Parser, Subcommand, ValueEnum}; @@ -10,7 +10,7 @@ use reqwest::{Client, ClientBuilder}; use rustypipe::{ client::RustyPipe, model::{UrlTarget, VideoId}, - param::{search_filter, ChannelVideoTab, StreamFilter}, + param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter}, }; use serde::Serialize; @@ -25,6 +25,12 @@ struct Cli { /// YouTube visitor data cookie #[clap(long, global = true)] vdata: Option, + /// YouTube content language + #[clap(long, global = true)] + lang: Option, + /// YouTube content country + #[clap(long, global = true)] + country: Option, } #[derive(Subcommand)] @@ -404,6 +410,12 @@ async fn main() { std::fs::create_dir_all(&storage_dir).expect("could not create data 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(); match cli.command { diff --git a/src/client/playlist.rs b/src/client/playlist.rs index d312010..39485c1 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -4,7 +4,10 @@ use time::OffsetDateTime; use crate::{ error::{Error, ExtractionError}, - model::{paginator::Paginator, ChannelId, Playlist, VideoItem}, + model::{ + paginator::{ContinuationEndpoint, Paginator}, + ChannelId, Playlist, VideoItem, + }, util::{self, timeago, TryRemove}, }; @@ -15,18 +18,27 @@ impl RustyPipeQuery { #[tracing::instrument(skip(self))] pub async fn playlist + Debug>(&self, playlist_id: S) -> Result { 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 = 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 { context, browse_id: &format!("VL{playlist_id}"), }; - self.execute_request::( + self.execute_request_vdata::( ClientType::Desktop, "playlist", playlist_id, "browse", &request_body, + visitor_data.as_deref(), ) .await } @@ -147,7 +159,13 @@ impl MapResponse for response::Playlist { c: Playlist { id: playlist_id, 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, thumbnail: thumbnails.into(), description, diff --git a/src/param/locale.rs b/src/param/locale.rs index a9f1be2..21f049d 100644 --- a/src/param/locale.rs +++ b/src/param/locale.rs @@ -844,7 +844,11 @@ impl FromStr for Language { Some(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(), + )) + } } } }