commit 2c52b4fcfa6a9c4d801b0590e24e026dc1847c4e Author: loveb Date: Tue Jan 17 18:05:15 2023 +0100 Reads standard cache and config dirs and does basically what it should diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d4d51e2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "iptvnator_rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +colored = "2.0.0" +ctrlc = "3.2.4" +directories = "4.0.1" +filetime = "0.2.19" +reqwest = { version = "0.11.13", features = ["blocking", "deflate", "gzip", "rustls", "rustls-tls"] } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..656b2ba --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,54 @@ +mod m3u8; +mod parser; +use std::{ + fs, + io::{stdin, stdout, Write}, + process, +}; + +pub use m3u8::M3u8; +pub use parser::Parser; +mod config; + +use directories; + +#[test] +pub fn aaaaaaaa() { + let a = directories::ProjectDirs::from("com", "billenius", "iptvnator_rs").unwrap(); + let datadir = a.data_dir(); + let configdir = a.config_dir(); + println!("{:?}", datadir); + println!("{:?}", configdir); +} + +pub fn setup() -> String { + let project_dirs = directories::ProjectDirs::from("com", "billenius", "iptvnator_rs").unwrap(); + let config_dir = project_dirs.config_dir(); + let ilovetv_config_file = config_dir.join("iptv_url.txt"); + if ilovetv_config_file.exists() { + return fs::read_to_string(&ilovetv_config_file).expect("Failed to read iptv_url"); + } + + println!("Hello, I would need an url to your iptv/m3u/m3u8 stream"); + print!("enter url: "); + let mut stdout = stdout().lock(); + stdout.flush().unwrap(); + let mut url = String::new(); + let stdin = stdin(); + let _ = stdin.read_line(&mut url); + print!("Are you sure? (Y/n) "); + stdout.flush().unwrap(); + let mut yn = String::new(); + let _ = stdin.read_line(&mut yn); + if yn.trim() == "n" { + setup(); + } + + let _ = fs::create_dir_all(config_dir); + if let Err(e) = fs::write(ilovetv_config_file, url.trim()) { + eprintln!("{:?}", e); + process::exit(-1); + } + + url.to_string() +} diff --git a/src/m3u8.rs b/src/m3u8.rs new file mode 100644 index 0000000..79f0a1f --- /dev/null +++ b/src/m3u8.rs @@ -0,0 +1,24 @@ +use colored::Colorize; +use std::fmt::Display; + +pub struct M3u8 { + pub tvg_id: String, + pub tvg_name: String, + pub tvg_logo: String, + pub group_title: String, + pub name: String, + pub link: String, + pub watched: bool, +} + +impl Display for M3u8 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let colored_name = if self.watched { + self.name.bold().green() + } else { + self.name.bold() + }; + f.write_fmt(format_args!("{}({})", colored_name, self.link))?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..64e82ca --- /dev/null +++ b/src/main.rs @@ -0,0 +1,76 @@ +use std::io::{self, stdout, Write}; +use std::process::Command; +use std::rc::Rc; + +use iptvnator_rs::{setup, M3u8, Parser}; +fn main() { + println!("Welcome to iptvnator_rs, the somewhat faster version and certainly harder to write iptvnator"); + let p = Parser::new("iptv.m3u8".to_owned(), setup(), "watched.txt".to_owned()); + + let stdin = io::stdin(); + let mut stdout = stdout().lock(); + let mut search_result: Option>> = None; + + loop { + let mut buf = String::new(); + + // Dont't perform a search if user has just watched, instead present the previous search + if search_result.is_none() { + print!("Search by name: "); + stdout.flush().unwrap(); + stdin.read_line(&mut buf).unwrap(); + buf = buf.trim().to_owned(); + + // If they want to quit, let them- + if buf.trim() == "q" { + break; + } + + search_result = Some(Rc::new(p.find(&buf))); + + if search_result.as_ref().unwrap().len() == 0 { + println!("Nothing found"); + stdout.flush().unwrap(); + continue; + } + } + + // Let them choose which one to stream + for (idx, m3u8_item) in search_result.as_ref().unwrap().iter().enumerate().rev() { + println!(" {}: {}", idx + 1, m3u8_item); + } + print!("Which one do you wish to stream? [q | s]: "); + stdout.flush().unwrap(); + buf = String::new(); + stdin.read_line(&mut buf).unwrap(); + + // If they want to quit, let them- + if buf.trim() == "q" { + break; + } + + let choosen = buf.trim().parse::(); + match choosen { + Ok(k) => { + let search_result = search_result.as_ref().unwrap().clone(); + stream(&(search_result[k - 1])) + } + Err(e) => println!("Have to be a valid number! {:?}", e), + } + } + + p.save_watched(); +} + +fn stream(m3u8item: &M3u8) { + // Well I know that this is frowned upon, but it's honestly the most efficient way of doing this + let ptr = m3u8item as *const M3u8; + let ptr = ptr as *mut M3u8; + let mut item = unsafe { &mut *ptr }; + item.watched = true; + + Command::new("mpv") + .arg(&m3u8item.link) + .output() + .expect("Could not listen for output"); +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..7abb40e --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,170 @@ +use std::ops::Deref; +use std::path::PathBuf; +use std::rc::Rc; +use std::{fs, process}; + +use directories::ProjectDirs; + +use crate::m3u8::M3u8; + +const MAX_TRIES: usize = 4; + +pub struct Parser { + watched_name: Rc, + m3u8_items: Vec, +} + +impl Parser { + fn should_update(file_name: &PathBuf) -> bool { + fs::metadata(&file_name) + .and_then(|metadata| { + Ok({ + let seconds = metadata.modified()?; + seconds + .elapsed() + .expect("Failed to get systemtime") + .as_secs() + > 60 * 60 * 24 * 3 + }) + }) + .map_or_else( + |_| { + println!("Could not find playlist-file, Downloading a new one"); + false + }, + |x| x, + ) + } + + pub fn new(file_name: String, iptv_url: String, watched_name: String) -> Self { + let project_dirs = ProjectDirs::from("com", "billenius", "iptvnator_rs").unwrap(); + let cache = project_dirs.cache_dir(); + let _ = fs::create_dir_all(&cache); + + let file_name = Rc::new(cache.join(file_name)); + let iptv_url = Rc::new(iptv_url); + let watched_name = Rc::new(cache.join(watched_name)); + + Self { + watched_name: watched_name.clone(), + m3u8_items: Self::get_parsed_content(&iptv_url, &file_name, &watched_name), + } + } + + pub fn find(&self, name: &str) -> Vec<&M3u8> { + let name = name.to_uppercase(); + self.m3u8_items + .iter() + .filter(|item| item.name.to_uppercase().contains(&name) || item.tvg_id.contains(&name)) + .collect() + } + + pub fn save_watched(&self) { + let project_dirs = ProjectDirs::from("com", "billenius", "iptvnator_rs").unwrap(); + let cache_dir = project_dirs.cache_dir(); + let watched_cache_file = cache_dir.join(&*self.watched_name); + + let watched_items = self + .m3u8_items + .iter() + .filter(|item| item.watched) + .map(|item| item.link.clone()) + .collect::>(); + + let _ = fs::create_dir_all(cache_dir); + + match fs::write(watched_cache_file, watched_items.join("\n")) { + Ok(_) => { + println!("Saved watched") + } + Err(e) => { + eprintln!("Failed to write downloaded m3u8file {:?}", e); + } + } + } + + fn get_parsed_content(link: &String, file_name: &PathBuf, watched_name: &PathBuf) -> Vec { + let saved_watches = fs::read_to_string(&watched_name); + let saved_watches = if saved_watches.is_ok() { + saved_watches.unwrap() + } else { + String::from("") + }; + + let watched: Vec = saved_watches.lines().map(String::from).collect(); + + let mut m3u8_items: Vec = Vec::new(); + let interesting_lines: Vec = Self::get_stringcontent(link, file_name, 0) + .replacen("#EXTM3U\n", "", 1) + .lines() + .map(str::trim) + .map(String::from) + .collect(); + + for i in (0..interesting_lines.len()).step_by(2) { + let mut items = Vec::new(); + for to_find in ["tvg-id", "tvg-name", "tvg-logo", "group-title"] { + let offset: usize = format!("{}=", to_find).bytes().len(); + let start: usize = + interesting_lines[i].find(&format!("{}=", to_find)).unwrap() as usize + offset; + + let end: usize = interesting_lines[i].rfind("=").unwrap(); + items.push(&interesting_lines[i][start..=end]) + } + let name_start = interesting_lines[i].rfind(",").unwrap() + 1; + let name = &interesting_lines[i][name_start..]; + let link = &interesting_lines[i + 1]; + let is_watched = watched.contains(link); + let m3u8_item = M3u8 { + tvg_id: items[0].to_owned(), + tvg_name: items[1].to_owned(), + tvg_logo: items[2].to_owned(), + group_title: items[3].to_owned(), + name: name.to_owned(), + link: link.to_string(), + watched: is_watched, + }; + m3u8_items.push(m3u8_item); + } + m3u8_items + } + + fn get_stringcontent(link: &String, file_name: &PathBuf, tried: usize) -> String { + if !Self::should_update(file_name) { + let content = fs::read_to_string(&file_name); + if content.is_ok() { + return content.unwrap(); + } + } + + let content = Self::download(link); + if content.is_err() && tried < 4 { + println!("Retrying {}/{}", tried + 1, MAX_TRIES); + Self::get_stringcontent(link, file_name, tried + 1); + } + + match content { + Ok(s) => { + let _ = fs::write(&file_name, s.as_bytes()); + s + } + Err(_) => { + println!("Couldn't get m3u8 file!"); + process::exit(-1); + } + } + } + + fn download(link: &String) -> Result { + reqwest::blocking::get(link.clone()) + .and_then(|resp| Ok(resp.text().expect("Could not get m3u8 from server"))) + } +} + +impl Deref for Parser { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.m3u8_items + } +}