Split into library #1
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(¶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<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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
pub mod client;
|
||||
|
||||
pub use client::*;
|
||||
209
src/main.rs
209
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<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(¶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<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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue