From 2b30b5335f1bd6ff8636c6535ad7a4f7e4e768c7 Mon Sep 17 00:00:00 2001 From: lov3b Date: Sun, 5 Mar 2023 16:35:01 +0100 Subject: [PATCH] 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 {