├── .gitignore ├── src ├── youtubei │ ├── mod.rs │ ├── resolve_url.rs │ └── browse.rs ├── api │ ├── mod.rs │ ├── types.rs │ ├── error.rs │ └── handlers.rs ├── youtube │ ├── mod.rs │ ├── playlist_items.rs │ ├── subscriptions.rs │ ├── videos.rs │ └── channels.rs ├── main.rs ├── errors.rs └── models.rs ├── static ├── screenshot_1.png ├── screenshot_2.png ├── screenshot_3.png ├── screenshot_4.png └── index.html ├── Cargo.toml ├── README.md ├── Dockerfile └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | docker-compose.yml -------------------------------------------------------------------------------- /src/youtubei/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod browse; 2 | pub mod resolve_url; -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | mod handlers; 2 | mod types; 3 | mod error; 4 | 5 | pub use handlers::create_router; -------------------------------------------------------------------------------- /static/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddd/youtube-lookup/HEAD/static/screenshot_1.png -------------------------------------------------------------------------------- /static/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddd/youtube-lookup/HEAD/static/screenshot_2.png -------------------------------------------------------------------------------- /static/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddd/youtube-lookup/HEAD/static/screenshot_3.png -------------------------------------------------------------------------------- /static/screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddd/youtube-lookup/HEAD/static/screenshot_4.png -------------------------------------------------------------------------------- /src/youtube/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod channels; 2 | pub mod videos; 3 | pub mod subscriptions; 4 | pub mod playlist_items; -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod youtubei; 2 | mod youtube; 3 | mod models; 4 | mod errors; 5 | mod api; 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | let app = api::create_router(); 10 | 11 | let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 12 | println!("Server starting on http://0.0.0.0:3000"); 13 | 14 | axum::serve(listener, app).await.unwrap(); 15 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "youtube-lookup" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | tracing = "0.1.40" 8 | tracing-subscriber = { version = "0.3.18", features = ["json"] } 9 | tracing-appender = "0.2.3" 10 | serde = { version = "1.0", features = ["derive"] } 11 | serde_json = "1.0.132" 12 | tokio = { version = "1.41.0", features = ["full"] } 13 | async-channel = "2.3.1" 14 | thiserror = "2.0.9" 15 | hyper = "1.5.0" 16 | hyper-tls = "0.6.0" 17 | hyper-util = { version = "0.1.9", features = ["full"] } 18 | native-tls = "0.2.12" 19 | rand = "0.8.5" 20 | http-body-util = "0.1.2" 21 | prost = "0.13.4" 22 | reqwest = { version = "0.12.9", features = ["json", "native-tls"] } 23 | tempfile = "3.14.0" 24 | parking_lot = "0.12.3" 25 | chrono = "0.4.39" 26 | axum = "0.7.9" 27 | dotenvy = "0.15" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # youtube-lookup 3 | 4 | ![Windows of Ken](./static/screenshot_2.png) 5 | 6 | ## Description 7 | 8 | youtube-lookup is a Rust tool to lookup a channel by the **custom url**, **handle**, **username**, **vanity** and **channel ID** and provide as much metadata as possible 9 | 10 | **It supports:** 11 | - channel/conditional redirect detection 12 | - subscriptions 13 | - blocked countries list 14 | 15 | ... and so much more! 16 | 17 | 18 | ## Installation (docker) 19 | 20 | 1. Update `.env` with your YouTube Data API key 21 | 22 | ``` 23 | $ echo "API_KEY=your_youtube_api_key_here" > .env 24 | ``` 25 | 26 | 2. Build the docker container 27 | 28 | ``` 29 | $ sudo docker build -t youtube-lookup . 30 | ``` 31 | 32 | 3. Run docker container 33 | 34 | ``` 35 | $ sudo docker run --env-file .env -p 3000:3000 youtube-lookup 36 | ``` 37 | 38 | The frontend & API should be running on `0.0.0.0:3000` 39 | 40 | ## More Screenshots 41 | 42 | ![Jason Urgo](./static/screenshot_3.png) 43 | 44 | ![1](./static/screenshot_1.png) 45 | 46 | ![Future Proof](./static/screenshot_4.png) -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | use hyper::StatusCode; 3 | use std::error::Error; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum YouTubeError { 7 | #[error("Account is closed")] 8 | AccountClosed, 9 | #[error("Account is terminated")] 10 | AccountTerminated, 11 | #[error("Subscriptions are private")] 12 | SubscriptionsPrivate, 13 | #[error("Not found")] 14 | NotFound, 15 | #[error("Ratelimited")] 16 | Ratelimited, 17 | #[error("Unauthorized")] 18 | Unauthorized, 19 | #[error("Forbidden")] 20 | Forbidden, 21 | #[error("Internal server error")] 22 | InternalServerError, 23 | #[error("Unknown Status Code")] 24 | UnknownStatusCode(StatusCode), 25 | #[error("Parse error")] 26 | ParseError(String), 27 | #[error("HTTP error: {0}")] 28 | HttpError(#[from] hyper::Error), 29 | #[error("Legacy HTTP error: {0}")] 30 | LegacyHttpError(#[from] hyper_util::client::legacy::Error), 31 | #[error("Protobuf error: {0}")] 32 | ProtobufError(#[from] prost::DecodeError), 33 | #[error("Other error: {0}")] 34 | Other(Box), 35 | } -------------------------------------------------------------------------------- /src/api/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use reqwest::Client; 3 | use crate::models::{Video, Subscription, Channel}; 4 | 5 | pub struct AppState { 6 | pub client: Client, 7 | } 8 | 9 | #[derive(Debug, Deserialize)] 10 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 11 | pub enum LookupType { 12 | CustomUrl, 13 | Vanity, 14 | Username, 15 | Handle, 16 | ChannelId, 17 | } 18 | 19 | #[derive(Debug, Deserialize)] 20 | pub struct ChannelLookupRequest { 21 | pub r#type: LookupType, 22 | pub id: String, 23 | } 24 | 25 | #[derive(Debug, Serialize)] 26 | pub struct ChannelLookupResponse { 27 | pub channel: Channel, 28 | pub redirect_url: Option, 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | pub struct PaginatedRequest { 33 | pub id: String, 34 | pub page_token: Option, 35 | } 36 | 37 | #[derive(Debug, Serialize)] 38 | pub struct PlaylistItemsResponse { 39 | pub items: Vec