diff --git a/Cargo.lock b/Cargo.lock index 821e2a1..4b8d63c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -530,6 +530,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -713,9 +723,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" +checksum = "aaac441002f822bc9705a681810a4dd2963094b9ca0ddc41cb963a4c189189ea" dependencies = [ "aho-corasick", "memchr", @@ -725,9 +735,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" +checksum = "5011c7e263a695dc8ca064cddb722af1be54e517a280b12a5356f98366899e5d" dependencies = [ "aho-corasick", "memchr", @@ -736,9 +746,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3cbb081b9784b07cceb8824c8583f86db4814d172ab043f3c23f7dc600bf83d" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" @@ -760,6 +770,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -788,9 +799,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.18" +version = "0.38.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" +checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" dependencies = [ "bitflags 2.4.0", "errno", @@ -845,18 +856,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", @@ -1070,7 +1081,7 @@ dependencies = [ [[package]] name = "tootube" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "clap", @@ -1091,20 +1102,19 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] @@ -1115,6 +1125,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -1159,6 +1178,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 437b0bc..7d61b51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "tootube" authors = ["VC "] -version = "0.3.0" +version = "0.3.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = { version = "^0.11", features = ["json", "stream"] } +reqwest = { version = "^0.11", features = ["json", "stream", "multipart"] } tokio = { version = "^1", features = ["full"] } clap = "^4" serde = { version = "1.0", features = ["derive"] } diff --git a/README.md b/README.md index bfbe526..d4589ac 100644 --- a/README.md +++ b/README.md @@ -8,48 +8,45 @@ This is an early prototype not really suited for production purposes for now: * it still relies way too much on pre-determined value to upload the video to YouTube * it cannot determine the playlists it needs to put them in * it cannot determine the recording date (believe me, I tried!) -* there are still a lot of static values that I would rather not have static (like the cache directory…) -* it is 100% sync, meaning it’s clearly not optimal for now and probably won’t be for the next releases -So consider this a work in progress that will slowly get better with time +So consider this a work in progress that will slowly get better with time. # What does it do exactly? -* it downloads the latest PeerTube video from an instance -* stores it in cache directory -* uploads it to YouTube +* it retrieves the latest PeerTube video download URL from an instance +* it creates a resumable upload into the target YouTube account +* it downloads/uploads the latest PeerTube video to YouTube without using a cache (stream-to-stream) # What doesn’t it do exactly? -* it cannot register the original key (see below) -* it relies on a local cache despite the fact that it might well be possible to just download from PT/upload to YT at the same time (I don’t see why not in fact) * it doesn’t retrieve ALL the original PeerTube video properties like licences, languages, categories, etc… again: early prototype # Obtain Authorization Token from Google -That the complicated part: -* create an OAuth2.0 application with authorization for Youtube DATA Api v3 Upload +The complicated part: +* create an OAuth2.0 application with authorization for Youtube DATA Api v3 Upload and Youtube DATA Api v3 (generally referenced as `../auth/youtube.upload` and `../auth/youtube`) * create a OAuth2.0 client with Desktop client You’ll need: * the `client_id` from your OAuth2.0 client * the `client_secret` from you OAuth2.0 client -Then enter in: +Create your `tootube.toml` config file: -``` -https://accounts.google.com/o/oauth2/v2/auth?client_id=XXX.apps.googleusercontent.com&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/youtube.upload&response_type=code +```toml +[peertube] +base_url="https://p.nintendojo.fr" + +[youtube] +refresh_token="" # leave empty for now +client_id="" +client_secret="" ``` -And accept that your YouTube account might be modified. You’ll get a code then; enter this `curl` post: +Then run: -``` -curl -s \ ---request POST \ ---data "code=[THE_CODE]&client_id=XXX.apps.googleusercontent.com&client_secret=[THE_CLIENT_SECRET]&redirect_uri=urn:ietf:wg:oauth:2.0:oob&grant_type=authorization_code" \ -https://accounts.google.com/o/oauth2/token +```bash +tootube register --config ``` -You’ll get a Token. The only important part is the `refresh_token` - -In your `tootube.toml` config file, put the `refresh_token`. +You’ll be then prompted with all the necessary information to register `tootube`. You’ll end with a `refresh_token` that you can now paste inside your tootube configuration. diff --git a/src/lib.rs b/src/lib.rs index 4c4f66c..44412d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ mod peertube; use peertube::{get_latest_video, get_max_resolution_dl}; mod youtube; +pub use youtube::register; use youtube::{create_resumable_upload, now_kiss}; async fn get_dl_video_stream( diff --git a/src/main.rs b/src/main.rs index 3f3b5cb..1efeb9a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,8 +17,30 @@ fn main() { .default_value(DEFAULT_CONFIG_PATH) .display_order(1), ) + .subcommand( + Command::new("register") + .version(env!("CARGO_PKG_VERSION")) + .about("Command to register to YouTube OAuth2.0") + .arg( + Arg::new("config") + .short('c') + .long("config") + .value_name("CONFIG_FILE") + .help("TOML config file for tootube") + .num_args(1) + .default_value(DEFAULT_CONFIG_PATH) + .display_order(1), + ), + ) .get_matches(); + if let Some(("register", sub_m)) = matches.subcommand() { + let config = parse_toml(sub_m.get_one::("config").unwrap()); + register(&config.youtube) + .unwrap_or_else(|e| panic!("Cannot register to YouTube API: {}", e)); + return; + } + let config = parse_toml(matches.get_one::("config").unwrap()); env_logger::init(); diff --git a/src/youtube.rs b/src/youtube.rs index b5bb729..1c527b6 100644 --- a/src/youtube.rs +++ b/src/youtube.rs @@ -1,9 +1,9 @@ use crate::{config::YoutubeConfig, error::TootubeError, peertube::PeerTubeVideo}; use bytes::Bytes; use futures_core::stream::Stream; -use reqwest::{Body, Client}; +use reqwest::{multipart::Form, Body, Client}; use serde::{Deserialize, Serialize}; -use std::error::Error; +use std::{error::Error, io::stdin}; use tokio::sync::OnceCell; static ACCESS_TOKEN: OnceCell = OnceCell::const_new(); @@ -32,6 +32,7 @@ impl Default for RefreshTokenRequest { #[derive(Deserialize, Debug)] struct AccessTokenResponse { access_token: String, + refresh_token: String, } #[derive(Serialize, Debug)] @@ -82,6 +83,41 @@ impl Default for YoutubeUploadParamsStatus { } } +/// This function makes the registration process a little bit easier +#[tokio::main] +pub async fn register(config: &YoutubeConfig) -> Result<(), Box> { + println!("Click on the link below to authorize {} to upload to YouTube and deal with your playlists:", env!("CARGO_PKG_NAME")); + println!("https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/youtube%20https://www.googleapis.com/auth/youtube.upload&response_type=code", config.client_id); + println!("Paste the returned authorization code:"); + + let mut input = String::new(); + stdin() + .read_line(&mut input) + .expect("Unable to read back the authorization code!"); + + let form = Form::new() + .text("code", input) + .text("client_id", config.client_id.clone()) + .text("client_secret", config.client_secret.clone()) + .text("redirect_uri", "urn:ietf:wg:oauth:2.0:oob") + .text("grant_type", "authorization_code"); + + let client = Client::new(); + let res = client + .post("https://accounts.google.com/o/oauth2/token") + .multipart(form) + .send() + .await?; + + let access_token: AccessTokenResponse = res.json().await?; + + println!("You can now paste the following line inside the `youtube` section of your tootube.toml file:"); + + println!("refresh_token=\"{}\"", access_token.refresh_token); + + Ok(()) +} + /// Ensures that Token has been refreshed and that it is unique async fn refresh_token(config: &YoutubeConfig) -> Result { ACCESS_TOKEN