From c162450bddd99ccfd883274f4dd4eacc797a799b Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 12:10:49 +0100 Subject: [PATCH] Create new struct GrandMother and refactor to her --- Cargo.toml | 2 +- src/config.rs | 120 +++-------------------------------- src/lib.rs | 17 +++-- src/main.rs | 38 +++++------ src/parser.rs | 79 ++++++++--------------- src/playlist.rs | 165 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 232 insertions(+), 189 deletions(-) create mode 100644 src/playlist.rs diff --git a/Cargo.toml b/Cargo.toml index a76a8ad..b1c1225 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +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"] } -serde = { version = "1.0.152", features = ["serde_derive"] } +serde = { version = "1.0.152", features = ["serde_derive","rc"] } serde_json = "1.0.93" tokio = { version = "1.24.2", features = ["full"] } diff --git a/src/config.rs b/src/config.rs index e102a05..7334cda 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,9 @@ use std::{ fs::{self, File}, io::{self, BufReader}, - ops::{Deref, DerefMut}, + ops::Deref, path::{Path, PathBuf}, + rc::Rc, }; use directories::ProjectDirs; @@ -10,20 +11,15 @@ use serde::{Deserialize, Serialize}; use serde_json; use crate::{ - download_with_progress, downloader::DualWriter, get_mut_ref, m3u8::DataEntry, M3u8, Readline, + download_with_progress, get_mut_ref, m3u8::DataEntry, Readline, APP_IDENTIFIER, + JSON_CONFIG_FILENAME, STANDARD_OFFLINE_FILENAME, STANDARD_PLAYLIST_FILENAME, + STANDARD_SEEN_LINKS_FILENAME, }; -const JSON_CONFIG_FILENAME: &'static str = "config.json"; -const APP_IDENTIFIER: [&'static str; 3] = ["com", "billenius", "ilovetv"]; -const STANDARD_PLAYLIST_FILENAME: &'static str = "playlist.m3u8"; -const STANDARD_SEEN_LINKS_FILENAME: &'static str = "watched_links.json"; -const STANDARD_OFFLINE_FILENAME: &'static str = "ilovetv_offline.json"; -const MAX_TRIES: u8 = 4; - #[derive(Serialize, Deserialize, Debug)] pub struct Conf { pub playlist_filename: String, - pub playlist_url: String, + pub playlist_url: Rc, pub last_search: Option, pub seen_links_filename: String, } @@ -45,7 +41,7 @@ impl Conf { } // Get fresh config with url from user - let playlist_url = Self::user_setup(); + let playlist_url = Self::user_setup().into(); Ok(Self { playlist_filename: STANDARD_PLAYLIST_FILENAME.to_owned(), @@ -83,7 +79,7 @@ impl Conf { pub struct Configuration { pub conf: Conf, - pub playlist_path: PathBuf, + pub playlist_path: Rc, pub seen_links_path: PathBuf, pub seen_links: Vec, config_file_path: PathBuf, @@ -115,7 +111,7 @@ impl Configuration { )?; // Playlist - let playlist_path = cache_dir.join(&configuration.playlist_filename); + let playlist_path = cache_dir.join(&configuration.playlist_filename).into(); let seen_links_path = cache_dir.join(&configuration.seen_links_filename); let seen_links = Self::get_watched(&seen_links_path).unwrap_or_default(); @@ -156,12 +152,6 @@ impl Configuration { serde_json::from_reader(reader).ok() } - async fn just_download(&self) -> Result { - download_with_progress(&self.playlist_url, None) - .await? - .get_string() - } - fn get_datafile_content(datafile: &PathBuf) -> Option> { let reader = BufReader::new(File::open(datafile).ok()?); serde_json::from_reader(reader).ok() @@ -175,95 +165,3 @@ impl Deref for Configuration { &self.conf } } - -struct Playlist { - pub entries: Vec, - path_to_playlist: PathBuf, -} - -impl Playlist { - pub fn new(path_to_playlist: PathBuf) -> Self { - todo!() - } - - pub fn write() -> Result<(), io::Error> { - todo!() - } - - fn get_saved_playlist(path: &Path) -> Option { - if !Self::should_update_playlist(path) { - return fs::read_to_string(path).ok(); - } - None - } - - fn should_update_playlist(path: &Path) -> bool { - fs::metadata(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, url: &str, path: &Path) -> Result { - let content = if let Some(content) = Self::get_saved_playlist(path) { - content - } else { - let downloaded = Self::download_playlist(url).await?; - if let Err(e) = fs::write(path, &downloaded) { - println!( - "Failed to save downloaded playlist to file, {:?}, path: '{}'", - e, - path.as_os_str().to_str().unwrap() - ); - } - downloaded - }; - - Ok(content) - } - - pub async fn download_playlist(url: &str) -> Result { - let mut counter: u8 = 0; - loop { - counter += 1; - - let downloaded = download_with_progress(url, None) - .await - .and_then(DualWriter::get_string); - if let Ok(content) = downloaded { - break Ok(content); - } else if counter > MAX_TRIES { - break Err("Failed to download playlist".to_owned()); - } - println!("Retrying {}/{}", counter + 1, MAX_TRIES); - } - } -} - -impl Deref for Playlist { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.entries - } -} - -impl DerefMut for Playlist { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.entries - } -} diff --git a/src/lib.rs b/src/lib.rs index 80467b5..6487e8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,23 @@ +mod config; +mod downloader; mod m3u8; mod parser; +mod playlist; + use std::io::{stdin, stdout, Stdin, StdoutLock, Write}; +pub use config::Configuration; +pub use downloader::download_with_progress; pub use m3u8::{DataEntry, M3u8}; pub use parser::Parser; -mod config; -pub use config::Configuration; -mod downloader; -pub use downloader::download_with_progress; +pub use playlist::{GrandMother, Playlist}; +pub const JSON_CONFIG_FILENAME: &'static str = "config.json"; +pub const APP_IDENTIFIER: [&'static str; 3] = ["com", "billenius", "ilovetv"]; +pub const STANDARD_PLAYLIST_FILENAME: &'static str = "playlist.m3u8"; +pub const STANDARD_SEEN_LINKS_FILENAME: &'static str = "watched_links.json"; +pub const STANDARD_OFFLINE_FILENAME: &'static str = "ilovetv_offline.json"; +pub const MAX_TRIES: u8 = 4; pub struct Readline<'a> { stdout: StdoutLock<'a>, stdin: Stdin, diff --git a/src/main.rs b/src/main.rs index 98166b1..6a3aa08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,8 @@ use std::rc::Rc; use colored::Colorize; use ilovetv::{ - download_with_progress, get_mut_ref, Configuration, DataEntry, M3u8, Parser, Readline, + download_with_progress, get_mut_ref, Configuration, DataEntry, GrandMother, M3u8, Parser, + Readline, }; #[tokio::main] @@ -32,9 +33,10 @@ async fn main() { .iter() .for_each(|s| println!("{}", &s)); - let config = Rc::new(Configuration::new().expect("Failed to write to configfile")); - - let parser = Parser::new(config.clone()).await; + let gm = GrandMother::new(Configuration::new().expect("Failed to write to configfile")) + .await + .unwrap(); + // let parser = Parser::new(config.clone()).await; let mut mpv_fs = false; let mut search_result: Option>> = None; @@ -55,7 +57,7 @@ async fn main() { // Refresh playlist "r" => { search_result = None; - refresh(&parser).await; + gm.refresh_dirty().await; continue; } // Toggle fullscreen for mpv @@ -68,7 +70,7 @@ async fn main() { continue; } "l" => { - search = if let Some(s) = config.last_search.as_ref() { + search = if let Some(s) = gm.config.last_search.as_ref() { s } else { println!("There is no search saved from earlier"); @@ -76,19 +78,19 @@ async fn main() { }; } "c" => { - config.update_last_search_ugly(None); + gm.config.update_last_search_ugly(None); continue; } _ => {} } - search_result = Some(Rc::new(parser.find(search))); + search_result = Some(Rc::new(gm.parser.find(search))); if search_result.as_ref().unwrap().is_empty() { println!("Nothing found"); search_result = None; continue; } - config.update_last_search_ugly(Some(search.to_owned())); + gm.config.update_last_search_ugly(Some(search.to_owned())); } // Let them choose which one to stream @@ -114,7 +116,7 @@ async fn main() { "r" => { println!("Refreshing local m3u8-file"); search_result = None; - refresh(&parser).await; + gm.refresh_dirty().await; continue; } "f" => { @@ -140,14 +142,14 @@ async fn main() { ask_which_to_download(&mut readline, &search_result.as_ref().unwrap()); for to_download in download_selections.iter() { - let path = config.data_dir.join(&to_download.name); + let path = gm.config.data_dir.join(&to_download.name); let path = path.to_string_lossy().to_string(); download_m3u8(to_download, Some(&path)).await; let data_entry = DataEntry::new((*to_download).clone(), path); - config.push_datafile_ugly(data_entry); + gm.config.push_datafile_ugly(data_entry); } - if let Err(e) = config.write_datafile() { + if let Err(e) = gm.config.write_datafile() { println!( "Failed to information about downloaded entries for offline use {:?}", e @@ -162,7 +164,7 @@ async fn main() { Ok(k) => { let search_result = search_result.as_ref().unwrap(); stream(search_result[k - 1], mpv_fs); - parser.save_watched(); + gm.save_watched(); } Err(e) => println!("Have to be a valid number! {:?}", e), } @@ -251,11 +253,3 @@ fn stream(m3u8item: &M3u8, launch_in_fullscreen: bool) { .output() .expect("Could not listen for output"); } - -/* - * I know that this is also frowned upon, but it is perfectly safe right here, - * even though the borrowchecker complains - */ -async fn refresh(parser: &Parser) { - unsafe { get_mut_ref(parser) }.forcefully_update().await; -} diff --git a/src/parser.rs b/src/parser.rs index b2ae4b8..1e67b00 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,24 +1,15 @@ -use std::fs; use std::ops::Deref; -use std::rc::Rc; use crate::m3u8::M3u8; -use crate::Configuration; - -const MAX_TRIES: usize = 4; pub struct Parser { - configuration: Rc, m3u8_items: Vec, } impl Parser { - pub async fn new(configuration: Rc) -> Self { - let m3u8_items = Self::get_parsed_m3u8(&configuration).await.unwrap(); - + pub async fn new(m3u_content: &str, watched_links: &Vec<&str>) -> Self { Self { - configuration, - m3u8_items, + m3u8_items: Self::parse_m3u8(m3u_content, watched_links), } } @@ -30,41 +21,34 @@ impl Parser { .collect() } - pub async fn forcefully_update(&mut self) { - let mut counter = 0; - let content = loop { - counter += 1; - let content = self.download_playlist().await; - if counter > MAX_TRIES { - return; - } else if content.is_ok() { - break content.unwrap(); - } - println!("Retrying {}/{}", counter, MAX_TRIES); - }; + /* + * I know that this is also frowned upon, but it is perfectly safe right here, + * even though the borrowchecker complains + */ + // async fn refresh(&self) { + // unsafe { get_mut_ref(&self.pla) }.forcefully_update().await; + // } - self.m3u8_items = Self::parse_m3u8(content, &self.seen_links); - } - - pub fn save_watched(&self) { - let watched_items = self + pub async fn forcefully_update(&mut self, content: &str) { + let seen_links: &Vec<&str> = &self .m3u8_items .iter() - .filter(|item| item.watched) - .map(|item| item.link.clone()) - .collect::>(); + .filter(|x| x.watched) + .map(|x| x.link.as_str()) + .collect(); - 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); - } + self.m3u8_items = Self::parse_m3u8(content, seen_links); } - fn parse_m3u8(content: String, watched_links: &Vec) -> Vec { + pub fn get_watched(&self) -> Vec<&String> { + self.m3u8_items + .iter() + .filter(|x| x.watched) + .map(|x| &x.link) + .collect() + } + + fn parse_m3u8(content: &str, watched_links: &Vec<&str>) -> Vec { let mut m3u8_items: Vec = Vec::new(); let interesting_lines: Vec = content .replacen("#EXTM3U\n", "", 1) @@ -85,8 +69,8 @@ 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_links.contains(link); + let link = interesting_lines[i + 1].as_str(); + let is_watched = watched_links.contains(&link); let m3u8_item = M3u8 { tvg_id: items[0].to_owned(), tvg_name: items[1].to_owned(), @@ -100,19 +84,12 @@ impl Parser { } m3u8_items } - - 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; + type Target = Vec; fn deref(&self) -> &Self::Target { - &self.configuration + &self.m3u8_items } } diff --git a/src/playlist.rs b/src/playlist.rs new file mode 100644 index 0000000..320031a --- /dev/null +++ b/src/playlist.rs @@ -0,0 +1,165 @@ +use std::{ + borrow::BorrowMut, + fs, + ops::{Deref, DerefMut}, + path::PathBuf, + rc::Rc, +}; + +use crate::{download_with_progress, downloader::DualWriter, Configuration, Parser, MAX_TRIES}; + +pub struct Playlist { + pub content: String, + path_to_playlist: Rc, + url: Rc, +} + +impl Playlist { + pub async fn new(path_to_playlist: Rc, url: Rc) -> Result { + let mut me = Self { + content: String::new(), + path_to_playlist, + url, + }; + me.content = me.get_saved_or_download().await?; + + Ok(me) + } + + fn get_saved(&self) -> Option { + if !self.should_update() { + return fs::read_to_string(&*self.path_to_playlist).ok(); + } + None + } + + fn should_update(&self) -> bool { + fs::metadata(&*self.path_to_playlist) + .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_saved_or_download(&self) -> Result { + let content = if let Some(content) = self.get_saved() { + content + } else { + let downloaded = self.download().await?; + if let Err(e) = fs::write(&*self.path_to_playlist, &downloaded) { + println!( + "Failed to save downloaded playlist to file, {:?}, path: '{}'", + e, + &self.path_to_playlist.as_os_str().to_str().unwrap() + ); + } + downloaded + }; + + Ok(content) + } + + pub async fn download(&self) -> Result { + let mut counter: u8 = 0; + loop { + counter += 1; + + let downloaded = download_with_progress(&self.url, None) + .await + .and_then(DualWriter::get_string); + if let Ok(content) = downloaded { + break Ok(content); + } else if counter > MAX_TRIES { + break Err("Failed to download playlist".to_owned()); + } + println!("Retrying {}/{}", counter + 1, MAX_TRIES); + } + } +} + +impl Deref for Playlist { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.content + } +} + +impl DerefMut for Playlist { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.content + } +} + +pub struct GrandMother { + pub parser: Parser, + pub playlist: Playlist, + pub config: Configuration, +} + +impl GrandMother { + pub async fn new(config: Configuration) -> Result { + let playlist = Playlist::new(config.playlist_path.clone(), config.playlist_url.clone()); + let seen_links = config.seen_links.iter().map(|x| x.as_str()).collect(); + let playlist = playlist.await?; + let playlist_content = playlist.get_saved_or_download().await?; + + let parser = Parser::new(&playlist_content, &seen_links).await; + + Ok(Self { + parser, + playlist, + config, + }) + } + + pub async fn refresh_dirty(&self) { + let ptr = self as *const Self as *mut Self; + unsafe { &mut *ptr }.refresh().await; + } + + pub async fn refresh(&mut self) { + let mut counter = 0; + let content = loop { + counter += 1; + let content = self.playlist.download().await; + if counter > MAX_TRIES { + return; + } + if let Ok(content) = content { + break content; + } + println!("Retrying {}/{}", counter, MAX_TRIES); + }; + + let watched_links = self.parser.get_watched(); + let watched_links = watched_links.iter().map(|x| x.as_str()).collect(); + self.parser = Parser::new(&content, &watched_links).await; + } + + pub fn save_watched(&self) { + let watched_items = self.parser.get_watched(); + + let resp = fs::write( + &self.config.seen_links_path, + serde_json::to_string(&watched_items).unwrap(), + ); + + if let Err(e) = resp { + eprintln!("Failed to write watched links {:?}", e); + } + } +}