From 6376371358923fb1d29d91acd72faf97da5f8edd Mon Sep 17 00:00:00 2001 From: Pedro de Oliveira Date: Sun, 26 Dec 2021 06:33:39 +0000 Subject: [PATCH] Split into library --- Cargo.toml | 8 ++ src/client.rs | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/main.rs | 209 +------------------------------------------------ 4 files changed, 225 insertions(+), 207 deletions(-) create mode 100644 src/client.rs create mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index bfda724..3d4ae38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "recorder" +path = "src/lib.rs" + +[[bin]] +name = "chaturbate" +path = "src/main.rs" + [dependencies] reqwest = { version = "0.11", features = ["blocking", "cookies"] } scraper = "0.12.0" diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..1861a72 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,212 @@ +use std::collections::HashMap; +use std::time::{Duration, Instant}; +use std::{thread, time}; +use log::LevelFilter; +use log::{info}; +use scraper::{Html, Selector}; +use simple_logger::SimpleLogger; +use subprocess::*; + +/// Represents an instance of youtube-dl subprocess +/// and the the Instant when it started +struct Recording { + time: Instant, + process: subprocess::Popen +} + +/// Contains the reqwest client with cookies. +/// The settings from the ini file. +/// And the list of current Recording +pub struct Chaturbate { + client: reqwest::blocking::Client, + settings: config::Config, + recording: HashMap +} + +impl Default for Chaturbate { + fn default() -> Self { + Self::new() + } +} + +impl Chaturbate { + /// Returns a new Chaturbate client + pub fn new() -> Chaturbate { + // Initialize Logging + SimpleLogger::new() + .with_utc_timestamps() + .with_level(LevelFilter::Info) + .with_colors(true).init().unwrap(); + // Initialize Settings + let mut settings = config::Config::default(); + settings.merge(config::File::with_name("chaturbate")).unwrap(); + // Create the HTTP Client + let client = reqwest::blocking::Client::builder() + .referer(true) + .cookie_store(true) + .timeout(Duration::from_secs(10)) + .build() + .unwrap(); + Chaturbate { client, settings, recording: HashMap::new() } + } + + /// Returns a bool after parsing the HTML source to find out + /// if the login worked + /// + /// # Arguments + /// + /// * `html_source` - A string containing HTML source. + fn is_logged(&mut self, html_source: &str) -> bool { + let mut result = false; + let document = Html::parse_document(html_source); + // Does div#user_information_profile_container contain the class anonymous + let selector = Selector::parse("div#user_information_profile_container").unwrap(); + let input = document.select(&selector).next().unwrap(); + let class = input.value().attr("class"); + if class != None && !class.unwrap().contains("anonymous") { + result = true; + } + result + } + + /// Does an HTTP POST to login with the credentials from the settings + fn login(&mut self) { + info!("Logging in"); + // Open the login page to get the CSRF Token + let response = self.client.get("https://chaturbate.com/auth/login/").send().expect("Could not open page"); + let document = Html::parse_document(&response.text().expect("No response to parse")); + let selector = Selector::parse(r#"input[name="csrfmiddlewaretoken"]"#).unwrap(); + let input = document.select(&selector).next(); + // If we got a Token + if input != None { + let csrfmiddlewaretoken = input.unwrap().value().attr("value").unwrap(); + // Use settings from ini file + let params = [ + ("username", self.settings.get_str("username").unwrap()), + ("password", self.settings.get_str("password").unwrap()), + ("csrfmiddlewaretoken", csrfmiddlewaretoken.to_string()), + ("rememberme", "on".to_string()), + ("next", "".to_string()) + ]; + // Login! + let _response = self.client.post("https://chaturbate.com/auth/login/") + .header("Referer", "https://chaturbate.com/auth/login/") + .form(¶ms) + .send().expect("Could not post to login"); + } + } + + /// Returns a list of the followed models that are currently online + fn get_online_models(&mut self) -> Vec { + let mut models: Vec = Vec::new(); + // Get the followed list + let response = self.client.get("https://chaturbate.com/followed-cams/").send().expect("Could not open page"); + let mut text: String = response.text().unwrap(); + // If we arent logged in, do it + while !self.is_logged(&text) { + self.login(); + let response = self.client.get("https://chaturbate.com/followed-cams/").send().expect("Could not open page"); + text = response.text().unwrap(); + } + // Parse HTML + let document = Html::parse_document(&text); + // Iterate over "li.room_list_room" + let room_sel = Selector::parse("li.room_list_room").unwrap(); + for element in document.select(&room_sel) { + // Find the "a" to get the model name + let name_sel = Selector::parse("a").unwrap(); + let val = element.select(&name_sel).next().unwrap(); + let name = val.value().attr("data-room").unwrap(); + // Check if div.thumbnail_label_offline exists + let status_sel = Selector::parse("div.thumbnail_label_offline").unwrap(); + let val = element.select(&status_sel).next(); + if val != None { + // Is Offline, ignore + continue; + } + // Check if its marked as private + // + let priv_sel = Selector::parse("div.thumbnail_label_c_private_show").unwrap(); + let val = element.select(&priv_sel).next(); + if val != None { + continue; + } + models.push(name.to_string()); + } + models + } + + /// Checks if the `model` is already being recorded + /// + /// # Arguments + /// + /// * `model` - A string with the name of the model + fn is_recording(&self, model: &str) -> bool { + self.recording.contains_key(&model.to_string()) + } + + /// Starts recording the `model` + /// + /// # Arguments + /// + /// * `model` - A string with the name of the model + fn record_model(&mut self, model: &str) { + // If its already being recorded, exit + if self.is_recording(model) { + return; + } + // Create URL + let url = format!("https://chaturbate.com/{}/", model); + // Start subprocess + let p = Popen::create(&["youtube-dl", "-q", url.as_ref()], PopenConfig { + stdout: Redirection::Pipe, stderr: Redirection::Merge, ..Default::default() + }).unwrap(); + info!("Started recording {}", model); + let r = Recording { time: Instant::now(), process: p }; + self.recording.insert(model.to_string(), r); + } + + /// Iterates over the running recordings and checks which ones ended + fn clean_recordings(&mut self) { + // Will hold a list of the names of the terminated recordings + let mut terminated: Vec = Vec::new(); + for (name, c) in self.recording.iter_mut() { + if c.process.poll() != None { + // Terminated, add to list + terminated.push(name.to_string()); + } + } + for name in terminated { + let recording = self.recording.get(&name).unwrap(); + let elapsed = recording.time.elapsed(); + let mut extra: String = String::from(""); + if elapsed < Duration::from_secs(10) { + extra = String::from(" - probably private") + } + info!("Recording of {} finished after {:?}{}", name, elapsed, extra); + // No longer recording it + self.recording.remove(&name); + } + } + + /// Main loop + pub fn do_cycle(&mut self) { + // Get online models, and start recording them + let models = self.get_online_models(); + for name in models { + self.record_model(&name); + } + // Print how many we are recording at the moment + let recording = self.recording.len(); + if recording > 0 { + info!("Currently recording {} streams", recording); + } + // Sleep + let delay = self.settings.get_int("sleep").unwrap() as u64; + let end = Instant::now() + Duration::from_secs(delay); + while Instant::now() < end { + self.clean_recordings(); + thread::sleep(time::Duration::from_secs(5)); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c8ab57b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod client; + +pub use client::*; diff --git a/src/main.rs b/src/main.rs index d50dd07..ba87d27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,212 +1,7 @@ -use std::collections::HashMap; -use std::time::{Duration, Instant}; -use std::{thread, time}; -use log::LevelFilter; -use log::{info}; -use scraper::{Html, Selector}; -use simple_logger::SimpleLogger; -use subprocess::*; - -/// Represents an instance of youtube-dl subprocess -/// and the the Instant when it started -struct Recording { - time: Instant, - process: subprocess::Popen -} - -/// Contains the reqwest client with cookies. -/// The settings from the ini file. -/// And the list of current Recording -struct Chaturbate { - client: reqwest::blocking::Client, - settings: config::Config, - recording: HashMap -} - -impl Chaturbate { - /// Returns a new Chaturbate client - fn new() -> Chaturbate { - // Initialize Logging - SimpleLogger::new() - .with_utc_timestamps() - .with_level(LevelFilter::Info) - .with_colors(true).init().unwrap(); - // Initialize Settings - let mut settings = config::Config::default(); - settings.merge(config::File::with_name("chaturbate")).unwrap(); - // Create the HTTP Client - let client = reqwest::blocking::Client::builder() - .referer(true) - .cookie_store(true) - .timeout(Duration::from_secs(10)) - .build() - .unwrap(); - Chaturbate { client, settings, recording: HashMap::new() } - } - - /// Returns a bool after parsing the HTML source to find out - /// if the login worked - /// - /// # Arguments - /// - /// * `html_source` - A string containing HTML source. - fn is_logged(&mut self, html_source: &str) -> bool { - let mut result = false; - let document = Html::parse_document(html_source); - // Does div#user_information_profile_container contain the class anonymous - let selector = Selector::parse("div#user_information_profile_container").unwrap(); - let input = document.select(&selector).next().unwrap(); - let class = input.value().attr("class"); - if class != None && !class.unwrap().contains("anonymous") { - result = true; - } - result - } - - /// Does an HTTP POST to login with the credentials from the settings - fn login(&mut self) { - info!("Logging in"); - // Open the login page to get the CSRF Token - let response = self.client.get("https://chaturbate.com/auth/login/").send().expect("Could not open page"); - let document = Html::parse_document(&response.text().expect("No response to parse")); - let selector = Selector::parse(r#"input[name="csrfmiddlewaretoken"]"#).unwrap(); - let input = document.select(&selector).next(); - // If we got a Token - if input != None { - let csrfmiddlewaretoken = input.unwrap().value().attr("value").unwrap(); - // Use settings from ini file - let params = [ - ("username", self.settings.get_str("username").unwrap()), - ("password", self.settings.get_str("password").unwrap()), - ("csrfmiddlewaretoken", csrfmiddlewaretoken.to_string()), - ("rememberme", "on".to_string()), - ("next", "".to_string()) - ]; - // Login! - let _response = self.client.post("https://chaturbate.com/auth/login/") - .header("Referer", "https://chaturbate.com/auth/login/") - .form(¶ms) - .send().expect("Could not post to login"); - } - } - - /// Returns a list of the followed models that are currently online - fn get_online_models(&mut self) -> Vec { - let mut models: Vec = Vec::new(); - // Get the followed list - let response = self.client.get("https://chaturbate.com/followed-cams/").send().expect("Could not open page"); - let mut text: String = response.text().unwrap(); - // If we arent logged in, do it - while !self.is_logged(&text) { - self.login(); - let response = self.client.get("https://chaturbate.com/followed-cams/").send().expect("Could not open page"); - text = response.text().unwrap(); - } - // Parse HTML - let document = Html::parse_document(&text); - // Iterate over "li.room_list_room" - let room_sel = Selector::parse("li.room_list_room").unwrap(); - for element in document.select(&room_sel) { - // Find the "a" to get the model name - let name_sel = Selector::parse("a").unwrap(); - let val = element.select(&name_sel).next().unwrap(); - let name = val.value().attr("data-room").unwrap(); - // Check if div.thumbnail_label_offline exists - let status_sel = Selector::parse("div.thumbnail_label_offline").unwrap(); - let val = element.select(&status_sel).next(); - if val != None { - // Is Offline, ignore - continue; - } - // Check if its marked as private - // - let priv_sel = Selector::parse("div.thumbnail_label_c_private_show").unwrap(); - let val = element.select(&priv_sel).next(); - if val != None { - continue; - } - models.push(name.to_string()); - } - models - } - - /// Checks if the `model` is already being recorded - /// - /// # Arguments - /// - /// * `model` - A string with the name of the model - fn is_recording(&self, model: &str) -> bool { - self.recording.contains_key(&model.to_string()) - } - - /// Starts recording the `model` - /// - /// # Arguments - /// - /// * `model` - A string with the name of the model - fn record_model(&mut self, model: &str) { - // If its already being recorded, exit - if self.is_recording(model) { - return; - } - // Create URL - let url = format!("https://chaturbate.com/{}/", model); - // Start subprocess - let p = Popen::create(&["youtube-dl", "-q", url.as_ref()], PopenConfig { - stdout: Redirection::Pipe, stderr: Redirection::Merge, ..Default::default() - }).unwrap(); - info!("Started recording {}", model); - let r = Recording { time: Instant::now(), process: p }; - self.recording.insert(model.to_string(), r); - } - - /// Iterates over the running recordings and checks which ones ended - fn clean_recordings(&mut self) { - // Will hold a list of the names of the terminated recordings - let mut terminated: Vec = Vec::new(); - for (name, c) in self.recording.iter_mut() { - if c.process.poll() != None { - // Terminated, add to list - terminated.push(name.to_string()); - } - } - for name in terminated { - let recording = self.recording.get(&name).unwrap(); - let elapsed = recording.time.elapsed(); - let mut extra: String = String::from(""); - if elapsed < Duration::from_secs(10) { - extra = String::from(" - probably private") - } - info!("Recording of {} finished after {:?}{}", name, elapsed, extra); - // No longer recording it - self.recording.remove(&name); - } - } - - /// Main loop - fn do_cycle(&mut self) { - // Get online models, and start recording them - let models = self.get_online_models(); - for name in models { - self.record_model(&name); - } - // Print how many we are recording at the moment - let recording = self.recording.len(); - if recording > 0 { - info!("Currently recording {} streams", recording); - } - // Sleep - let delay = self.settings.get_int("sleep").unwrap() as u64; - let end = Instant::now() + Duration::from_secs(delay); - while Instant::now() < end { - self.clean_recordings(); - thread::sleep(time::Duration::from_secs(5)); - } - } -} +use recorder::client; fn main() { - let mut client = Chaturbate::new(); + let mut client = client::Chaturbate::new(); loop { client.do_cycle(); } -- 2.40.1