From d0d9f34f9e7f877aa7e6aab43e11cc38c8204b54 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Thu, 2 Mar 2023 22:52:35 +0100 Subject: [PATCH 01/15] sections --- src/config.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 57889e4..4793232 100644 --- a/src/config.rs +++ b/src/config.rs @@ -90,12 +90,13 @@ impl Configuration { pub fn new() -> Result { let project_dirs = ProjectDirs::from(APP_IDENTIFIER[0], APP_IDENTIFIER[1], APP_IDENTIFIER[2]).unwrap(); + + // Config dir let config_dir = project_dirs.config_dir(); - let _ = fs::create_dir_all(config_dir); let config_file_path = config_dir.join(JSON_CONFIG_FILENAME).to_path_buf(); - + let _ = fs::create_dir_all(config_dir); + // Read/create config let configuration = Conf::new(&config_file_path)?; - fs::write( &config_file_path, serde_json::to_string(&configuration).unwrap(), From 3c3acfe0ef8e57ac921e7a9f52e8d200543babe7 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Thu, 2 Mar 2023 23:51:37 +0100 Subject: [PATCH 02/15] Created basic structure --- src/config.rs | 42 ++++++++++++++++++++++++++++++++++-------- src/m3u8.rs | 23 ++++++++++++++++++++++- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/config.rs b/src/config.rs index 4793232..aa57f62 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,12 +9,13 @@ use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use serde_json; -use crate::{download_with_progress, get_mut_ref, Readline}; +use crate::{download_with_progress, get_mut_ref, m3u8::DataEntry, Readline}; 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)] @@ -84,6 +85,8 @@ pub struct Configuration { pub seen_links_path: PathBuf, pub seen_links: Vec, config_file_path: PathBuf, + pub data_dir: PathBuf, + pub datafile_content: Vec, } impl Configuration { @@ -91,31 +94,40 @@ impl Configuration { let project_dirs = ProjectDirs::from(APP_IDENTIFIER[0], APP_IDENTIFIER[1], APP_IDENTIFIER[2]).unwrap(); - // Config dir + // Make sure all the dirs for the project are setup correctly let config_dir = project_dirs.config_dir(); + let cache_dir = project_dirs.cache_dir().to_path_buf(); + let data_dir = project_dirs.data_local_dir().to_path_buf(); + let _ = [&config_dir, &cache_dir.as_path(), &data_dir.as_path()] + .iter() + .filter(|x| !x.exists()) + .map(fs::create_dir_all); + + // Config setup let config_file_path = config_dir.join(JSON_CONFIG_FILENAME).to_path_buf(); - let _ = fs::create_dir_all(config_dir); - // Read/create config let configuration = Conf::new(&config_file_path)?; fs::write( &config_file_path, serde_json::to_string(&configuration).unwrap(), )?; - // Setup dirs for playlist - let cache_dir = project_dirs.cache_dir().to_path_buf(); + // Playlist 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(); + // Datadir + let datafile = data_dir.join(STANDARD_OFFLINE_FILENAME); + let datafile_content = Self::get_datafile_content(&datafile).unwrap_or_default(); + Ok(Self { conf: configuration, playlist_path, seen_links, seen_links_path, config_file_path, + data_dir, + datafile_content, }) } @@ -127,6 +139,15 @@ impl Configuration { } } + pub fn add_datafile_ugly(&self, data_entry: DataEntry) { + unsafe { get_mut_ref(&self.datafile_content) }.push(data_entry); + } + + pub fn write_datafile(&self) -> Result<(), io::Error> { + let path = self.data_dir.join(STANDARD_OFFLINE_FILENAME); + fs::write(path, serde_json::to_string(&self.datafile_content)?) + } + fn get_watched(path: &Path) -> Option> { let reader = BufReader::new(File::open(&path).ok()?); serde_json::from_reader(reader).ok() @@ -196,6 +217,11 @@ impl Configuration { .await? .get_string() } + + fn get_datafile_content(datafile: &PathBuf) -> Option> { + let reader = BufReader::new(File::open(datafile).ok()?); + serde_json::from_reader(reader).ok() + } } impl Deref for Configuration { diff --git a/src/m3u8.rs b/src/m3u8.rs index 3f258d5..d9fa9ac 100644 --- a/src/m3u8.rs +++ b/src/m3u8.rs @@ -1,6 +1,8 @@ use colored::Colorize; -use std::fmt::Display; +use serde::{Deserialize, Serialize}; +use std::{fmt::Display, ops::Deref}; +#[derive(Serialize, Deserialize)] pub struct M3u8 { pub tvg_id: String, pub tvg_name: String, @@ -22,3 +24,22 @@ impl Display for M3u8 { Ok(()) } } + +#[derive(Serialize, Deserialize)] +pub struct DataEntry { + m3u8: M3u8, + pub path: String, +} +impl DataEntry { + pub fn new(m3u8: M3u8, path: String) -> Self { + Self { m3u8, path } + } +} + +impl Deref for DataEntry { + type Target = M3u8; + + fn deref(&self) -> &Self::Target { + &self.m3u8 + } +} From b9007713f593736e0f4ad0403e7fe9cff80387c3 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Fri, 3 Mar 2023 00:36:06 +0100 Subject: [PATCH 03/15] saves to downloadmode correctly --- src/config.rs | 11 +++++---- src/lib.rs | 2 +- src/m3u8.rs | 4 ++-- src/main.rs | 64 +++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/config.rs b/src/config.rs index aa57f62..fd2f958 100644 --- a/src/config.rs +++ b/src/config.rs @@ -98,10 +98,11 @@ impl Configuration { let config_dir = project_dirs.config_dir(); let cache_dir = project_dirs.cache_dir().to_path_buf(); let data_dir = project_dirs.data_local_dir().to_path_buf(); - let _ = [&config_dir, &cache_dir.as_path(), &data_dir.as_path()] - .iter() - .filter(|x| !x.exists()) - .map(fs::create_dir_all); + for dir in [&config_dir, &cache_dir.as_path(), &data_dir.as_path()].iter() { + if !dir.exists() { + let _ = fs::create_dir_all(dir); + } + } // Config setup let config_file_path = config_dir.join(JSON_CONFIG_FILENAME).to_path_buf(); @@ -139,7 +140,7 @@ impl Configuration { } } - pub fn add_datafile_ugly(&self, data_entry: DataEntry) { + pub fn push_datafile_ugly(&self, data_entry: DataEntry) { unsafe { get_mut_ref(&self.datafile_content) }.push(data_entry); } diff --git a/src/lib.rs b/src/lib.rs index 8bbd1ed..80467b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ mod m3u8; mod parser; use std::io::{stdin, stdout, Stdin, StdoutLock, Write}; -pub use m3u8::M3u8; +pub use m3u8::{DataEntry, M3u8}; pub use parser::Parser; mod config; pub use config::Configuration; diff --git a/src/m3u8.rs b/src/m3u8.rs index d9fa9ac..ab40a32 100644 --- a/src/m3u8.rs +++ b/src/m3u8.rs @@ -2,7 +2,7 @@ use colored::Colorize; use serde::{Deserialize, Serialize}; use std::{fmt::Display, ops::Deref}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone, Hash)] pub struct M3u8 { pub tvg_id: String, pub tvg_name: String, @@ -25,7 +25,7 @@ impl Display for M3u8 { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone, Hash)] pub struct DataEntry { m3u8: M3u8, pub path: String, diff --git a/src/main.rs b/src/main.rs index 417db1c..98166b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,9 @@ use std::process::Command; use std::rc::Rc; use colored::Colorize; -use ilovetv::{download_with_progress, get_mut_ref, Configuration, M3u8, Parser, Readline}; +use ilovetv::{ + download_with_progress, get_mut_ref, Configuration, DataEntry, M3u8, Parser, Readline, +}; #[tokio::main] async fn main() { @@ -125,9 +127,32 @@ async fn main() { } // Downloadmode "d" => { - let to_download = + let download_selections = ask_which_to_download(&mut readline, &search_result.as_ref().unwrap()); - download_m3u8(&to_download).await; + + for to_download in download_selections.iter() { + download_m3u8(to_download, None).await; + } + } + // Save to offlinemode + "o" => { + let download_selections = + 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 = 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); + } + + if let Err(e) = config.write_datafile() { + println!( + "Failed to information about downloaded entries for offline use {:?}", + e + ) + } } _ => {} } @@ -186,20 +211,25 @@ fn ask_which_to_download<'a>( ) } -async fn download_m3u8(files_to_download: &Vec<&M3u8>) { - for m3u8 in files_to_download.iter() { - let file_ending_place = m3u8.link.rfind(".").unwrap(); - let potential_file_ending = &m3u8.link[file_ending_place..]; - let file_ending = if potential_file_ending.len() > 6 { - ".mkv" - } else { - potential_file_ending - }; - let file_name = format!("{}{}", m3u8.name, file_ending); - println!("Downloading {}", &file_name); - if let Err(e) = download_with_progress(&m3u8.link, Some(&file_name)).await { - eprintln!("Failed to download {}, {:?}", &file_name, e); - } +async fn download_m3u8(file_to_download: &M3u8, path: Option<&str>) { + let file_ending_place = file_to_download.link.rfind(".").unwrap(); + let potential_file_ending = &file_to_download.link[file_ending_place..]; + let file_ending = if potential_file_ending.len() > 6 { + ".mkv" + } else { + potential_file_ending + }; + let file_name = format!("{}{}", file_to_download.name, file_ending); + println!("Downloading {}", &file_name); + let path = if let Some(path) = path { + format!("{}{}", path, file_ending) + } else { + file_name.clone() + }; + println!("{}", &path); + + if let Err(e) = download_with_progress(&file_to_download.link, Some(&path)).await { + eprintln!("Failed to download {}, {:?}", &file_name, e); } } From 2f95755adc6aef52f5c657396585caa60a50b8f8 Mon Sep 17 00:00:00 2001 From: lov3b Date: Fri, 3 Mar 2023 13:13:39 +0100 Subject: [PATCH 04/15] start --- src/config.rs | 157 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 96 insertions(+), 61 deletions(-) diff --git a/src/config.rs b/src/config.rs index fd2f958..e102a05 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use std::{ fs::{self, File}, io::{self, BufReader}, - ops::Deref, + ops::{Deref, DerefMut}, path::{Path, PathBuf}, }; @@ -9,7 +9,9 @@ use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use serde_json; -use crate::{download_with_progress, get_mut_ref, m3u8::DataEntry, Readline}; +use crate::{ + download_with_progress, downloader::DualWriter, get_mut_ref, m3u8::DataEntry, M3u8, Readline, +}; const JSON_CONFIG_FILENAME: &'static str = "config.json"; const APP_IDENTIFIER: [&'static str; 3] = ["com", "billenius", "ilovetv"]; @@ -154,65 +156,6 @@ impl Configuration { 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? @@ -232,3 +175,95 @@ 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 + } +} From c18a991fef43f07ab8215fe6c3c8c19182994955 Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 12:10:49 +0100 Subject: [PATCH 05/15] 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); + } + } +} From be3839ae679e65362fba90a2bc69fd2bbe7e102e Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 12:13:42 +0100 Subject: [PATCH 06/15] grandmother new file --- src/grandmother.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 ++- src/playlist.rs | 61 -------------------------------------------- 3 files changed, 66 insertions(+), 62 deletions(-) create mode 100644 src/grandmother.rs diff --git a/src/grandmother.rs b/src/grandmother.rs new file mode 100644 index 0000000..93c95ce --- /dev/null +++ b/src/grandmother.rs @@ -0,0 +1,63 @@ +use std::fs; + +use crate::{Configuration, Parser, Playlist, MAX_TRIES}; + +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); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 6487e8d..90f97f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ mod config; mod downloader; +mod grandmother; mod m3u8; mod parser; mod playlist; @@ -8,9 +9,10 @@ use std::io::{stdin, stdout, Stdin, StdoutLock, Write}; pub use config::Configuration; pub use downloader::download_with_progress; +pub use grandmother::GrandMother; pub use m3u8::{DataEntry, M3u8}; pub use parser::Parser; -pub use playlist::{GrandMother, Playlist}; +pub use playlist::Playlist; pub const JSON_CONFIG_FILENAME: &'static str = "config.json"; pub const APP_IDENTIFIER: [&'static str; 3] = ["com", "billenius", "ilovetv"]; diff --git a/src/playlist.rs b/src/playlist.rs index 320031a..d2ec2cd 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -1,5 +1,4 @@ use std::{ - borrow::BorrowMut, fs, ops::{Deref, DerefMut}, path::PathBuf, @@ -103,63 +102,3 @@ impl DerefMut for Playlist { &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); - } - } -} From fd41100b953e27944f13791294d1a6f0c73cf446 Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 12:14:34 +0100 Subject: [PATCH 07/15] Remove unneeded imports --- src/config.rs | 5 ++--- src/main.rs | 3 +-- src/playlist.rs | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/config.rs b/src/config.rs index 7334cda..2f7b8b0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,9 +11,8 @@ use serde::{Deserialize, Serialize}; use serde_json; use crate::{ - download_with_progress, get_mut_ref, m3u8::DataEntry, Readline, APP_IDENTIFIER, - JSON_CONFIG_FILENAME, STANDARD_OFFLINE_FILENAME, STANDARD_PLAYLIST_FILENAME, - STANDARD_SEEN_LINKS_FILENAME, + get_mut_ref, m3u8::DataEntry, Readline, APP_IDENTIFIER, JSON_CONFIG_FILENAME, + STANDARD_OFFLINE_FILENAME, STANDARD_PLAYLIST_FILENAME, STANDARD_SEEN_LINKS_FILENAME, }; #[derive(Serialize, Deserialize, Debug)] diff --git a/src/main.rs b/src/main.rs index 6a3aa08..a75ee09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,7 @@ use std::rc::Rc; use colored::Colorize; use ilovetv::{ - download_with_progress, get_mut_ref, Configuration, DataEntry, GrandMother, M3u8, Parser, - Readline, + download_with_progress, get_mut_ref, Configuration, DataEntry, GrandMother, M3u8, Readline, }; #[tokio::main] diff --git a/src/playlist.rs b/src/playlist.rs index d2ec2cd..bee4aeb 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -5,7 +5,7 @@ use std::{ rc::Rc, }; -use crate::{download_with_progress, downloader::DualWriter, Configuration, Parser, MAX_TRIES}; +use crate::{download_with_progress, downloader::DualWriter, MAX_TRIES}; pub struct Playlist { pub content: String, From c97761bc00c563a745a73b46e4790a04a5805253 Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 16:35:01 +0100 Subject: [PATCH 08/15] start with offlineparser and grandmother --- Cargo.lock | 127 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/config.rs | 25 ++++----- src/getm3u8.rs | 27 ++++++++++ src/grandmother.rs | 19 ++++--- src/lib.rs | 7 ++- src/m3u8.rs | 29 +++++++++-- src/main.rs | 39 ++++++++++++-- src/opt.rs | 37 +++++++++++++ src/parser.rs | 39 ++++++++++---- src/playlist.rs | 12 +++-- 11 files changed, 324 insertions(+), 39 deletions(-) create mode 100644 src/getm3u8.rs create mode 100644 src/opt.rs diff --git a/Cargo.lock b/Cargo.lock index bd2c44b..29c9d95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "async-compression" version = "0.3.15" @@ -21,6 +30,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-recursion" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b015a331cc64ebd1774ba119538573603427eaace0a1950c423ab971f903796" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -74,6 +94,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "colored" version = "2.0.0" @@ -304,6 +339,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -420,6 +464,7 @@ dependencies = [ name = "ilovetv" version = "1.0.0" dependencies = [ + "async-recursion", "bytes", "colored", "directories", @@ -428,6 +473,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "structopt", "tokio", ] @@ -686,6 +732,30 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.50" @@ -951,6 +1021,36 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.107" @@ -976,6 +1076,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.38" @@ -1130,6 +1239,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.10" @@ -1159,6 +1274,18 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index b1c1225..c3c5fcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "1.0.0" edition = "2021" [dependencies] +async-recursion = "1.0.2" bytes = "1.3.0" colored = "2.0.0" directories = "4.0.1" @@ -12,4 +13,5 @@ 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","rc"] } serde_json = "1.0.93" +structopt = { version = "0.3.26", features = ["color", "suggestions"] } tokio = { version = "1.24.2", features = ["full"] } diff --git a/src/config.rs b/src/config.rs index 2f7b8b0..863bfa5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use serde_json; use crate::{ - get_mut_ref, m3u8::DataEntry, Readline, APP_IDENTIFIER, JSON_CONFIG_FILENAME, + get_mut_ref, m3u8::OfflineEntry, Readline, APP_IDENTIFIER, JSON_CONFIG_FILENAME, STANDARD_OFFLINE_FILENAME, STANDARD_PLAYLIST_FILENAME, STANDARD_SEEN_LINKS_FILENAME, }; @@ -83,7 +83,7 @@ pub struct Configuration { pub seen_links: Vec, config_file_path: PathBuf, pub data_dir: PathBuf, - pub datafile_content: Vec, + pub offlinefile_content: Rc>, } impl Configuration { @@ -94,8 +94,8 @@ impl Configuration { // Make sure all the dirs for the project are setup correctly let config_dir = project_dirs.config_dir(); let cache_dir = project_dirs.cache_dir().to_path_buf(); - let data_dir = project_dirs.data_local_dir().to_path_buf(); - for dir in [&config_dir, &cache_dir.as_path(), &data_dir.as_path()].iter() { + let offline_dir = project_dirs.data_local_dir().to_path_buf(); + for dir in [&config_dir, &cache_dir.as_path(), &offline_dir.as_path()].iter() { if !dir.exists() { let _ = fs::create_dir_all(dir); } @@ -115,8 +115,9 @@ impl Configuration { let seen_links = Self::get_watched(&seen_links_path).unwrap_or_default(); // Datadir - let datafile = data_dir.join(STANDARD_OFFLINE_FILENAME); - let datafile_content = Self::get_datafile_content(&datafile).unwrap_or_default(); + let offlinefile = offline_dir.join(STANDARD_OFFLINE_FILENAME); + let offlinefile_content = + Rc::new(Self::get_offline_content(&offlinefile).unwrap_or_default()); Ok(Self { conf: configuration, @@ -124,8 +125,8 @@ impl Configuration { seen_links, seen_links_path, config_file_path, - data_dir, - datafile_content, + data_dir: offline_dir, + offlinefile_content, }) } @@ -137,13 +138,13 @@ impl Configuration { } } - pub fn push_datafile_ugly(&self, data_entry: DataEntry) { - unsafe { get_mut_ref(&self.datafile_content) }.push(data_entry); + pub fn push_offlinefile_ugly(&self, data_entry: OfflineEntry) { + unsafe { get_mut_ref(&*self.offlinefile_content) }.push(data_entry); } pub fn write_datafile(&self) -> Result<(), io::Error> { let path = self.data_dir.join(STANDARD_OFFLINE_FILENAME); - fs::write(path, serde_json::to_string(&self.datafile_content)?) + fs::write(path, serde_json::to_string(&self.offlinefile_content)?) } fn get_watched(path: &Path) -> Option> { @@ -151,7 +152,7 @@ impl Configuration { serde_json::from_reader(reader).ok() } - fn get_datafile_content(datafile: &PathBuf) -> Option> { + fn get_offline_content(datafile: &PathBuf) -> Option> { let reader = BufReader::new(File::open(datafile).ok()?); serde_json::from_reader(reader).ok() } diff --git a/src/getm3u8.rs b/src/getm3u8.rs new file mode 100644 index 0000000..47f2530 --- /dev/null +++ b/src/getm3u8.rs @@ -0,0 +1,27 @@ +use crate::M3u8; + +pub trait GetM3u8 { + fn get_m3u8(&self) -> Vec<&M3u8>; +} + +pub trait WatchedFind { + fn find(&self, name: &str) -> Vec<&M3u8>; + fn get_watched(&self) -> Vec<&M3u8>; +} + +impl WatchedFind for T +where + T: GetM3u8, +{ + fn find(&self, name: &str) -> Vec<&M3u8> { + let name = name.to_lowercase(); + self.get_m3u8() + .into_iter() + .filter(|item| item.name.to_lowercase().contains(&name) || item.tvg_id.contains(&name)) + .collect() + } + + fn get_watched(&self) -> Vec<&M3u8> { + self.get_m3u8().into_iter().filter(|x| x.watched).collect() + } +} diff --git a/src/grandmother.rs b/src/grandmother.rs index 93c95ce..9701cf6 100644 --- a/src/grandmother.rs +++ b/src/grandmother.rs @@ -1,14 +1,17 @@ use std::fs; -use crate::{Configuration, Parser, Playlist, MAX_TRIES}; +use crate::{getm3u8::WatchedFind, Configuration, GetM3u8, Parser, Playlist, MAX_TRIES}; -pub struct GrandMother { - pub parser: Parser, +pub struct GrandMother +where + T: GetM3u8, +{ + pub parser: T, pub playlist: Playlist, pub config: Configuration, } -impl GrandMother { +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(); @@ -43,8 +46,12 @@ impl GrandMother { println!("Retrying {}/{}", counter, MAX_TRIES); }; - let watched_links = self.parser.get_watched(); - let watched_links = watched_links.iter().map(|x| x.as_str()).collect(); + let watched_links = self + .parser + .get_watched() + .iter() + .map(|x| x.link.as_str()) + .collect(); self.parser = Parser::new(&content, &watched_links).await; } diff --git a/src/lib.rs b/src/lib.rs index 90f97f7..52dc792 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,17 +2,21 @@ mod config; mod downloader; mod grandmother; mod m3u8; +mod opt; mod parser; mod playlist; +mod getm3u8; use std::io::{stdin, stdout, Stdin, StdoutLock, Write}; pub use config::Configuration; pub use downloader::download_with_progress; pub use grandmother::GrandMother; -pub use m3u8::{DataEntry, M3u8}; +pub use m3u8::{OfflineEntry, M3u8}; +pub use opt::{Mode, Opt}; pub use parser::Parser; pub use playlist::Playlist; +pub use getm3u8::GetM3u8; pub const JSON_CONFIG_FILENAME: &'static str = "config.json"; pub const APP_IDENTIFIER: [&'static str; 3] = ["com", "billenius", "ilovetv"]; @@ -20,6 +24,7 @@ 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/m3u8.rs b/src/m3u8.rs index ab40a32..49d58a2 100644 --- a/src/m3u8.rs +++ b/src/m3u8.rs @@ -1,6 +1,8 @@ use colored::Colorize; use serde::{Deserialize, Serialize}; -use std::{fmt::Display, ops::Deref}; +use std::{fmt::Display, ops::Deref, rc::Rc}; + +use crate::{Configuration, GetM3u8}; #[derive(Serialize, Deserialize, Clone, Hash)] pub struct M3u8 { @@ -26,20 +28,39 @@ impl Display for M3u8 { } #[derive(Serialize, Deserialize, Clone, Hash)] -pub struct DataEntry { +pub struct OfflineEntry { m3u8: M3u8, pub path: String, } -impl DataEntry { + +impl OfflineEntry { pub fn new(m3u8: M3u8, path: String) -> Self { Self { m3u8, path } } } -impl Deref for DataEntry { +impl Deref for OfflineEntry { type Target = M3u8; fn deref(&self) -> &Self::Target { &self.m3u8 } } + +struct OfflineParser { + entries: Rc>, +} + +impl OfflineParser { + pub fn new(conf: &Configuration) -> Self { + Self { + entries: conf.offlinefile_content.clone(), + } + } +} + +impl GetM3u8 for OfflineParser { + fn get_m3u8(&self) -> Vec<&M3u8> { + self.entries.iter().map(|x| &**x).collect() + } +} diff --git a/src/main.rs b/src/main.rs index a75ee09..cfcb3a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,18 @@ use std::num::ParseIntError; use std::process::Command; use std::rc::Rc; +use async_recursion::async_recursion; use colored::Colorize; use ilovetv::{ - download_with_progress, get_mut_ref, Configuration, DataEntry, GrandMother, M3u8, Readline, + download_with_progress, get_mut_ref, Configuration, GetM3u8, GrandMother, M3u8, Mode, + OfflineEntry, Opt, Parser, Readline, }; +use structopt::StructOpt; #[tokio::main] async fn main() { + let opt = Opt::from_args(); + // Greet the user [ format!( @@ -144,8 +149,8 @@ async fn main() { 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); - gm.config.push_datafile_ugly(data_entry); + let data_entry = OfflineEntry::new((*to_download).clone(), path); + gm.config.push_offlinefile_ugly(data_entry); } if let Err(e) = gm.config.write_datafile() { @@ -170,6 +175,34 @@ async fn main() { } } +#[async_recursion(?Send)] +async fn get_gm( + mode: Mode, + readline: &mut Readline<'_>, + config: Configuration, +) -> Result, String> { + match mode { + Mode::Online => GrandMother::new(config).await, + Mode::Offline => GrandMother::new_in_offline(config), + Mode::Ask => loop { + let input = readline + .input("Online/Offline mode? [1/2]") + .trim() + .parse::(); + if let Ok(num) = input { + if num == 1 { + return get_gm(Mode::Online, readline, config).await; + } else if num == 2 { + return get_gm(Mode::Offline, readline, config).await; + } + println!("Has to be either 1 (Onine) or 2 (Offline)"); + } else { + println!("Has to be a number"); + } + }, + } +} + fn ask_which_to_download<'a>( readline: &mut Readline, search_result: &Rc>, diff --git a/src/opt.rs b/src/opt.rs new file mode 100644 index 0000000..d0939ac --- /dev/null +++ b/src/opt.rs @@ -0,0 +1,37 @@ +use std::str::FromStr; +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +#[structopt(name = "ilovetv")] +pub struct Opt { + #[structopt(short, long, default_value = "ask")] + /// Choose whether to launch in offlinemode, onlinemode or wheter to ask during startup. + /// In offlinemode it's only possible to watch downloaded entries + pub offline_mode: Mode, +} + +#[derive(Debug)] +pub enum Mode { + Online, + Offline, + Ask, +} + +impl Default for Mode { + fn default() -> Self { + Self::Ask + } +} + +impl FromStr for Mode { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "online" => Ok(Self::Online), + "offline" => Ok(Self::Offline), + "ask" | "default" => Ok(Self::Ask), + _ => Err("No such enum"), + } + } +} diff --git a/src/parser.rs b/src/parser.rs index 1e67b00..26c588d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,6 +1,12 @@ -use std::ops::Deref; +use std::{ + fs::{self, File}, + io::BufReader, + ops::Deref, +}; -use crate::m3u8::M3u8; +use serde::Serialize; + +use crate::{m3u8::M3u8, Configuration, GetM3u8}; pub struct Parser { m3u8_items: Vec, @@ -40,14 +46,6 @@ impl Parser { self.m3u8_items = Self::parse_m3u8(content, seen_links); } - 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 @@ -93,3 +91,24 @@ impl Deref for Parser { &self.m3u8_items } } + +impl GetM3u8 for Parser { + fn get_m3u8(&self) -> Vec<&M3u8> { + self.m3u8_items.iter().collect() + } +} + +#[derive(Serialize)] +struct OfflineEntry { + m3u8: M3u8, + path: String, +} +#[derive(Serialize)] +struct OfflineParser { + m3u8_items: Vec, +} +impl OfflineParser { + pub fn new(config: &Configuration) -> Result { + todo!() + } +} diff --git a/src/playlist.rs b/src/playlist.rs index bee4aeb..5f5c132 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -10,7 +10,7 @@ use crate::{download_with_progress, downloader::DualWriter, MAX_TRIES}; pub struct Playlist { pub content: String, path_to_playlist: Rc, - url: Rc, + url: Option>, } impl Playlist { @@ -18,7 +18,7 @@ impl Playlist { let mut me = Self { content: String::new(), path_to_playlist, - url, + url: Some(url), }; me.content = me.get_saved_or_download().await?; @@ -76,7 +76,13 @@ impl Playlist { loop { counter += 1; - let downloaded = download_with_progress(&self.url, None) + let url = self + .url + .as_ref() + .clone() + .ok_or_else(|| String::from("In offline mode"))?; + + let downloaded = download_with_progress(url, None) .await .and_then(DualWriter::get_string); if let Ok(content) = downloaded { From 8290b67f2595488e63680068ba3f60eeb019f586 Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 19:14:26 +0100 Subject: [PATCH 09/15] Early working version --- src/getm3u8.rs | 13 +++++--- src/grandmother.rs | 57 ++++++++++++++++++++----------- src/lib.rs | 37 ++++++++++++++++++--- src/m3u8.rs | 27 +++------------ src/main.rs | 83 +++++++++++++++++++++++----------------------- src/opt.rs | 2 +- src/parser.rs | 53 ++++++++++++++++++----------- 7 files changed, 161 insertions(+), 111 deletions(-) diff --git a/src/getm3u8.rs b/src/getm3u8.rs index 47f2530..defa90b 100644 --- a/src/getm3u8.rs +++ b/src/getm3u8.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use crate::M3u8; pub trait GetM3u8 { @@ -9,10 +11,7 @@ pub trait WatchedFind { fn get_watched(&self) -> Vec<&M3u8>; } -impl WatchedFind for T -where - T: GetM3u8, -{ +impl WatchedFind for T { fn find(&self, name: &str) -> Vec<&M3u8> { let name = name.to_lowercase(); self.get_m3u8() @@ -25,3 +24,9 @@ where self.get_m3u8().into_iter().filter(|x| x.watched).collect() } } +pub trait GetPlayPath { + fn get_path_to_play(&self, link: Rc) -> Result, String>; +} + +pub trait M3u8PlayPath: GetM3u8 + GetPlayPath {} +impl M3u8PlayPath for T {} diff --git a/src/grandmother.rs b/src/grandmother.rs index 9701cf6..ffc3fca 100644 --- a/src/grandmother.rs +++ b/src/grandmother.rs @@ -1,44 +1,61 @@ +#[allow(unused_imports)] +use crate::GetM3u8; +use crate::{ + getm3u8::{M3u8PlayPath, WatchedFind}, + Configuration, OfflineParser, Parser, Playlist, MAX_TRIES, +}; use std::fs; -use crate::{getm3u8::WatchedFind, Configuration, GetM3u8, Parser, Playlist, MAX_TRIES}; +type Error = String; -pub struct GrandMother -where - T: GetM3u8, -{ - pub parser: T, - pub playlist: Playlist, +pub struct GrandMother { + pub parser: Box, + pub playlist: Option, pub config: Configuration, } -impl GrandMother { - pub async fn new(config: Configuration) -> Result { +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; + let parser: Box = + Box::new(Parser::new(&playlist_content, &seen_links).await); Ok(Self { parser, - playlist, + playlist: Some(playlist), config, }) } - pub async fn refresh_dirty(&self) { - let ptr = self as *const Self as *mut Self; - unsafe { &mut *ptr }.refresh().await; + pub fn new_offline(config: Configuration) -> Self { + let parser: Box = Box::new(OfflineParser::new(&config)); + Self { + parser, + playlist: None, + config, + } } - pub async fn refresh(&mut self) { + pub async fn refresh_dirty(&self) -> Result<(), Error> { + let ptr = self as *const Self as *mut Self; + unsafe { &mut *ptr }.refresh().await + } + + pub async fn refresh(&mut self) -> Result<(), Error> { let mut counter = 0; let content = loop { counter += 1; - let content = self.playlist.download().await; + let content = self + .playlist + .as_ref() + .ok_or_else(|| "Cannot refresh playlist in offlinemode")? + .download() + .await; if counter > MAX_TRIES { - return; + return Ok(()); } if let Ok(content) = content { break content; @@ -52,7 +69,9 @@ impl GrandMother { .iter() .map(|x| x.link.as_str()) .collect(); - self.parser = Parser::new(&content, &watched_links).await; + self.parser = Box::new(Parser::new(&content, &watched_links).await); + + Ok(()) } pub fn save_watched(&self) { diff --git a/src/lib.rs b/src/lib.rs index 52dc792..7bf07dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,22 +1,23 @@ mod config; mod downloader; +pub mod getm3u8; mod grandmother; mod m3u8; mod opt; mod parser; mod playlist; -mod getm3u8; use std::io::{stdin, stdout, Stdin, StdoutLock, Write}; +use async_recursion::async_recursion; pub use config::Configuration; pub use downloader::download_with_progress; +pub use getm3u8::{GetM3u8, GetPlayPath, WatchedFind}; pub use grandmother::GrandMother; -pub use m3u8::{OfflineEntry, M3u8}; +pub use m3u8::{M3u8, OfflineEntry}; pub use opt::{Mode, Opt}; -pub use parser::Parser; +pub use parser::{OfflineParser, Parser}; pub use playlist::Playlist; -pub use getm3u8::GetM3u8; pub const JSON_CONFIG_FILENAME: &'static str = "config.json"; pub const APP_IDENTIFIER: [&'static str; 3] = ["com", "billenius", "ilovetv"]; @@ -56,3 +57,31 @@ pub unsafe fn get_mut_ref(reference: &T) -> &mut T { let ptr = reference as *const T as *mut T; &mut *ptr } + +#[async_recursion(?Send)] +pub async fn get_gm( + mode: Mode, + readline: &mut Readline<'_>, + config: Configuration, +) -> Result { + match mode { + Mode::Online => GrandMother::new(config).await, + Mode::Offline => Ok(GrandMother::new_offline(config)), + Mode::Ask => loop { + let input = readline + .input("Online/Offline mode? [1/2] ") + .trim() + .parse::(); + if let Ok(num) = input { + if num == 1 { + return get_gm(Mode::Online, readline, config).await; + } else if num == 2 { + return get_gm(Mode::Offline, readline, config).await; + } + println!("Has to be either 1 (Onine) or 2 (Offline)"); + } else { + println!("Has to be a number"); + } + }, + } +} diff --git a/src/m3u8.rs b/src/m3u8.rs index 49d58a2..9e74ecc 100644 --- a/src/m3u8.rs +++ b/src/m3u8.rs @@ -2,7 +2,8 @@ use colored::Colorize; use serde::{Deserialize, Serialize}; use std::{fmt::Display, ops::Deref, rc::Rc}; -use crate::{Configuration, GetM3u8}; +#[allow(unused_imports)] +use crate::GetM3u8; #[derive(Serialize, Deserialize, Clone, Hash)] pub struct M3u8 { @@ -11,7 +12,7 @@ pub struct M3u8 { pub tvg_logo: String, pub group_title: String, pub name: String, - pub link: String, + pub link: Rc, pub watched: bool, } @@ -30,11 +31,11 @@ impl Display for M3u8 { #[derive(Serialize, Deserialize, Clone, Hash)] pub struct OfflineEntry { m3u8: M3u8, - pub path: String, + pub path: Rc, } impl OfflineEntry { - pub fn new(m3u8: M3u8, path: String) -> Self { + pub fn new(m3u8: M3u8, path: Rc) -> Self { Self { m3u8, path } } } @@ -46,21 +47,3 @@ impl Deref for OfflineEntry { &self.m3u8 } } - -struct OfflineParser { - entries: Rc>, -} - -impl OfflineParser { - pub fn new(conf: &Configuration) -> Self { - Self { - entries: conf.offlinefile_content.clone(), - } - } -} - -impl GetM3u8 for OfflineParser { - fn get_m3u8(&self) -> Vec<&M3u8> { - self.entries.iter().map(|x| &**x).collect() - } -} diff --git a/src/main.rs b/src/main.rs index cfcb3a1..d7961c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,14 +2,16 @@ use std::num::ParseIntError; use std::process::Command; use std::rc::Rc; -use async_recursion::async_recursion; use colored::Colorize; -use ilovetv::{ - download_with_progress, get_mut_ref, Configuration, GetM3u8, GrandMother, M3u8, Mode, - OfflineEntry, Opt, Parser, Readline, -}; use structopt::StructOpt; +use ilovetv::{ + download_with_progress, get_gm, get_mut_ref, Configuration, M3u8, OfflineEntry, Opt, Readline, + WatchedFind, +}; +#[allow(unused_imports)] +use ilovetv::{GetM3u8, GetPlayPath, OfflineParser}; + #[tokio::main] async fn main() { let opt = Opt::from_args(); @@ -37,14 +39,21 @@ async fn main() { .iter() .for_each(|s| println!("{}", &s)); - let gm = GrandMother::new(Configuration::new().expect("Failed to write to configfile")) - .await - .unwrap(); + // 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; let mut readline = Readline::new(); + let gm = get_gm( + opt.mode, + &mut readline, + Configuration::new().expect("Failed to write to configfile"), + ) + .await + .expect("Failed to retrive online playlist"); loop { // Dont't perform a search if user has just watched, instead present the previous search @@ -61,7 +70,12 @@ async fn main() { // Refresh playlist "r" => { search_result = None; - gm.refresh_dirty().await; + if let Err(e) = gm.refresh_dirty().await { + println!( + "Cannot refresh. This is probably due to offlinemode {:?}", + e + ); + }; continue; } // Toggle fullscreen for mpv @@ -120,7 +134,12 @@ async fn main() { "r" => { println!("Refreshing local m3u8-file"); search_result = None; - gm.refresh_dirty().await; + if let Err(e) = gm.refresh_dirty().await { + println!( + "Cannot refresh. This is probably due to offlinemode {:?}", + e + ); + }; continue; } "f" => { @@ -147,7 +166,7 @@ async fn main() { for to_download in download_selections.iter() { let path = gm.config.data_dir.join(&to_download.name); - let path = path.to_string_lossy().to_string(); + let path = Rc::new(path.to_string_lossy().to_string()); download_m3u8(to_download, Some(&path)).await; let data_entry = OfflineEntry::new((*to_download).clone(), path); gm.config.push_offlinefile_ugly(data_entry); @@ -167,7 +186,15 @@ async fn main() { match choosen { Ok(k) => { let search_result = search_result.as_ref().unwrap(); - stream(search_result[k - 1], mpv_fs); + let to_play = search_result[k - 1]; + let path_link = if let Ok(link) = gm.parser.get_path_to_play(to_play.link.clone()) { + link + } else { + println!("Not possible to refresh playlist while in offlinemode"); + continue; + }; + + stream(to_play, &*path_link, mpv_fs); gm.save_watched(); } Err(e) => println!("Have to be a valid number! {:?}", e), @@ -175,34 +202,6 @@ async fn main() { } } -#[async_recursion(?Send)] -async fn get_gm( - mode: Mode, - readline: &mut Readline<'_>, - config: Configuration, -) -> Result, String> { - match mode { - Mode::Online => GrandMother::new(config).await, - Mode::Offline => GrandMother::new_in_offline(config), - Mode::Ask => loop { - let input = readline - .input("Online/Offline mode? [1/2]") - .trim() - .parse::(); - if let Ok(num) = input { - if num == 1 { - return get_gm(Mode::Online, readline, config).await; - } else if num == 2 { - return get_gm(Mode::Offline, readline, config).await; - } - println!("Has to be either 1 (Onine) or 2 (Offline)"); - } else { - println!("Has to be a number"); - } - }, - } -} - fn ask_which_to_download<'a>( readline: &mut Readline, search_result: &Rc>, @@ -272,10 +271,10 @@ async fn download_m3u8(file_to_download: &M3u8, path: Option<&str>) { * in this context and also the most efficient way. * With other words, it's BLAZINGLY FAST */ -fn stream(m3u8item: &M3u8, launch_in_fullscreen: bool) { +fn stream(m3u8item: &M3u8, link: &String, launch_in_fullscreen: bool) { let mut m3u8item = unsafe { get_mut_ref(m3u8item) }; m3u8item.watched = true; - let mut args: Vec<&str> = vec![&m3u8item.link]; + let mut args: Vec<&str> = vec![link]; if launch_in_fullscreen { args.push("--fs"); } diff --git a/src/opt.rs b/src/opt.rs index d0939ac..168e3b8 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -7,7 +7,7 @@ pub struct Opt { #[structopt(short, long, default_value = "ask")] /// Choose whether to launch in offlinemode, onlinemode or wheter to ask during startup. /// In offlinemode it's only possible to watch downloaded entries - pub offline_mode: Mode, + pub mode: Mode, } #[derive(Debug)] diff --git a/src/parser.rs b/src/parser.rs index 26c588d..8931569 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,12 +1,8 @@ -use std::{ - fs::{self, File}, - io::BufReader, - ops::Deref, -}; +use std::{ops::Deref, rc::Rc}; use serde::Serialize; -use crate::{m3u8::M3u8, Configuration, GetM3u8}; +use crate::{m3u8::M3u8, Configuration, GetM3u8, GetPlayPath, OfflineEntry}; pub struct Parser { m3u8_items: Vec, @@ -75,7 +71,7 @@ impl Parser { tvg_logo: items[2].to_owned(), group_title: items[3].to_owned(), name: name.to_owned(), - link: link.to_string(), + link: Rc::new(link.to_string()), watched: is_watched, }; m3u8_items.push(m3u8_item); @@ -98,17 +94,36 @@ impl GetM3u8 for Parser { } } -#[derive(Serialize)] -struct OfflineEntry { - m3u8: M3u8, - path: String, -} -#[derive(Serialize)] -struct OfflineParser { - m3u8_items: Vec, -} -impl OfflineParser { - pub fn new(config: &Configuration) -> Result { - todo!() +impl GetPlayPath for Parser { + fn get_path_to_play<'a>(&'a self, link: Rc) -> Result, String> { + Ok(link.clone()) + } +} +#[derive(Serialize)] +pub struct OfflineParser { + m3u8_items: Rc>, +} +impl OfflineParser { + pub fn new(config: &Configuration) -> Self { + Self { + m3u8_items: config.offlinefile_content.clone(), + } + } +} + +impl GetPlayPath for OfflineParser { + fn get_path_to_play(&self, link: Rc) -> Result, String> { + for offline_entry in &*self.m3u8_items { + if *offline_entry.link == *link { + return Ok(offline_entry.path.clone()); + } + } + Err("Not stored for offline use".to_owned()) + } +} + +impl GetM3u8 for OfflineParser { + fn get_m3u8(&self) -> Vec<&M3u8> { + self.m3u8_items.iter().map(|x| &**x).collect() } } From 624c70e2eca23316a15f5c0ccb73e6e26d494e0a Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 19:51:21 +0100 Subject: [PATCH 10/15] More refactors --- src/getm3u8.rs | 4 +-- src/grandmother.rs | 14 ++++----- src/lib.rs | 6 ++-- src/offlineparser.rs | 42 +++++++++++++++++++++++++++ src/{parser.rs => onlineparser.rs} | 46 ++++-------------------------- src/opt.rs | 3 +- src/playlist.rs | 19 +++--------- 7 files changed, 65 insertions(+), 69 deletions(-) create mode 100644 src/offlineparser.rs rename src/{parser.rs => onlineparser.rs} (71%) diff --git a/src/getm3u8.rs b/src/getm3u8.rs index defa90b..ee9e5b6 100644 --- a/src/getm3u8.rs +++ b/src/getm3u8.rs @@ -28,5 +28,5 @@ pub trait GetPlayPath { fn get_path_to_play(&self, link: Rc) -> Result, String>; } -pub trait M3u8PlayPath: GetM3u8 + GetPlayPath {} -impl M3u8PlayPath for T {} +pub trait Parser: GetM3u8 + GetPlayPath {} +impl Parser for T {} diff --git a/src/grandmother.rs b/src/grandmother.rs index ffc3fca..013b6de 100644 --- a/src/grandmother.rs +++ b/src/grandmother.rs @@ -1,15 +1,15 @@ #[allow(unused_imports)] use crate::GetM3u8; use crate::{ - getm3u8::{M3u8PlayPath, WatchedFind}, - Configuration, OfflineParser, Parser, Playlist, MAX_TRIES, + getm3u8::{Parser, WatchedFind}, + Configuration, OfflineParser, OnlineParser, Playlist, MAX_TRIES, }; use std::fs; type Error = String; pub struct GrandMother { - pub parser: Box, + pub parser: Box, pub playlist: Option, pub config: Configuration, } @@ -20,8 +20,8 @@ impl GrandMother { 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: Box = - Box::new(Parser::new(&playlist_content, &seen_links).await); + let parser: Box = + Box::new(OnlineParser::new(&playlist_content, &seen_links).await); Ok(Self { parser, @@ -31,7 +31,7 @@ impl GrandMother { } pub fn new_offline(config: Configuration) -> Self { - let parser: Box = Box::new(OfflineParser::new(&config)); + let parser: Box = Box::new(OfflineParser::new(&config)); Self { parser, playlist: None, @@ -69,7 +69,7 @@ impl GrandMother { .iter() .map(|x| x.link.as_str()) .collect(); - self.parser = Box::new(Parser::new(&content, &watched_links).await); + self.parser = Box::new(OnlineParser::new(&content, &watched_links).await); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 7bf07dc..5fb06de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,9 @@ mod downloader; pub mod getm3u8; mod grandmother; mod m3u8; +mod offlineparser; +mod onlineparser; mod opt; -mod parser; mod playlist; use std::io::{stdin, stdout, Stdin, StdoutLock, Write}; @@ -15,8 +16,9 @@ pub use downloader::download_with_progress; pub use getm3u8::{GetM3u8, GetPlayPath, WatchedFind}; pub use grandmother::GrandMother; pub use m3u8::{M3u8, OfflineEntry}; +pub use offlineparser::OfflineParser; +pub use onlineparser::OnlineParser; pub use opt::{Mode, Opt}; -pub use parser::{OfflineParser, Parser}; pub use playlist::Playlist; pub const JSON_CONFIG_FILENAME: &'static str = "config.json"; diff --git a/src/offlineparser.rs b/src/offlineparser.rs new file mode 100644 index 0000000..43960c7 --- /dev/null +++ b/src/offlineparser.rs @@ -0,0 +1,42 @@ +use std::{ops::Deref, rc::Rc}; + +use serde::Serialize; + +use crate::{m3u8::M3u8, Configuration, GetM3u8, GetPlayPath, OfflineEntry}; + +#[derive(Serialize)] +pub struct OfflineParser { + m3u8_items: Rc>, +} +impl OfflineParser { + pub fn new(config: &Configuration) -> Self { + Self { + m3u8_items: config.offlinefile_content.clone(), + } + } +} + +impl Deref for OfflineParser { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &*self.m3u8_items + } +} + +impl GetPlayPath for OfflineParser { + fn get_path_to_play(&self, link: Rc) -> Result, String> { + for offline_entry in &*self.m3u8_items { + if *offline_entry.link == *link { + return Ok(offline_entry.path.clone()); + } + } + Err("Not stored for offline use".to_owned()) + } +} + +impl GetM3u8 for OfflineParser { + fn get_m3u8(&self) -> Vec<&M3u8> { + self.m3u8_items.iter().map(|x| &**x).collect() + } +} diff --git a/src/parser.rs b/src/onlineparser.rs similarity index 71% rename from src/parser.rs rename to src/onlineparser.rs index 8931569..d6f5765 100644 --- a/src/parser.rs +++ b/src/onlineparser.rs @@ -4,11 +4,11 @@ use serde::Serialize; use crate::{m3u8::M3u8, Configuration, GetM3u8, GetPlayPath, OfflineEntry}; -pub struct Parser { +pub struct OnlineParser { m3u8_items: Vec, } -impl Parser { +impl OnlineParser { pub async fn new(m3u_content: &str, watched_links: &Vec<&str>) -> Self { Self { m3u8_items: Self::parse_m3u8(m3u_content, watched_links), @@ -23,14 +23,6 @@ impl Parser { .collect() } - /* - * 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; - // } - pub async fn forcefully_update(&mut self, content: &str) { let seen_links: &Vec<&str> = &self .m3u8_items @@ -80,7 +72,7 @@ impl Parser { } } -impl Deref for Parser { +impl Deref for OnlineParser { type Target = Vec; fn deref(&self) -> &Self::Target { @@ -88,42 +80,14 @@ impl Deref for Parser { } } -impl GetM3u8 for Parser { +impl GetM3u8 for OnlineParser { fn get_m3u8(&self) -> Vec<&M3u8> { self.m3u8_items.iter().collect() } } -impl GetPlayPath for Parser { +impl GetPlayPath for OnlineParser { fn get_path_to_play<'a>(&'a self, link: Rc) -> Result, String> { Ok(link.clone()) } } -#[derive(Serialize)] -pub struct OfflineParser { - m3u8_items: Rc>, -} -impl OfflineParser { - pub fn new(config: &Configuration) -> Self { - Self { - m3u8_items: config.offlinefile_content.clone(), - } - } -} - -impl GetPlayPath for OfflineParser { - fn get_path_to_play(&self, link: Rc) -> Result, String> { - for offline_entry in &*self.m3u8_items { - if *offline_entry.link == *link { - return Ok(offline_entry.path.clone()); - } - } - Err("Not stored for offline use".to_owned()) - } -} - -impl GetM3u8 for OfflineParser { - fn get_m3u8(&self) -> Vec<&M3u8> { - self.m3u8_items.iter().map(|x| &**x).collect() - } -} diff --git a/src/opt.rs b/src/opt.rs index 168e3b8..e728fe2 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -5,8 +5,7 @@ use structopt::StructOpt; #[structopt(name = "ilovetv")] pub struct Opt { #[structopt(short, long, default_value = "ask")] - /// Choose whether to launch in offlinemode, onlinemode or wheter to ask during startup. - /// In offlinemode it's only possible to watch downloaded entries + /// Possible options: online, offline and ask pub mode: Mode, } diff --git a/src/playlist.rs b/src/playlist.rs index 5f5c132..5aecba4 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -1,9 +1,4 @@ -use std::{ - fs, - ops::{Deref, DerefMut}, - path::PathBuf, - rc::Rc, -}; +use std::{fs, ops::Deref, path::PathBuf, rc::Rc}; use crate::{download_with_progress, downloader::DualWriter, MAX_TRIES}; @@ -79,10 +74,10 @@ impl Playlist { let url = self .url .as_ref() - .clone() - .ok_or_else(|| String::from("In offline mode"))?; + .ok_or_else(|| String::from("In offline mode"))? + .clone(); - let downloaded = download_with_progress(url, None) + let downloaded = download_with_progress(&url, None) .await .and_then(DualWriter::get_string); if let Ok(content) = downloaded { @@ -102,9 +97,3 @@ impl Deref for Playlist { &self.content } } - -impl DerefMut for Playlist { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.content - } -} From fa12365719fa39c7c0e8e144ddfc9e243e0131f8 Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 19:55:46 +0100 Subject: [PATCH 11/15] remove unneeded imports --- src/onlineparser.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/onlineparser.rs b/src/onlineparser.rs index d6f5765..c87b60b 100644 --- a/src/onlineparser.rs +++ b/src/onlineparser.rs @@ -1,8 +1,6 @@ use std::{ops::Deref, rc::Rc}; -use serde::Serialize; - -use crate::{m3u8::M3u8, Configuration, GetM3u8, GetPlayPath, OfflineEntry}; +use crate::{m3u8::M3u8, GetM3u8, GetPlayPath}; pub struct OnlineParser { m3u8_items: Vec, From 618e8f97ce7f4bf3b84b1f9f4019a615a9012b25 Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 20:02:26 +0100 Subject: [PATCH 12/15] consider the traits parser instead --- src/grandmother.rs | 2 +- src/lib.rs | 4 ++-- src/{getm3u8.rs => parser.rs} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename src/{getm3u8.rs => parser.rs} (100%) diff --git a/src/grandmother.rs b/src/grandmother.rs index 013b6de..3dc824c 100644 --- a/src/grandmother.rs +++ b/src/grandmother.rs @@ -1,7 +1,7 @@ #[allow(unused_imports)] use crate::GetM3u8; use crate::{ - getm3u8::{Parser, WatchedFind}, + parser::{Parser, WatchedFind}, Configuration, OfflineParser, OnlineParser, Playlist, MAX_TRIES, }; use std::fs; diff --git a/src/lib.rs b/src/lib.rs index 5fb06de..da785fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ mod config; mod downloader; -pub mod getm3u8; +pub mod parser; mod grandmother; mod m3u8; mod offlineparser; @@ -13,7 +13,7 @@ use std::io::{stdin, stdout, Stdin, StdoutLock, Write}; use async_recursion::async_recursion; pub use config::Configuration; pub use downloader::download_with_progress; -pub use getm3u8::{GetM3u8, GetPlayPath, WatchedFind}; +pub use parser::{GetM3u8, GetPlayPath, WatchedFind}; pub use grandmother::GrandMother; pub use m3u8::{M3u8, OfflineEntry}; pub use offlineparser::OfflineParser; diff --git a/src/getm3u8.rs b/src/parser.rs similarity index 100% rename from src/getm3u8.rs rename to src/parser.rs From bf751c1cee9cb183531d267e240a5023f321f443 Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 20:26:52 +0100 Subject: [PATCH 13/15] Functionality for switching to online from offline --- src/grandmother.rs | 15 +++++++++++---- src/lib.rs | 10 +++++----- src/main.rs | 26 ++++++++++++++++++++++---- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/grandmother.rs b/src/grandmother.rs index 3dc824c..ea04f67 100644 --- a/src/grandmother.rs +++ b/src/grandmother.rs @@ -4,18 +4,18 @@ use crate::{ parser::{Parser, WatchedFind}, Configuration, OfflineParser, OnlineParser, Playlist, MAX_TRIES, }; -use std::fs; +use std::{fs, rc::Rc}; type Error = String; pub struct GrandMother { pub parser: Box, pub playlist: Option, - pub config: Configuration, + pub config: Rc, } impl GrandMother { - pub async fn new(config: Configuration) -> Result { + pub async fn new_online(config: Rc) -> 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?; @@ -30,7 +30,14 @@ impl GrandMother { }) } - pub fn new_offline(config: Configuration) -> Self { + pub async fn promote_to_online(&mut self) -> Result<(), Error> { + let online_mother = GrandMother::new_online(self.config.clone()).await?; + (self.parser, self.playlist) = (online_mother.parser, online_mother.playlist); + + Ok(()) + } + + pub fn new_offline(config: Rc) -> Self { let parser: Box = Box::new(OfflineParser::new(&config)); Self { parser, diff --git a/src/lib.rs b/src/lib.rs index da785fa..4a74237 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,24 +1,24 @@ mod config; mod downloader; -pub mod parser; mod grandmother; mod m3u8; mod offlineparser; mod onlineparser; mod opt; +pub mod parser; mod playlist; -use std::io::{stdin, stdout, Stdin, StdoutLock, Write}; +use std::{io::{stdin, stdout, Stdin, StdoutLock, Write}, rc::Rc}; use async_recursion::async_recursion; pub use config::Configuration; pub use downloader::download_with_progress; -pub use parser::{GetM3u8, GetPlayPath, WatchedFind}; pub use grandmother::GrandMother; pub use m3u8::{M3u8, OfflineEntry}; pub use offlineparser::OfflineParser; pub use onlineparser::OnlineParser; pub use opt::{Mode, Opt}; +pub use parser::{GetM3u8, GetPlayPath, WatchedFind}; pub use playlist::Playlist; pub const JSON_CONFIG_FILENAME: &'static str = "config.json"; @@ -64,10 +64,10 @@ pub unsafe fn get_mut_ref(reference: &T) -> &mut T { pub async fn get_gm( mode: Mode, readline: &mut Readline<'_>, - config: Configuration, + config: Rc, ) -> Result { match mode { - Mode::Online => GrandMother::new(config).await, + Mode::Online => GrandMother::new_online(config).await, Mode::Offline => Ok(GrandMother::new_offline(config)), Mode::Ask => loop { let input = readline diff --git a/src/main.rs b/src/main.rs index d7961c6..2c517b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,11 @@ async fn main() { format!(" {} is to refresh the local iptvfile.", "r".bold()), format!(" {} is to quit and save watched fields", "q".bold()), format!(" {} is to download fields", "d".bold()), + format!( + " {} is to make entries availibe for offline use later on in the program", + "o".bold() + ), + format!(" {} is to switch to online mode", "m".bold()), format!(" {} is to perform a new search", "s".bold()), format!(" {} is to select all", "a".bold()), format!(" {} is to toggtle fullscreen for mpv", "f".bold()), @@ -50,7 +55,7 @@ async fn main() { let gm = get_gm( opt.mode, &mut readline, - Configuration::new().expect("Failed to write to configfile"), + Rc::new(Configuration::new().expect("Failed to write to configfile")), ) .await .expect("Failed to retrive online playlist"); @@ -59,7 +64,7 @@ async fn main() { // Dont't perform a search if user has just watched, instead present the previous search if search_result.is_none() { let search = readline - .input("Search by name [ r/q/f/l ]: ") + .input("Search by name [ r/q/f/l/m ]: ") .to_lowercase(); let mut search = search.trim(); @@ -99,6 +104,13 @@ async fn main() { gm.config.update_last_search_ugly(None); continue; } + "m" => { + let result = unsafe { get_mut_ref(&gm) }.promote_to_online().await; + if let Err(e) = result { + println!("Failed to switch to onlinemode {:?}", e); + } + continue; + } _ => {} } search_result = Some(Rc::new(gm.parser.find(search))); @@ -117,7 +129,7 @@ async fn main() { } let user_wish = readline - .input("Which one do you wish to stream? [ q/f/s/r/d ]: ") + .input("Which one do you wish to stream? [ q/f/s/r/d/o/m ]: ") .to_lowercase(); let user_wish = user_wish.trim(); @@ -179,6 +191,13 @@ async fn main() { ) } } + "m" => { + let result = unsafe { get_mut_ref(&gm) }.promote_to_online().await; + if let Err(e) = result { + println!("Failed to switch to onlinemode {:?}", e); + } + continue; + } _ => {} } @@ -259,7 +278,6 @@ async fn download_m3u8(file_to_download: &M3u8, path: Option<&str>) { } else { file_name.clone() }; - println!("{}", &path); if let Err(e) = download_with_progress(&file_to_download.link, Some(&path)).await { eprintln!("Failed to download {}, {:?}", &file_name, e); From 2d5f795bfb9839243a7e49d39fd2c0eaa2438ef7 Mon Sep 17 00:00:00 2001 From: lov3b Date: Mon, 6 Mar 2023 07:03:26 +0100 Subject: [PATCH 14/15] Fix parsing error when leaving download/offline-mode --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index 2c517b9..4b1b7ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -170,6 +170,7 @@ async fn main() { for to_download in download_selections.iter() { download_m3u8(to_download, None).await; } + continue; } // Save to offlinemode "o" => { @@ -190,6 +191,7 @@ async fn main() { e ) } + continue; } "m" => { let result = unsafe { get_mut_ref(&gm) }.promote_to_online().await; From 8a1ad6c9877d6d3f8aaae1f23da5fa067d863161 Mon Sep 17 00:00:00 2001 From: lov3b Date: Mon, 6 Mar 2023 07:04:50 +0100 Subject: [PATCH 15/15] use get_ref_mut --- src/grandmother.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/grandmother.rs b/src/grandmother.rs index ea04f67..10758fe 100644 --- a/src/grandmother.rs +++ b/src/grandmother.rs @@ -1,6 +1,7 @@ #[allow(unused_imports)] use crate::GetM3u8; use crate::{ + get_mut_ref, parser::{Parser, WatchedFind}, Configuration, OfflineParser, OnlineParser, Playlist, MAX_TRIES, }; @@ -47,8 +48,7 @@ impl GrandMother { } pub async fn refresh_dirty(&self) -> Result<(), Error> { - let ptr = self as *const Self as *mut Self; - unsafe { &mut *ptr }.refresh().await + unsafe { get_mut_ref(self) }.refresh().await } pub async fn refresh(&mut self) -> Result<(), Error> {