Split into library #1

Merged
falso merged 1 commits from library into master 2021-12-26 06:36:39 +00:00
4 changed files with 225 additions and 207 deletions

View File

@ -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"

212
src/client.rs Normal file
View File

@ -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<String, Recording>
}
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(&params)
.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<String> {
let mut models: Vec<String> = 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
// <div class="thumbnail_label_featured thumbnail_label_c_private_show">EM PRIVADO</div>
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<String> = 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));
}
}
}

3
src/lib.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod client;
pub use client::*;

View File

@ -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<String, Recording>
}
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(&params)
.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<String> {
let mut models: Vec<String> = 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
// <div class="thumbnail_label_featured thumbnail_label_c_private_show">EM PRIVADO</div>
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<String> = 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();
}