304 lines
11 KiB
Markdown
304 lines
11 KiB
Markdown
# 
|
|
|
|
[](LICENSE)
|
|
[](https://docs.rs/rustypipe)
|
|
|
|
RustyPipe is a fully featured Rust client for the public YouTube / YouTube Music API
|
|
(Innertube), inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
|
It lets you fetch videos, streams, playlists, channels, search results, music metadata
|
|
and more, without an API key and without the official YouTube SDK.
|
|
|
|
> **About this fork**
|
|
>
|
|
> This is a maintained fork of the upstream
|
|
> [RustyPipe by ThetaDev](https://codeberg.org/ThetaDev/rustypipe) (GPL-3.0).
|
|
> All credit for the original library goes to the upstream author and contributors.
|
|
>
|
|
> The fork exists to keep the player pipeline working against YouTube's frequently
|
|
> rotating `player.js`. The notable changes (see the `sulkta-sig-port` branch and the
|
|
> `*-sulkta` tags) are:
|
|
>
|
|
> - **Soft-fail signature deobfuscation** — when YouTube ships a `player.js` shape the
|
|
> built-in regexes don't recognise, the player path no longer aborts. Only the
|
|
> load-bearing signature timestamp is treated as required; the actual sig/nsig
|
|
> functions are best-effort and a warning is logged instead of failing the whole call.
|
|
> - **iOS-first default client order** — the iOS Innertube path returns pre-signed
|
|
> stream URLs (no cipher/throttling params) and needs neither device attestation nor
|
|
> signature deobfuscation, so it is the most reliable "just works" default. Android is
|
|
> kept in rotation only when BotGuard / PO-token signing is wired up.
|
|
>
|
|
> If you just want the canonical library, use upstream. If you want the resilience
|
|
> patches above, this fork is a drop-in replacement at the same API.
|
|
|
|
## Features
|
|
|
|
### YouTube
|
|
|
|
- **Player** (video/audio streams, subtitles)
|
|
- **VideoDetails** (metadata, comments, recommended videos)
|
|
- **Playlist**
|
|
- **Channel** (videos, shorts, livestreams, playlists, info, search)
|
|
- **ChannelRSS**
|
|
- **Search** (with filters)
|
|
- **Search suggestions**
|
|
- **Trending**
|
|
- **URL resolver**
|
|
- **Subscriptions**
|
|
- **Playback history**
|
|
|
|
### YouTube Music
|
|
|
|
- **Playlist**
|
|
- **Album**
|
|
- **Artist**
|
|
- **Search**
|
|
- **Search suggestions**
|
|
- **Radio**
|
|
- **Track details** (lyrics, recommendations)
|
|
- **Moods/Genres**
|
|
- **Charts**
|
|
- **New** (albums, music videos)
|
|
- **Saved items**
|
|
- **Playback history**
|
|
|
|
## Getting started
|
|
|
|
The RustyPipe library works as follows: first you instantiate a RustyPipe client. You can
|
|
either create it with default options or use `RustyPipe::builder()` to customize it.
|
|
|
|
For fetching data you start with a new RustyPipe query object (`rp.query()`). The query
|
|
object holds options for an individual query (e.g. content language or country). You can
|
|
adjust these options with setter methods. Finally call your query method to fetch the data
|
|
you need.
|
|
|
|
All query methods are async; you need the tokio runtime to execute them.
|
|
|
|
```rust ignore
|
|
let rp = RustyPipe::new();
|
|
let rp = RustyPipe::builder().storage_dir("/app/data").build().unwrap();
|
|
let channel = rp.query().lang(Language::De).channel_videos("UCl2mFZoRqjw_ELax4Yisf6w").await.unwrap();
|
|
```
|
|
|
|
Here are a few examples to get you started:
|
|
|
|
### Cargo.toml
|
|
|
|
```toml
|
|
[dependencies]
|
|
rustypipe = "0.11"
|
|
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));
|
|
}
|
|
```
|
|
|
|
### 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()));
|
|
}
|
|
```
|
|
|
|
## Building and testing
|
|
|
|
RustyPipe is a standard Cargo workspace (the library plus the `rustypipe-cli` and
|
|
`rustypipe-downloader` companion crates).
|
|
|
|
```sh
|
|
# Build everything
|
|
cargo build --workspace
|
|
|
|
# Run the offline unit + snapshot tests (no network access required)
|
|
cargo test --workspace
|
|
|
|
# Run the fork's end-to-end smoke tests against live YouTube
|
|
# (network access required; some clients may be rate-limited from datacenter IPs)
|
|
cargo test --test sulkta_smoke -- --nocapture
|
|
```
|
|
|
|
## Crate features
|
|
|
|
Some features of RustyPipe are gated behind Cargo features to avoid compiling unneeded
|
|
dependencies.
|
|
|
|
- `rss` Fetch a channel's RSS feed, which is faster than fetching the channel page
|
|
- `userdata` Add functions to fetch YouTube user data (watch history, subscriptions,
|
|
music library)
|
|
|
|
You can also choose the TLS library used for making web requests using the same features
|
|
as the reqwest crate (`default-tls`, `native-tls`, `native-tls-alpn`,
|
|
`native-tls-vendored`, `rustls-tls-webpki-roots`, `rustls-tls-native-roots`).
|
|
|
|
## Cache storage
|
|
|
|
The RustyPipe cache holds the current version numbers for all clients, the JavaScript
|
|
code used to deobfuscate video URLs and the authentication token/cookies. Never share
|
|
the contents of the cache if you are using authentication.
|
|
|
|
By default the cache is written to a JSON file named `rustypipe_cache.json` in the
|
|
current working directory. This path can be changed with the `storage_dir` option of the
|
|
RustyPipeBuilder. The RustyPipe CLI stores its cache in the userdata folder. The full
|
|
path on Linux is `~/.local/share/rustypipe/rustypipe_cache.json`.
|
|
|
|
You can integrate your own cache storage backend (e.g. database storage) by implementing
|
|
the `CacheStorage` trait.
|
|
|
|
## Reports
|
|
|
|
RustyPipe has a builtin error reporting system. If a YouTube response cannot be
|
|
deserialized or parsed, the original response data along with some request metadata is
|
|
written to a JSON file in the folder `rustypipe_reports`, located in RustyPipe's storage
|
|
directory (current folder by default, `~/.local/share/rustypipe` for the CLI).
|
|
|
|
When submitting a bug report, you can share this report to help resolve the issue.
|
|
|
|
RustyPipe reports come in 3 severity levels:
|
|
|
|
- DBG (no error occurred, report creation was enabled by the `RustyPipeQuery::report`
|
|
query option)
|
|
- WRN (parts of the response could not be deserialized/parsed, response data may be
|
|
incomplete)
|
|
- ERR (entire response could not be deserialized/parsed, RustyPipe returned an error)
|
|
|
|
## PO tokens
|
|
|
|
Since August 2024 YouTube requires PO tokens to access streams from web-based clients
|
|
(Desktop, Mobile). Otherwise streams will return a 403 error.
|
|
|
|
Generating PO tokens requires a simulated browser environment, which would be too large
|
|
to include in RustyPipe directly.
|
|
|
|
Therefore, PO token generation is handled by a separate CLI application
|
|
([rustypipe-botguard](https://codeberg.org/ThetaDev/rustypipe-botguard)) which is called
|
|
by the RustyPipe crate. RustyPipe automatically detects the rustypipe-botguard binary if
|
|
it is located in PATH or the current working directory. If your rustypipe-botguard binary
|
|
is located at a different path, you can specify it with the `.botguard_bin(path)` option.
|
|
|
|
## Authentication
|
|
|
|
RustyPipe supports authenticating with your YouTube account to access
|
|
age-restricted/private videos and user information. There are 2 supported authentication
|
|
methods: OAuth and cookies.
|
|
|
|
To execute a query with authentication, use the `.authenticated()` query option. This
|
|
option is enabled by default for queries that always require authentication like fetching
|
|
user data. RustyPipe may automatically use authentication in case a video is
|
|
age-restricted or your IP address is banned by YouTube. If you never want to use
|
|
authentication, set the `.unauthenticated()` query option.
|
|
|
|
### OAuth
|
|
|
|
OAuth is the authentication method used by the YouTube TV client. It is more
|
|
user-friendly than extracting cookies, however it only works with the TV client. This
|
|
means that you can only fetch videos and not access any user data.
|
|
|
|
To login using OAuth, you first have to get a new device code using the
|
|
`rp.user_auth_get_code()` function. You can then enter the code on
|
|
<https://google.com/device> and log in with your Google account. After generating the
|
|
code, you can call the `rp.user_auth_wait_for_login()` function which waits until the
|
|
user has logged in and stores the authentication token in the cache.
|
|
|
|
### Cookies
|
|
|
|
Authenticating with cookies allows you to use the functionality of the YouTube/YouTube
|
|
Music Desktop client. You can fetch your subscribed channels, playlists and your music
|
|
collection. You can also fetch videos using the Desktop client, including private videos,
|
|
as long as you have access to them.
|
|
|
|
To authenticate with cookies you have to log into YouTube in a fresh browser session
|
|
(open Incognito/Private mode). Then extract the cookies from the developer tools or by
|
|
using a browser extension. Close the browser window after extracting the cookies to
|
|
prevent YouTube from rotating the cookies.
|
|
|
|
You can then add the cookies to your RustyPipe client using the `user_auth_set_cookie` or
|
|
`user_auth_set_cookie_txt` function. The cookies are stored in the cache file. To log out,
|
|
use the function `user_auth_remove_cookie`.
|
|
|
|
## Contributing
|
|
|
|
Issues and pull requests are welcome. Please keep changes focused, run
|
|
`cargo fmt`, `cargo clippy --workspace` and `cargo test --workspace` before submitting,
|
|
and include a snapshot/unit test for any new parsing behaviour. If your change concerns
|
|
the player or signature pipeline, the live smoke tests (`cargo test --test sulkta_smoke`)
|
|
are a useful sanity check.
|
|
|
|
## License
|
|
|
|
RustyPipe is licensed under the **GNU General Public License v3.0 or later**
|
|
(GPL-3.0-or-later). See [LICENSE](LICENSE) for the full text. As a fork of the upstream
|
|
RustyPipe project, this repository preserves the original license and attribution.
|
|
|
|
This project is not affiliated with, sponsored by, or endorsed by YouTube or Google.
|