docs: improve documentation
This commit is contained in:
parent
a2bbc850a7
commit
a6bf9359b9
20 changed files with 323 additions and 45 deletions
|
|
@ -6,6 +6,7 @@ authors = ["ThetaDev <t.testboy@gmail.com>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
description = "Client for the public YouTube / YouTube Music API (Innertube), inspired by NewPipe"
|
description = "Client for the public YouTube / YouTube Music API (Innertube), inspired by NewPipe"
|
||||||
keywords = ["youtube", "video", "music"]
|
keywords = ["youtube", "video", "music"]
|
||||||
|
categories = ["api-bindings", "multimedia"]
|
||||||
|
|
||||||
include = ["/src", "README.md", "LICENSE", "!snapshots"]
|
include = ["/src", "README.md", "LICENSE", "!snapshots"]
|
||||||
|
|
||||||
|
|
|
||||||
129
README.md
129
README.md
|
|
@ -2,16 +2,16 @@
|
||||||
|
|
||||||
[](https://ci.thetadev.de/ThetaDev/rustypipe)
|
[](https://ci.thetadev.de/ThetaDev/rustypipe)
|
||||||
|
|
||||||
Client for the public YouTube / YouTube Music API (Innertube),
|
Client for the public YouTube / YouTube Music API (Innertube), inspired by
|
||||||
inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
[NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### YouTube
|
### YouTube
|
||||||
|
|
||||||
- **Player** (video/audio streams, subtitles)
|
- **Player** (video/audio streams, subtitles)
|
||||||
- **Playlist**
|
|
||||||
- **VideoDetails** (metadata, comments, recommended videos)
|
- **VideoDetails** (metadata, comments, recommended videos)
|
||||||
|
- **Playlist**
|
||||||
- **Channel** (videos, shorts, livestreams, playlists, info, search)
|
- **Channel** (videos, shorts, livestreams, playlists, info, search)
|
||||||
- **ChannelRSS**
|
- **ChannelRSS**
|
||||||
- **Search** (with filters)
|
- **Search** (with filters)
|
||||||
|
|
@ -31,3 +31,126 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||||
- **Moods/Genres**
|
- **Moods/Genres**
|
||||||
- **Charts**
|
- **Charts**
|
||||||
- **New** (albums, music videos)
|
- **New** (albums, music videos)
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
### Cargo.toml
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
rustypipe = "0.1.0"
|
||||||
|
tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Watch a video
|
||||||
|
|
||||||
|
```rust ignore
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use rustypipe::{client::RustyPipe, param::StreamFilter};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Create a client
|
||||||
|
let rp = RustyPipe::new();
|
||||||
|
// Fetch the player
|
||||||
|
let player = rp.query().player("pPvd8UxmSbQ").await.unwrap();
|
||||||
|
// Select the best streams
|
||||||
|
let (video, audio) = player.select_video_audio_stream(&StreamFilter::default());
|
||||||
|
|
||||||
|
// Open mpv player
|
||||||
|
let mut args = vec![video.expect("no video stream").url.to_owned()];
|
||||||
|
if let Some(audio) = audio {
|
||||||
|
args.push(format!("--audio-file={}", audio.url));
|
||||||
|
}
|
||||||
|
Command::new("mpv").args(args).output().unwrap();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get a playlist
|
||||||
|
|
||||||
|
```rust ignore
|
||||||
|
use rustypipe::client::RustyPipe
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Create a client
|
||||||
|
let rp = RustyPipe::new();
|
||||||
|
// Get the playlist
|
||||||
|
let playlist = rp
|
||||||
|
.query()
|
||||||
|
.playlist("PL2_OBreMn7FrsiSW0VDZjdq0xqUKkZYHT")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
// Get all items (maximum: 1000)
|
||||||
|
playlist.videos.extend_limit(rp.query(), 1000).await.unwrap();
|
||||||
|
|
||||||
|
println!("Name: {}", playlist.name);
|
||||||
|
println!("Author: {}", playlist.channel.unwrap().name);
|
||||||
|
println!("Last update: {}", playlist.last_update.unwrap());
|
||||||
|
|
||||||
|
playlist
|
||||||
|
.videos
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.for_each(|v| println!("[{}] {} ({}s)", v.id, v.name, v.length));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Name: Homelab
|
||||||
|
Author: Jeff Geerling
|
||||||
|
Last update: 2023-05-04
|
||||||
|
[cVWF3u-y-Zg] I put a computer in my computer (720s)
|
||||||
|
[ecdm3oA-QdQ] 6-in-1: Build a 6-node Ceph cluster on this Mini ITX Motherboard (783s)
|
||||||
|
[xvE4HNJZeIg] Scrapyard Server: Fastest all-SSD NAS! (733s)
|
||||||
|
[RvnG-ywF6_s] Nanosecond clock sync with a Raspberry Pi (836s)
|
||||||
|
[R2S2RMNv7OU] I made the Petabyte Raspberry Pi even faster! (572s)
|
||||||
|
[FG--PtrDmw4] Hiding Macs in my Rack! (515s)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get a channel
|
||||||
|
|
||||||
|
```rust ignore
|
||||||
|
use rustypipe::client::RustyPipe
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Create a client
|
||||||
|
let rp = RustyPipe::new();
|
||||||
|
// Get the channel
|
||||||
|
let channel = rp
|
||||||
|
.query()
|
||||||
|
.channel_videos("UCl2mFZoRqjw_ELax4Yisf6w")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Name: {}", channel.name);
|
||||||
|
println!("Description: {}", channel.description);
|
||||||
|
println!("Subscribers: {}", channel.subscriber_count.unwrap());
|
||||||
|
|
||||||
|
channel
|
||||||
|
.content
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.for_each(|v| println!("[{}] {} ({}s)", v.id, v.name, v.length.unwrap()));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Name: Louis Rossmann
|
||||||
|
Description: I discuss random things of interest to me. (...)
|
||||||
|
Subscribers: 1780000
|
||||||
|
[qBHgJx_rb8E] Introducing Rossmann senior, a genuine fossil 😃 (122s)
|
||||||
|
[TmV8eAtXc3s] Am I wrong about CompTIA? (592s)
|
||||||
|
[CjOJJc1qzdY] How FUTO projects loosen Google's grip on your life! (588s)
|
||||||
|
[0A10JtkkL9A] a private moment between a man and his kitten (522s)
|
||||||
|
[zbHq5_1Cd5U] Is Texas mandating auto repair shops use OEM parts? SB1083 analysis & breakdown; tldr, no. (645s)
|
||||||
|
[6Fv8bd9ICb4] Who owns this? (199s)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
name = "rustypipe-cli"
|
name = "rustypipe-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
authors = ["ThetaDev <t.testboy@gmail.com>"]
|
||||||
|
license = "GPL-3.0"
|
||||||
|
description = "CLI for RustyPipe - download videos and extract data from YouTube / YouTube Music"
|
||||||
|
keywords = ["youtube", "video", "music"]
|
||||||
|
categories = ["multimedia"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["rustls-tls-native-roots"]
|
default = ["rustls-tls-native-roots"]
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ pub enum ABTest {
|
||||||
TrendsPageHeaderRenderer = 5,
|
TrendsPageHeaderRenderer = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TESTS_TO_RUN: [ABTest; 1] = [ABTest::TrendsVideoTab];
|
const TESTS_TO_RUN: [ABTest; 2] = [ABTest::TrendsVideoTab, ABTest::TrendsPageHeaderRenderer];
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ABTestRes {
|
pub struct ABTestRes {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use path_macro::path;
|
||||||
use rustypipe::{
|
use rustypipe::{
|
||||||
client::{ClientType, RustyPipe, RustyPipeQuery},
|
client::{ClientType, RustyPipe, RustyPipeQuery},
|
||||||
model::AlbumType,
|
model::AlbumType,
|
||||||
param::{locale::LANGUAGES, Language},
|
param::{Language, LANGUAGES},
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use once_cell::sync::Lazy;
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery};
|
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery};
|
||||||
use rustypipe::param::{locale::LANGUAGES, Language};
|
use rustypipe::param::{Language, LANGUAGES};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::{Channel, ContinuationResponse};
|
use crate::model::{Channel, ContinuationResponse};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use futures::{stream, StreamExt};
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use rustypipe::{
|
use rustypipe::{
|
||||||
client::RustyPipe,
|
client::RustyPipe,
|
||||||
param::{locale::LANGUAGES, Language},
|
param::{Language, LANGUAGES},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use futures::{stream, StreamExt};
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use rustypipe::{
|
use rustypipe::{
|
||||||
client::{ClientType, RustyPipe, RustyPipeQuery},
|
client::{ClientType, RustyPipe, RustyPipeQuery},
|
||||||
param::{locale::LANGUAGES, Language},
|
param::{Language, LANGUAGES},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
name = "rustypipe-downloader"
|
name = "rustypipe-downloader"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
authors = ["ThetaDev <t.testboy@gmail.com>"]
|
||||||
|
license = "GPL-3.0"
|
||||||
|
description = "Downloader extension for RustyPipe"
|
||||||
|
keywords = ["youtube", "video", "music"]
|
||||||
|
categories = ["multimedia"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["default-tls"]
|
default = ["default-tls"]
|
||||||
|
|
|
||||||
23
src/cache.rs
23
src/cache.rs
|
|
@ -1,4 +1,19 @@
|
||||||
//! Persistent cache storage
|
//! # Persistent cache storage
|
||||||
|
//!
|
||||||
|
//! RustyPipe caches some information fetched from YouTube: specifically
|
||||||
|
//! the client versions and the JavaScript code used to deobfuscate the stream URLs.
|
||||||
|
//!
|
||||||
|
//! Without a persistent cache storage, this information would have to be re-fetched
|
||||||
|
//! with every new instantiation of the client. This would make operation a lot slower,
|
||||||
|
//! especially with CLI applications. For this reason, persisting the cache between
|
||||||
|
//! program executions is recommended.
|
||||||
|
//!
|
||||||
|
//! Since there are many diferent ways to store this data (Text file, SQL, Redis, etc),
|
||||||
|
//! RustyPipe allows you to plug in your own cache storage by implementing the
|
||||||
|
//! [`CacheStorage`] trait.
|
||||||
|
//!
|
||||||
|
//! RustyPipe already comes with the [`FileStorage`] implementation which stores
|
||||||
|
//! the cache as a JSON file.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
|
|
@ -9,14 +24,16 @@ use log::error;
|
||||||
|
|
||||||
pub(crate) const DEFAULT_CACHE_FILE: &str = "rustypipe_cache.json";
|
pub(crate) const DEFAULT_CACHE_FILE: &str = "rustypipe_cache.json";
|
||||||
|
|
||||||
|
/// Cache storage trait
|
||||||
|
///
|
||||||
/// RustyPipe has to cache some information fetched from YouTube: specifically
|
/// RustyPipe has to cache some information fetched from YouTube: specifically
|
||||||
/// the client versions and the JavaScript code used to deobfuscate the stream URLs.
|
/// the client versions and the JavaScript code used to deobfuscate the stream URLs.
|
||||||
///
|
///
|
||||||
/// This trait is used to abstract the cache storage behavior so you can store
|
/// This trait is used to abstract the cache storage behavior so you can store
|
||||||
/// cache data in your preferred way (File, SQL, Redis, etc).
|
/// cache data in your preferred way (File, SQL, Redis, etc).
|
||||||
///
|
///
|
||||||
/// The cache is read when building the [`crate::client::RustyPipe`] client and updated
|
/// The cache is read when building the [`RustyPipe`](crate::client::RustyPipe)
|
||||||
/// whenever additional data is fetched.
|
/// client and updated whenever additional data is fetched.
|
||||||
pub trait CacheStorage: Sync + Send {
|
pub trait CacheStorage: Sync + Send {
|
||||||
/// Write the given string to the cache
|
/// Write the given string to the cache
|
||||||
fn write(&self, data: &str);
|
fn write(&self, data: &str);
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ impl RustyPipeQuery {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the specified video tab 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>>(
|
pub async fn channel_videos_tab<S: AsRef<str>>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
|
|
@ -108,7 +108,7 @@ impl RustyPipeQuery {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a ordered list of videos from the specified tab 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>>(
|
pub async fn channel_videos_tab_order<S: AsRef<str>>(
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ impl RustyPipeQuery {
|
||||||
///
|
///
|
||||||
/// Fetching RSS feeds is a lot faster than querying the InnerTube API, so this method is great
|
/// Fetching RSS feeds is a lot faster than querying the InnerTube API, so this method is great
|
||||||
/// 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.
|
||||||
pub async fn channel_rss<S: AsRef<str>>(&self, channel_id: S) -> Result<ChannelRss, Error> {
|
pub async fn channel_rss<S: AsRef<str>>(&self, channel_id: S) -> Result<ChannelRss, Error> {
|
||||||
let channel_id = channel_id.as_ref();
|
let channel_id = channel_id.as_ref();
|
||||||
let url = format!(
|
let url = format!(
|
||||||
|
|
|
||||||
|
|
@ -214,9 +214,9 @@ static CLIENT_VERSION_REGEXES: Lazy<[Regex; 1]> =
|
||||||
|
|
||||||
/// The RustyPipe client used to access YouTube's API
|
/// The RustyPipe client used to access YouTube's API
|
||||||
///
|
///
|
||||||
/// RustyPipe includes an `Arc` internally, so if you are using the client
|
/// RustyPipe uses an [`Arc`] internally, so if you are using the client
|
||||||
/// at multiple locations, you can just clone it. Note that options (lang/country/report)
|
/// at multiple locations, you can just clone it. Note that query options
|
||||||
/// are not shared between clones.
|
/// (lang/country/report/visitor data) are not shared between clones.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RustyPipe {
|
pub struct RustyPipe {
|
||||||
inner: Arc<RustyPipeRef>,
|
inner: Arc<RustyPipeRef>,
|
||||||
|
|
@ -268,10 +268,78 @@ impl<T> DefaultOpt<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RustyPipe query object
|
/// # RustyPipe query
|
||||||
///
|
///
|
||||||
/// Contains a reference to the RustyPipe client as well as query-specific
|
/// ## Queries
|
||||||
/// options (e.g. language preference).
|
///
|
||||||
|
/// ### YouTube
|
||||||
|
///
|
||||||
|
/// - **Video**
|
||||||
|
/// - [`player`](RustyPipeQuery::player)
|
||||||
|
/// - [`video_details`](RustyPipeQuery::video_details)
|
||||||
|
/// - [`video_comments`](RustyPipeQuery::video_comments)
|
||||||
|
/// - **Channel**
|
||||||
|
/// - [`channel_videos`](RustyPipeQuery::channel_videos)
|
||||||
|
/// - [`channel_videos_order`](RustyPipeQuery::channel_videos_order)
|
||||||
|
/// - [`channel_videos_tab`](RustyPipeQuery::channel_videos_tab)
|
||||||
|
/// - [`channel_videos_tab_order`](RustyPipeQuery::channel_videos_tab_order)
|
||||||
|
/// - [`channel_playlists`](RustyPipeQuery::channel_playlists)
|
||||||
|
/// - [`channel_search`](RustyPipeQuery::channel_search)
|
||||||
|
/// - [`channel_info`](RustyPipeQuery::channel_info)
|
||||||
|
/// - [`channel_rss`](RustyPipeQuery::channel_rss) (🔒 Feature `rss`)
|
||||||
|
/// - **Playlist** [`playlist`](RustyPipeQuery::playlist)
|
||||||
|
/// - **Search**
|
||||||
|
/// - [`search`](RustyPipeQuery::search)
|
||||||
|
/// - [`search_filter`](RustyPipeQuery::search_filter)
|
||||||
|
/// - [`search_suggestion`](RustyPipeQuery::search_suggestion)
|
||||||
|
/// - **Trending** [`trending`](RustyPipeQuery::trending)
|
||||||
|
/// - **Resolver** (convert URLs and strings to YouTube IDs)
|
||||||
|
/// - [`resolve_url`](RustyPipeQuery::resolve_url)
|
||||||
|
/// - [`resolve_string`](RustyPipeQuery::resolve_string)
|
||||||
|
///
|
||||||
|
/// ### YouTube Music
|
||||||
|
///
|
||||||
|
/// - **Playlist** [`music_playlist`](RustyPipeQuery::music_playlist)
|
||||||
|
/// - **Album** [`music_album`](RustyPipeQuery::music_album)
|
||||||
|
/// - **Artist** [`music_artist`](RustyPipeQuery::music_artist)
|
||||||
|
/// - **Search**
|
||||||
|
/// - [`music_search`](RustyPipeQuery::music_search)
|
||||||
|
/// - [`music_search_tracks`](RustyPipeQuery::music_search_tracks)
|
||||||
|
/// - [`music_search_videos`](RustyPipeQuery::music_search_videos)
|
||||||
|
/// - [`music_search_albums`](RustyPipeQuery::music_search_albums)
|
||||||
|
/// - [`music_search_artists`](RustyPipeQuery::music_search_artists)
|
||||||
|
/// - [`music_search_playlists`](RustyPipeQuery::music_search_playlists)
|
||||||
|
/// - [`music_search_playlists_filter`](RustyPipeQuery::music_search_playlists_filter)
|
||||||
|
/// - [`music_search_suggestion`](RustyPipeQuery::music_search_suggestion)
|
||||||
|
/// - **Radio**
|
||||||
|
/// - [`music_radio`](RustyPipeQuery::music_radio)
|
||||||
|
/// - [`music_radio_playlist`](RustyPipeQuery::music_radio_playlist)
|
||||||
|
/// - [`music_radio_track`](RustyPipeQuery::music_radio_track)
|
||||||
|
/// - **Track details**
|
||||||
|
/// - [`music_details`](RustyPipeQuery::music_details)
|
||||||
|
/// - [`music_lyrics`](RustyPipeQuery::music_lyrics)
|
||||||
|
/// - [`music_related`](RustyPipeQuery::music_related)
|
||||||
|
/// - **Moods/Genres**
|
||||||
|
/// - [`music_genres`](RustyPipeQuery::music_genres)
|
||||||
|
/// - [`music_genre`](RustyPipeQuery::music_genre)
|
||||||
|
/// - **Charts** [`music_charts`](RustyPipeQuery::music_charts)
|
||||||
|
/// - **New**
|
||||||
|
/// - [`music_new_albums`](RustyPipeQuery::music_new_albums)
|
||||||
|
/// - [`music_new_videos`](RustyPipeQuery::music_new_videos)
|
||||||
|
///
|
||||||
|
/// ## Options
|
||||||
|
///
|
||||||
|
/// You can set the language, country and visitor data cookie for individual requests.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use rustypipe::client::RustyPipe;
|
||||||
|
/// let rp = RustyPipe::new();
|
||||||
|
/// rp.query()
|
||||||
|
/// .country(rustypipe::param::Country::De)
|
||||||
|
/// .lang(rustypipe::param::Language::De)
|
||||||
|
/// .visitor_data("CgthZVRCd1dkbTlRWSj3v_miBg%3D%3D")
|
||||||
|
/// .player("ZeerrnuLi5E");
|
||||||
|
/// ```
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RustyPipeQuery {
|
pub struct RustyPipeQuery {
|
||||||
client: RustyPipe,
|
client: RustyPipe,
|
||||||
|
|
@ -361,9 +429,9 @@ impl Default for RustyPipeBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RustyPipeBuilder {
|
impl RustyPipeBuilder {
|
||||||
/// Constructs a new `RustyPipeBuilder`.
|
/// Return a new `RustyPipeBuilder`.
|
||||||
///
|
///
|
||||||
/// This is the same as `RustyPipe::builder()`
|
/// This is the same as [`RustyPipe::builder`]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
RustyPipeBuilder {
|
RustyPipeBuilder {
|
||||||
default_opts: RustyPipeOpts::default(),
|
default_opts: RustyPipeOpts::default(),
|
||||||
|
|
@ -376,7 +444,7 @@ impl RustyPipeBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new, configured RustyPipe instance.
|
/// Return a new, configured RustyPipe instance.
|
||||||
pub fn build(self) -> RustyPipe {
|
pub fn build(self) -> RustyPipe {
|
||||||
let mut client_builder = ClientBuilder::new()
|
let mut client_builder = ClientBuilder::new()
|
||||||
.user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned()))
|
.user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned()))
|
||||||
|
|
@ -517,6 +585,7 @@ impl RustyPipeBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the language parameter used when accessing the YouTube API.
|
/// Set the language parameter used when accessing the YouTube API.
|
||||||
|
///
|
||||||
/// This will change multilanguage video titles, descriptions and textual dates
|
/// This will change multilanguage video titles, descriptions and textual dates
|
||||||
///
|
///
|
||||||
/// **Default value**: `Language::En` (English)
|
/// **Default value**: `Language::En` (English)
|
||||||
|
|
@ -528,6 +597,7 @@ impl RustyPipeBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the country parameter used when accessing the YouTube API.
|
/// Set the country parameter used when accessing the YouTube API.
|
||||||
|
///
|
||||||
/// This will change trends and recommended content.
|
/// This will change trends and recommended content.
|
||||||
///
|
///
|
||||||
/// **Default value**: `Country::Us` (USA)
|
/// **Default value**: `Country::Us` (USA)
|
||||||
|
|
@ -539,6 +609,7 @@ impl RustyPipeBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a report on every operation.
|
/// Generate a report on every operation.
|
||||||
|
///
|
||||||
/// This should only be used for debugging.
|
/// This should only be used for debugging.
|
||||||
///
|
///
|
||||||
/// **Info**: you can set this option for individual queries, too
|
/// **Info**: you can set this option for individual queries, too
|
||||||
|
|
@ -549,6 +620,7 @@ impl RustyPipeBuilder {
|
||||||
|
|
||||||
/// Enable strict mode, causing operations to fail if there
|
/// Enable strict mode, causing operations to fail if there
|
||||||
/// are warnings during deserialization (e.g. invalid items).
|
/// are warnings during deserialization (e.g. invalid items).
|
||||||
|
///
|
||||||
/// This should only be used for testing.
|
/// This should only be used for testing.
|
||||||
///
|
///
|
||||||
/// **Info**: you can set this option for individual queries, too
|
/// **Info**: you can set this option for individual queries, too
|
||||||
|
|
@ -557,15 +629,32 @@ impl RustyPipeBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the default YouTube visitor data cookie
|
/// Set the YouTube visitor data cookie
|
||||||
|
///
|
||||||
|
/// YouTube assigns a session cookie to each user which is used for personalized
|
||||||
|
/// recommendations. By default, RustyPipe does not send this cookie to preserve
|
||||||
|
/// user privacy. For requests that mandatate the cookie, a new one is requested
|
||||||
|
/// for every query.
|
||||||
|
///
|
||||||
|
/// This option allows you to manually set the visitor data cookie of your client,
|
||||||
|
/// allowing you to get personalized recommendations or reproduce A/B tests.
|
||||||
|
///
|
||||||
|
/// Note that YouTube has a rate limit on the number of requests from a single
|
||||||
|
/// visitor, so you should not use the same vistor data cookie for batch operations.
|
||||||
|
///
|
||||||
|
/// **Info**: you can set this option for individual queries, too
|
||||||
pub fn visitor_data<S: Into<String>>(mut self, visitor_data: S) -> Self {
|
pub fn visitor_data<S: Into<String>>(mut self, visitor_data: S) -> Self {
|
||||||
self.default_opts.visitor_data = Some(visitor_data.into());
|
self.default_opts.visitor_data = Some(visitor_data.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the default YouTube visitor data cookie to an optional value
|
/// Set the YouTube visitor data cookie to an optional value
|
||||||
pub fn visitor_data_opt(mut self, visitor_data: Option<String>) -> Self {
|
///
|
||||||
self.default_opts.visitor_data = visitor_data;
|
/// see also [`RustyPipeBuilder::visitor_data`]
|
||||||
|
///
|
||||||
|
/// **Info**: you can set this option for individual queries, too
|
||||||
|
pub fn visitor_data_opt<S: Into<String>>(mut self, visitor_data: Option<S>) -> Self {
|
||||||
|
self.default_opts.visitor_data = visitor_data.map(S::into);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -579,19 +668,19 @@ impl Default for RustyPipe {
|
||||||
impl RustyPipe {
|
impl RustyPipe {
|
||||||
/// Create a new RustyPipe instance with default settings.
|
/// Create a new RustyPipe instance with default settings.
|
||||||
///
|
///
|
||||||
/// To create an instance with custom options, use `RustyPipeBuilder` instead.
|
/// To create an instance with custom options, use [`RustyPipeBuilder`] instead.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
RustyPipeBuilder::new().build()
|
RustyPipeBuilder::new().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a new `RustyPipeBuilder`.
|
/// Create a new [`RustyPipeBuilder`]
|
||||||
///
|
///
|
||||||
/// This is the same as `RustyPipeBuilder::new()`
|
/// This is the same as [`RustyPipeBuilder::new`]
|
||||||
pub fn builder() -> RustyPipeBuilder {
|
pub fn builder() -> RustyPipeBuilder {
|
||||||
RustyPipeBuilder::new()
|
RustyPipeBuilder::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a new `RustyPipeQuery`.
|
/// Create a new [`RustyPipeQuery`] to run an API request
|
||||||
pub fn query(&self) -> RustyPipeQuery {
|
pub fn query(&self) -> RustyPipeQuery {
|
||||||
RustyPipeQuery {
|
RustyPipeQuery {
|
||||||
client: self.clone(),
|
client: self.clone(),
|
||||||
|
|
@ -826,8 +915,12 @@ impl RustyPipe {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Request a new visitor data cookie from YouTube
|
||||||
|
///
|
||||||
|
/// Since the cookie is shared between YT and YTM and the YTM page loads faster,
|
||||||
|
/// we request that.
|
||||||
async fn get_visitor_data(&self) -> Result<String, Error> {
|
async fn get_visitor_data(&self) -> Result<String, Error> {
|
||||||
log::debug!("getting YTM visitor data");
|
log::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?;
|
||||||
|
|
||||||
resp.headers()
|
resp.headers()
|
||||||
|
|
@ -849,6 +942,7 @@ impl RustyPipe {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Set the language parameter used when accessing the YouTube API
|
/// Set the language parameter used when accessing the YouTube API
|
||||||
|
///
|
||||||
/// This will change multilanguage video titles, descriptions and textual dates
|
/// This will change multilanguage video titles, descriptions and textual dates
|
||||||
pub fn lang(mut self, lang: Language) -> Self {
|
pub fn lang(mut self, lang: Language) -> Self {
|
||||||
self.opts.lang = lang;
|
self.opts.lang = lang;
|
||||||
|
|
@ -856,6 +950,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the country parameter used when accessing the YouTube API.
|
/// Set the country parameter used when accessing the YouTube API.
|
||||||
|
///
|
||||||
/// This will change trends and recommended content.
|
/// This will change trends and recommended content.
|
||||||
pub fn country(mut self, country: Country) -> Self {
|
pub fn country(mut self, country: Country) -> Self {
|
||||||
self.opts.country = validate_country(country);
|
self.opts.country = validate_country(country);
|
||||||
|
|
@ -863,6 +958,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a report on every operation.
|
/// Generate a report on every operation.
|
||||||
|
///
|
||||||
/// This should only be used for debugging.
|
/// This should only be used for debugging.
|
||||||
pub fn report(mut self) -> Self {
|
pub fn report(mut self) -> Self {
|
||||||
self.opts.report = true;
|
self.opts.report = true;
|
||||||
|
|
@ -871,6 +967,7 @@ impl RustyPipeQuery {
|
||||||
|
|
||||||
/// Enable strict mode, causing operations to fail if there
|
/// Enable strict mode, causing operations to fail if there
|
||||||
/// are warnings during deserialization (e.g. invalid items).
|
/// are warnings during deserialization (e.g. invalid items).
|
||||||
|
///
|
||||||
/// This should only be used for testing.
|
/// This should only be used for testing.
|
||||||
pub fn strict(mut self) -> Self {
|
pub fn strict(mut self) -> Self {
|
||||||
self.opts.strict = true;
|
self.opts.strict = true;
|
||||||
|
|
@ -878,14 +975,27 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the YouTube visitor data cookie
|
/// Set the YouTube visitor data cookie
|
||||||
|
///
|
||||||
|
/// YouTube assigns a session cookie to each user which is used for personalized
|
||||||
|
/// recommendations. By default, RustyPipe does not send this cookie to preserve
|
||||||
|
/// user privacy. For requests that mandatate the cookie, a new one is requested
|
||||||
|
/// for every query.
|
||||||
|
///
|
||||||
|
/// This option allows you to manually set the visitor data cookie of your query,
|
||||||
|
/// allowing you to get personalized recommendations or reproduce A/B tests.
|
||||||
|
///
|
||||||
|
/// Note that YouTube has a rate limit on the number of requests from a single
|
||||||
|
/// visitor, so you should not use the same vistor data cookie for batch operations.
|
||||||
pub fn visitor_data<S: Into<String>>(mut self, visitor_data: S) -> Self {
|
pub fn visitor_data<S: Into<String>>(mut self, visitor_data: S) -> Self {
|
||||||
self.opts.visitor_data = Some(visitor_data.into());
|
self.opts.visitor_data = Some(visitor_data.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the YouTube visitor data cookie to an optional value
|
/// Set the YouTube visitor data cookie to an optional value
|
||||||
pub fn visitor_data_opt(mut self, visitor_data: Option<String>) -> Self {
|
///
|
||||||
self.opts.visitor_data = visitor_data;
|
/// see also [`RustyPipeQuery::visitor_data`]
|
||||||
|
pub fn visitor_data_opt<S: Into<String>>(mut self, visitor_data: Option<S>) -> Self {
|
||||||
|
self.opts.visitor_data = visitor_data.map(S::into);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1121,6 +1121,12 @@ impl MusicListMapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sometimes the YT Music API returns responses containing unknown items.
|
||||||
|
///
|
||||||
|
/// In this case, the response data is likely missing some fields, which leads to
|
||||||
|
/// parsing errors and wrong data being extracted.
|
||||||
|
///
|
||||||
|
/// Therefore it is safest to discard such responses and retry the request.
|
||||||
pub fn check_unknown(&self) -> Result<(), ExtractionError> {
|
pub fn check_unknown(&self) -> Result<(), ExtractionError> {
|
||||||
match self.has_unknown {
|
match self.has_unknown {
|
||||||
true => Err(ExtractionError::InvalidData("unknown YTM items".into())),
|
true => Err(ExtractionError::InvalidData("unknown YTM items".into())),
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ impl RustyPipeQuery {
|
||||||
/// from alternative YouTube frontends like Piped or Invidious.
|
/// from alternative YouTube frontends like Piped or Invidious.
|
||||||
///
|
///
|
||||||
/// The `resolve_albums` flag enables resolving YTM album URLs (e.g.
|
/// The `resolve_albums` flag enables resolving YTM album URLs (e.g.
|
||||||
/// `OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE`) to their short album id (`MPREb_GyH43gCvdM5`).
|
/// `OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE`) to their short album ids (`MPREb_GyH43gCvdM5`).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -217,7 +217,7 @@ impl RustyPipeQuery {
|
||||||
/// rp.query().resolve_string("LinusTechTips", true).await.unwrap(),
|
/// rp.query().resolve_string("LinusTechTips", true).await.unwrap(),
|
||||||
/// UrlTarget::Channel {id: "UCXuqSBlHAE6Xw-yeJA0Tunw".to_owned()}
|
/// UrlTarget::Channel {id: "UCXuqSBlHAE6Xw-yeJA0Tunw".to_owned()}
|
||||||
/// );
|
/// );
|
||||||
/// //
|
/// // Playlist
|
||||||
/// assert_eq!(
|
/// assert_eq!(
|
||||||
/// rp.query().resolve_string("PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI", true).await.unwrap(),
|
/// rp.query().resolve_string("PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI", true).await.unwrap(),
|
||||||
/// UrlTarget::Playlist {id: "PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI".to_owned()}
|
/// UrlTarget::Playlist {id: "PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI".to_owned()}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,8 @@ pub enum ExtractionError {
|
||||||
pub enum UnavailabilityReason {
|
pub enum UnavailabilityReason {
|
||||||
/// Video is age restricted.
|
/// Video is age restricted.
|
||||||
///
|
///
|
||||||
/// Age restriction may be circumvented with the [`crate::client::ClientType::TvHtml5Embed`] client.
|
/// Age restriction may be circumvented with the
|
||||||
|
/// [`ClientType::TvHtml5Embed`](crate::client::ClientType::TvHtml5Embed) client.
|
||||||
AgeRestricted,
|
AgeRestricted,
|
||||||
/// Video was deleted or censored
|
/// Video was deleted or censored
|
||||||
Deleted,
|
Deleted,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![warn(missing_docs, clippy::todo, clippy::dbg_macro)]
|
#![warn(missing_docs, clippy::todo, clippy::dbg_macro)]
|
||||||
|
|
||||||
|
//! ## Go to
|
||||||
|
//!
|
||||||
|
//! - Client ([`rustypipe::client::Rustypipe`](crate::client::RustyPipe))
|
||||||
|
//! - Query ([`rustypipe::client::RustypipeQuery`](crate::client::RustyPipeQuery))
|
||||||
|
|
||||||
mod deobfuscate;
|
mod deobfuscate;
|
||||||
mod serializer;
|
mod serializer;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
|
||||||
|
|
@ -257,15 +257,15 @@ pub struct AudioStream {
|
||||||
pub codec: AudioCodec,
|
pub codec: AudioCodec,
|
||||||
/// Number of audio channels
|
/// Number of audio channels
|
||||||
pub channels: Option<u8>,
|
pub channels: Option<u8>,
|
||||||
/// Audio loudness for ReplayGain correction
|
/// Audio loudness for volume normalization
|
||||||
///
|
///
|
||||||
/// The track volume correction factor (0-1) can be calculated using this formula
|
/// The track volume correction factor (0-1) can be calculated using this formula
|
||||||
///
|
///
|
||||||
/// `10^(-loudness_db/20)`
|
/// `10^(-loudness_db/20)`
|
||||||
///
|
///
|
||||||
/// Note that the value is the inverse of the usual track gain parameter, i.e. a
|
/// Note that the `loudness_db` value is the inverse of the usual ReplayGain track gain
|
||||||
/// value of 6 means the volume should be reduced by 6dB and the ReplayGain track gain
|
/// parameter, i.e. a value of 6 means the volume should be reduced by 6dB and the
|
||||||
/// parameter would be -6.
|
/// track gain parameter would be -6.
|
||||||
///
|
///
|
||||||
/// More information about ReplayGain and how to apply this infomation to audio files
|
/// More information about ReplayGain and how to apply this infomation to audio files
|
||||||
/// can be found here: <https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification>.
|
/// can be found here: <https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification>.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
//! Query parameters
|
//! # Query parameters
|
||||||
|
//!
|
||||||
|
//! This module contains structs and enums used as input parameters
|
||||||
|
//! for the functions in RustyPipe.
|
||||||
|
|
||||||
|
mod locale;
|
||||||
mod stream_filter;
|
mod stream_filter;
|
||||||
|
|
||||||
pub mod locale;
|
|
||||||
pub mod search_filter;
|
pub mod search_filter;
|
||||||
|
|
||||||
pub use locale::{Country, Language};
|
pub use locale::{Country, Language, COUNTRIES, LANGUAGES};
|
||||||
pub use stream_filter::StreamFilter;
|
pub use stream_filter::StreamFilter;
|
||||||
|
|
||||||
/// Channel video tab
|
/// Channel video tab
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
//! [string resolver](crate::client::RustyPipeQuery::resolve_string) is great for handling
|
//! [string resolver](crate::client::RustyPipeQuery::resolve_string) is great for handling
|
||||||
//! arbitrary input and returns a [`UrlTarget`](crate::model::UrlTarget) enum that tells you
|
//! arbitrary input and returns a [`UrlTarget`](crate::model::UrlTarget) enum that tells you
|
||||||
//! whether the given URL points to a video, channel, playlist, etc.
|
//! whether the given URL points to a video, channel, playlist, etc.
|
||||||
//! - The validation functions of this module are meant vor validating concrete data (video IDs,
|
//! - The validation functions of this module are meant vor validating specific data (video IDs,
|
||||||
//! channel IDs, playlist IDs) and return [`true`] if the given input is valid
|
//! channel IDs, playlist IDs) and return [`true`] if the given input is valid
|
||||||
|
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
@ -138,7 +138,7 @@ pub fn genre_id<S: AsRef<str>>(genre_id: S) -> bool {
|
||||||
GENRE_ID_REGEX.is_match(genre_id.as_ref())
|
GENRE_ID_REGEX.is_match(genre_id.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate the given related ID
|
/// Validate the given related tracks ID
|
||||||
///
|
///
|
||||||
/// YouTube related IDs are exactly 17 characters long, start with the characters `MPTRt_`,
|
/// YouTube related IDs are exactly 17 characters long, start with the characters `MPTRt_`,
|
||||||
/// followed by 11 of these characters: `A-Za-z0-9_-`.
|
/// followed by 11 of these characters: `A-Za-z0-9_-`.
|
||||||
|
|
|
||||||
Reference in a new issue