diff --git a/Cargo.lock b/Cargo.lock index fce0c30..68036ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -464,6 +464,8 @@ dependencies = [ "futures-util", "indicatif", "reqwest", + "serde", + "serde_json", "tokio", ] @@ -871,12 +873,26 @@ name = "serde" version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml index 08a29bb..6bbd5a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,6 @@ directories = "4.0.1" futures-util = "0.3.25" indicatif = { version = "0.17.3", features = ["tokio"] } reqwest = { version = "0.11.13", features = ["blocking", "deflate", "gzip", "rustls", "rustls-tls", "stream"] } -tokio = { version = "1.24.2", features = ["full"] } \ No newline at end of file +serde = { version = "1.0.152", features = ["serde_derive"] } +serde_json = "1.0.93" +tokio = { version = "1.24.2", features = ["full"] } diff --git a/src/config.rs b/src/config.rs index e69de29..59cdc9d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -0,0 +1,188 @@ +use std::{ + fs::{self, File}, + io::{self, BufReader}, + ops::Deref, + path::{Path, PathBuf}, +}; + +use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; +use serde_json; + +use crate::{download_with_progress, Readline}; + +const JSON_CONFIG_FILENAME: &'static str = "iptvnator_config.json"; +const APP_IDENTIFIER: [&'static str; 3] = ["com", "billenius", "iptvnator"]; +const STANDARD_PLAYLIST_FILENAME: &'static str = "ilovetv.m3u8"; +const STANDARD_SEEN_LINKS_FILENAME: &'static str = "watched_links.json"; +const MAX_TRIES: u8 = 4; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Conf { + pub playlist_filename: String, + pub playlist_url: String, + pub last_search: Option, + pub seen_links_filename: String, +} + +impl Conf { + /** + * Read configurationfile or ask user for link input if it isn't created. + * Will error if it fails to write config file + */ + pub fn new(ilovetv_config_file: &Path) -> Result { + // Read the configuraionfile if it exists + if ilovetv_config_file.exists() { + let config_file = Self::read_configfile(&ilovetv_config_file); + if let Ok(cfg) = config_file { + return Ok(cfg); + } else { + println!("There are some problem with the configurationfile"); + } + } + + // Get fresh config with url from user + let playlist_url = Self::user_setup(); + + Ok(Self { + playlist_filename: STANDARD_PLAYLIST_FILENAME.to_owned(), + playlist_url, + last_search: None, + seen_links_filename: STANDARD_SEEN_LINKS_FILENAME.to_owned(), + }) + } + + fn read_configfile(config_file: &Path) -> Result { + let reader = BufReader::new(File::open(config_file)?); + let conf: Conf = serde_json::from_reader(reader)?; + Ok(conf) + } + + fn user_setup() -> String { + let mut readline = Readline::new(); + + println!("Hello, I would need an url to your iptv/m3u/m3u8 stream"); + loop { + let url = readline.input("enter url: "); + let yn = readline.input("Are you sure? (Y/n) "); + + if yn.trim().to_lowercase() != "n" { + break url.trim().to_owned(); + } + } + } +} + +pub struct Configuration { + pub conf: Conf, + pub playlist_path: PathBuf, + pub seen_links_path: PathBuf, + pub seen_links: Vec, +} + +impl Configuration { + pub fn new() -> Result { + let project_dirs = + ProjectDirs::from(APP_IDENTIFIER[0], APP_IDENTIFIER[1], APP_IDENTIFIER[2]).unwrap(); + let config_dir = project_dirs.config_dir(); + let _ = fs::create_dir_all(config_dir); + let config_file = config_dir.join(JSON_CONFIG_FILENAME); + + let configuration = Conf::new(&config_file)?; + + fs::write(config_file, serde_json::to_string(&configuration).unwrap())?; + + // Setup dirs for playlist + let cache_dir = project_dirs.cache_dir().to_path_buf(); + let playlist_path = cache_dir.join(&configuration.playlist_filename); + let seen_links_path = cache_dir.join(&configuration.seen_links_filename); + let _ = fs::create_dir_all(&cache_dir); + + let seen_links = Self::get_watched(&seen_links_path).unwrap_or_default(); + + Ok(Self { + conf: configuration, + playlist_path, + seen_links, + seen_links_path, + }) + } + + fn get_watched(path: &Path) -> Option> { + let reader = BufReader::new(File::open(&path).ok()?); + serde_json::from_reader(reader).ok() + } + + fn should_update_playlist(&self) -> bool { + fs::metadata(&self.playlist_path) + .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 async fn get_playlist(&self) -> Result { + let content = if let Some(content) = self.get_saved_playlist() { + content + } else { + let downloaded = self.download_playlist().await?; + if let Err(e) = fs::write(&self.playlist_path, &downloaded) { + println!( + "Failed to save downloaded playlist to file, {:?}, path: '{}'", + e, + &self.playlist_path.as_os_str().to_str().unwrap() + ); + } + downloaded + }; + + Ok(content) + } + + fn get_saved_playlist(&self) -> Option { + if !self.should_update_playlist() { + return fs::read_to_string(&self.playlist_path).ok(); + } + None + } + + pub async fn download_playlist(&self) -> Result { + let mut counter: u8 = 0; + loop { + counter += 1; + + if let Ok(content) = self.just_download().await { + break Ok(content); + } else if counter > MAX_TRIES { + break Err("Failed to download playlist".to_owned()); + } + println!("Retrying {}/{}", counter + 1, MAX_TRIES); + } + } + + async fn just_download(&self) -> Result { + download_with_progress(&self.playlist_url, None) + .await? + .get_string() + } +} + +impl Deref for Configuration { + type Target = Conf; + + fn deref(&self) -> &Self::Target { + &self.conf + } +} diff --git a/src/lib.rs b/src/lib.rs index 3e0ea2d..8bbd1ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,47 +1,14 @@ mod m3u8; mod parser; -use std::{ - fs, - io::{stdin, stdout, Stdin, StdoutLock, Write}, - process, -}; +use std::io::{stdin, stdout, Stdin, StdoutLock, Write}; pub use m3u8::M3u8; pub use parser::Parser; mod config; +pub use config::Configuration; mod downloader; -use directories::ProjectDirs; pub use downloader::download_with_progress; -pub fn setup() -> String { - let project_dirs = 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"); - } - - let mut readline = Readline::new(); - - println!("Hello, I would need an url to your iptv/m3u/m3u8 stream"); - let url = loop { - let url = readline.input("enter url: "); - let yn = readline.input("Are you sure? (Y/n) "); - - if yn.trim().to_lowercase() != "n" { - break url.trim().to_string(); - } - }; - - let _ = fs::create_dir_all(config_dir); - if let Err(e) = fs::write(ilovetv_config_file, &url) { - eprintln!("{:?}", e); - process::exit(-1); - } - - url -} - pub struct Readline<'a> { stdout: StdoutLock<'a>, stdin: Stdin, @@ -66,7 +33,7 @@ impl<'a> Readline<'a> { /** * I know that this isn't considered true rusty code, but the places it's used in is - * safe. For this I'll leave the funciton as unsafe, so to better see it's uses. + * safe. For this I'll leave the funciton as unsafe, so to better see it's uses. * This solution makes the uses BLAZINGLY FAST which moreover is the most rusty you can get. */ pub unsafe fn get_mut_ref(reference: &T) -> &mut T { diff --git a/src/main.rs b/src/main.rs index 090cbfb..019d83f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::process::Command; use std::rc::Rc; use colored::Colorize; -use iptvnator::{download_with_progress, get_mut_ref, setup, M3u8, Parser, Readline}; +use iptvnator::{download_with_progress, get_mut_ref, Configuration, M3u8, Parser, Readline}; #[tokio::main] async fn main() { @@ -17,7 +17,9 @@ async fn main() { "r".bold(),"q".bold(),"d".bold(),"s".bold(),"a".bold(), "f".bold() ); - let parser = Parser::new("iptv.m3u8".to_owned(), setup(), "watched.txt".to_owned()).await; + let config = Rc::new(Configuration::new().expect("Failed to write to configfile")); + + let parser = Parser::new(config.clone()).await; let mut mpv_fs = false; let mut search_result: Option>> = None; diff --git a/src/parser.rs b/src/parser.rs index d2e6825..b2ae4b8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,36 +1,24 @@ -use std::path::PathBuf; +use std::fs; +use std::ops::Deref; use std::rc::Rc; -use std::{fs, process}; -use directories::ProjectDirs; - -use crate::downloader::download_with_progress; use crate::m3u8::M3u8; +use crate::Configuration; const MAX_TRIES: usize = 4; pub struct Parser { - watched_name: Rc, + configuration: Rc, m3u8_items: Vec, - ilovetv_url: Rc, - file_name: Rc, } impl Parser { - pub async 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 ilovetv_url = Rc::new(iptv_url); - let watched_name = Rc::new(cache.join(watched_name)); + pub async fn new(configuration: Rc) -> Self { + let m3u8_items = Self::get_parsed_m3u8(&configuration).await.unwrap(); Self { - watched_name: watched_name.clone(), - m3u8_items: Self::get_parsed_content(&ilovetv_url, &file_name, &watched_name).await, - ilovetv_url, - file_name, + configuration, + m3u8_items, } } @@ -42,42 +30,20 @@ impl Parser { .collect() } - 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 async fn forcefully_update(&mut self) { let mut counter = 0; let content = loop { counter += 1; - let content = Self::download(&self.ilovetv_url).await.ok(); + let content = self.download_playlist().await; if counter > MAX_TRIES { return; - } else if content.is_some() { + } else if content.is_ok() { break content.unwrap(); } println!("Retrying {}/{}", counter, MAX_TRIES); }; - let _ = fs::write(&*self.file_name, &content); - self.m3u8_items = Self::parse_m3u8(content, &self.watched_name.clone()); + self.m3u8_items = Self::parse_m3u8(content, &self.seen_links); } pub fn save_watched(&self) { @@ -88,36 +54,17 @@ impl Parser { .map(|item| item.link.clone()) .collect::>(); - let _ = fs::create_dir_all(&*self.watched_name.parent().unwrap()); - - if let Err(e) = fs::write(&*self.watched_name, watched_items.join("\n")) { + let resp = fs::write( + &self.seen_links_path, + serde_json::to_string(&watched_items).unwrap(), + ); + + if let Err(e) = resp { eprintln!("Failed to write watched links {:?}", e); } } - async fn get_parsed_content( - link: &String, - file_name: &PathBuf, - watched_name: &PathBuf, - ) -> Vec { - Self::parse_m3u8( - Self::get_stringcontent(link, file_name) - .await - .expect("Failed to retrieve playlist"), - watched_name, - ) - } - - fn parse_m3u8(content: String, 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(); - + fn parse_m3u8(content: String, watched_links: &Vec) -> Vec { let mut m3u8_items: Vec = Vec::new(); let interesting_lines: Vec = content .replacen("#EXTM3U\n", "", 1) @@ -139,7 +86,7 @@ impl Parser { 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 is_watched = watched_links.contains(link); let m3u8_item = M3u8 { tvg_id: items[0].to_owned(), tvg_name: items[1].to_owned(), @@ -154,42 +101,18 @@ impl Parser { m3u8_items } - async fn get_stringcontent(link: &String, file_name: &PathBuf) -> Result { - if !Self::should_update(file_name) { - let content = fs::read_to_string(&file_name); - if content.is_ok() { - return Ok(content.unwrap()); - } - } - - let mut counter: usize = 0; - let content = loop { - counter += 1; - - if let Ok(content) = Self::download(link).await { - break Ok(content); - } else if counter > MAX_TRIES { - break Err("".to_owned()); - } - println!("Retrying {}/{}", counter + 1, MAX_TRIES); - }; - - match content { - Ok(s) => { - let _ = fs::write(&file_name, s.as_bytes()); - Ok(s) - } - Err(_) => { - println!("Couldn't get m3u8 file!"); - process::exit(-1); - } - } - } - - async fn download(link: &String) -> Result { - Ok(download_with_progress(link, None) - .await? - .get_string() - .unwrap()) + async fn get_parsed_m3u8(config: &Configuration) -> Result, String> { + Ok(Self::parse_m3u8( + config.get_playlist().await?, + &config.seen_links, + )) + } +} + +impl Deref for Parser { + type Target = Configuration; + + fn deref(&self) -> &Self::Target { + &self.configuration } }