Merge branch 'offline_mode'

This commit is contained in:
Love 2023-03-06 07:11:44 +01:00
commit 224970bf6c
12 changed files with 750 additions and 235 deletions

127
Cargo.lock generated
View File

@ -8,6 +8,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 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]] [[package]]
name = "async-compression" name = "async-compression"
version = "0.3.15" version = "0.3.15"
@ -21,6 +30,17 @@ dependencies = [
"tokio", "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]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -74,6 +94,21 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "colored" name = "colored"
version = "2.0.0" version = "2.0.0"
@ -304,6 +339,15 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 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]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.1.19"
@ -420,6 +464,7 @@ dependencies = [
name = "ilovetv" name = "ilovetv"
version = "1.0.0" version = "1.0.0"
dependencies = [ dependencies = [
"async-recursion",
"bytes", "bytes",
"colored", "colored",
"directories", "directories",
@ -428,6 +473,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"structopt",
"tokio", "tokio",
] ]
@ -686,6 +732,30 @@ version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.50" version = "1.0.50"
@ -951,6 +1021,36 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 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]] [[package]]
name = "syn" name = "syn"
version = "1.0.107" version = "1.0.107"
@ -976,6 +1076,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.38" version = "1.0.38"
@ -1130,6 +1239,12 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.10" version = "0.1.10"
@ -1159,6 +1274,18 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 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]] [[package]]
name = "want" name = "want"
version = "0.3.0" version = "0.3.0"

View File

@ -4,12 +4,14 @@ version = "1.0.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
async-recursion = "1.0.2"
bytes = "1.3.0" bytes = "1.3.0"
colored = "2.0.0" colored = "2.0.0"
directories = "4.0.1" directories = "4.0.1"
futures-util = "0.3.25" futures-util = "0.3.25"
indicatif = { version = "0.17.3", features = ["tokio"] } indicatif = { version = "0.17.3", features = ["tokio"] }
reqwest = { version = "0.11.13", features = ["blocking", "deflate", "gzip", "rustls", "rustls-tls", "stream"] } 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" serde_json = "1.0.93"
structopt = { version = "0.3.26", features = ["color", "suggestions"] }
tokio = { version = "1.24.2", features = ["full"] } tokio = { version = "1.24.2", features = ["full"] }

View File

@ -3,24 +3,22 @@ use std::{
io::{self, BufReader}, io::{self, BufReader},
ops::Deref, ops::Deref,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc,
}; };
use directories::ProjectDirs; use directories::ProjectDirs;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json; use serde_json;
use crate::{download_with_progress, get_mut_ref, Readline}; use crate::{
get_mut_ref, m3u8::OfflineEntry, Readline, APP_IDENTIFIER, JSON_CONFIG_FILENAME,
const JSON_CONFIG_FILENAME: &'static str = "config.json"; STANDARD_OFFLINE_FILENAME, STANDARD_PLAYLIST_FILENAME, STANDARD_SEEN_LINKS_FILENAME,
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 MAX_TRIES: u8 = 4;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Conf { pub struct Conf {
pub playlist_filename: String, pub playlist_filename: String,
pub playlist_url: String, pub playlist_url: Rc<String>,
pub last_search: Option<String>, pub last_search: Option<String>,
pub seen_links_filename: String, pub seen_links_filename: String,
} }
@ -42,7 +40,7 @@ impl Conf {
} }
// Get fresh config with url from user // Get fresh config with url from user
let playlist_url = Self::user_setup(); let playlist_url = Self::user_setup().into();
Ok(Self { Ok(Self {
playlist_filename: STANDARD_PLAYLIST_FILENAME.to_owned(), playlist_filename: STANDARD_PLAYLIST_FILENAME.to_owned(),
@ -80,41 +78,55 @@ impl Conf {
pub struct Configuration { pub struct Configuration {
pub conf: Conf, pub conf: Conf,
pub playlist_path: PathBuf, pub playlist_path: Rc<PathBuf>,
pub seen_links_path: PathBuf, pub seen_links_path: PathBuf,
pub seen_links: Vec<String>, pub seen_links: Vec<String>,
config_file_path: PathBuf, config_file_path: PathBuf,
pub data_dir: PathBuf,
pub offlinefile_content: Rc<Vec<OfflineEntry>>,
} }
impl Configuration { impl Configuration {
pub fn new() -> Result<Self, io::Error> { pub fn new() -> Result<Self, io::Error> {
let project_dirs = let project_dirs =
ProjectDirs::from(APP_IDENTIFIER[0], APP_IDENTIFIER[1], APP_IDENTIFIER[2]).unwrap(); ProjectDirs::from(APP_IDENTIFIER[0], APP_IDENTIFIER[1], APP_IDENTIFIER[2]).unwrap();
// Make sure all the dirs for the project are setup correctly
let config_dir = project_dirs.config_dir(); let config_dir = project_dirs.config_dir();
let _ = fs::create_dir_all(config_dir); let cache_dir = project_dirs.cache_dir().to_path_buf();
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);
}
}
// Config setup
let config_file_path = config_dir.join(JSON_CONFIG_FILENAME).to_path_buf(); let config_file_path = config_dir.join(JSON_CONFIG_FILENAME).to_path_buf();
let configuration = Conf::new(&config_file_path)?; let configuration = Conf::new(&config_file_path)?;
fs::write( fs::write(
&config_file_path, &config_file_path,
serde_json::to_string(&configuration).unwrap(), serde_json::to_string(&configuration).unwrap(),
)?; )?;
// Setup dirs for playlist // Playlist
let cache_dir = project_dirs.cache_dir().to_path_buf(); let playlist_path = cache_dir.join(&configuration.playlist_filename).into();
let playlist_path = cache_dir.join(&configuration.playlist_filename);
let seen_links_path = cache_dir.join(&configuration.seen_links_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(); let seen_links = Self::get_watched(&seen_links_path).unwrap_or_default();
// Datadir
let offlinefile = offline_dir.join(STANDARD_OFFLINE_FILENAME);
let offlinefile_content =
Rc::new(Self::get_offline_content(&offlinefile).unwrap_or_default());
Ok(Self { Ok(Self {
conf: configuration, conf: configuration,
playlist_path, playlist_path,
seen_links, seen_links,
seen_links_path, seen_links_path,
config_file_path, config_file_path,
data_dir: offline_dir,
offlinefile_content,
}) })
} }
@ -126,74 +138,23 @@ impl Configuration {
} }
} }
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.offlinefile_content)?)
}
fn get_watched(path: &Path) -> Option<Vec<String>> { fn get_watched(path: &Path) -> Option<Vec<String>> {
let reader = BufReader::new(File::open(&path).ok()?); let reader = BufReader::new(File::open(&path).ok()?);
serde_json::from_reader(reader).ok() serde_json::from_reader(reader).ok()
} }
fn should_update_playlist(&self) -> bool { fn get_offline_content(datafile: &PathBuf) -> Option<Vec<OfflineEntry>> {
fs::metadata(&self.playlist_path) let reader = BufReader::new(File::open(datafile).ok()?);
.and_then(|metadata| { serde_json::from_reader(reader).ok()
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<String, String> {
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<String> {
if !self.should_update_playlist() {
return fs::read_to_string(&self.playlist_path).ok();
}
None
}
pub async fn download_playlist(&self) -> Result<String, String> {
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<String, String> {
download_with_progress(&self.playlist_url, None)
.await?
.get_string()
} }
} }

96
src/grandmother.rs Normal file
View File

@ -0,0 +1,96 @@
#[allow(unused_imports)]
use crate::GetM3u8;
use crate::{
get_mut_ref,
parser::{Parser, WatchedFind},
Configuration, OfflineParser, OnlineParser, Playlist, MAX_TRIES,
};
use std::{fs, rc::Rc};
type Error = String;
pub struct GrandMother {
pub parser: Box<dyn Parser>,
pub playlist: Option<Playlist>,
pub config: Rc<Configuration>,
}
impl GrandMother {
pub async fn new_online(config: Rc<Configuration>) -> Result<Self, Error> {
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: Box<dyn Parser> =
Box::new(OnlineParser::new(&playlist_content, &seen_links).await);
Ok(Self {
parser,
playlist: Some(playlist),
config,
})
}
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<Configuration>) -> Self {
let parser: Box<dyn Parser> = Box::new(OfflineParser::new(&config));
Self {
parser,
playlist: None,
config,
}
}
pub async fn refresh_dirty(&self) -> Result<(), Error> {
unsafe { get_mut_ref(self) }.refresh().await
}
pub async fn refresh(&mut self) -> Result<(), Error> {
let mut counter = 0;
let content = loop {
counter += 1;
let content = self
.playlist
.as_ref()
.ok_or_else(|| "Cannot refresh playlist in offlinemode")?
.download()
.await;
if counter > MAX_TRIES {
return Ok(());
}
if let Ok(content) = content {
break content;
}
println!("Retrying {}/{}", counter, MAX_TRIES);
};
let watched_links = self
.parser
.get_watched()
.iter()
.map(|x| x.link.as_str())
.collect();
self.parser = Box::new(OnlineParser::new(&content, &watched_links).await);
Ok(())
}
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);
}
}
}

View File

@ -1,13 +1,32 @@
mod m3u8;
mod parser;
use std::io::{stdin, stdout, Stdin, StdoutLock, Write};
pub use m3u8::M3u8;
pub use parser::Parser;
mod config; mod config;
pub use config::Configuration;
mod downloader; mod downloader;
mod grandmother;
mod m3u8;
mod offlineparser;
mod onlineparser;
mod opt;
pub mod parser;
mod playlist;
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 downloader::download_with_progress;
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";
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> { pub struct Readline<'a> {
stdout: StdoutLock<'a>, stdout: StdoutLock<'a>,
@ -40,3 +59,31 @@ pub unsafe fn get_mut_ref<T>(reference: &T) -> &mut T {
let ptr = reference as *const T as *mut T; let ptr = reference as *const T as *mut T;
&mut *ptr &mut *ptr
} }
#[async_recursion(?Send)]
pub async fn get_gm(
mode: Mode,
readline: &mut Readline<'_>,
config: Rc<Configuration>,
) -> Result<GrandMother, String> {
match mode {
Mode::Online => GrandMother::new_online(config).await,
Mode::Offline => Ok(GrandMother::new_offline(config)),
Mode::Ask => loop {
let input = readline
.input("Online/Offline mode? [1/2] ")
.trim()
.parse::<u8>();
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");
}
},
}
}

View File

@ -1,13 +1,18 @@
use colored::Colorize; use colored::Colorize;
use std::fmt::Display; use serde::{Deserialize, Serialize};
use std::{fmt::Display, ops::Deref, rc::Rc};
#[allow(unused_imports)]
use crate::GetM3u8;
#[derive(Serialize, Deserialize, Clone, Hash)]
pub struct M3u8 { pub struct M3u8 {
pub tvg_id: String, pub tvg_id: String,
pub tvg_name: String, pub tvg_name: String,
pub tvg_logo: String, pub tvg_logo: String,
pub group_title: String, pub group_title: String,
pub name: String, pub name: String,
pub link: String, pub link: Rc<String>,
pub watched: bool, pub watched: bool,
} }
@ -22,3 +27,23 @@ impl Display for M3u8 {
Ok(()) Ok(())
} }
} }
#[derive(Serialize, Deserialize, Clone, Hash)]
pub struct OfflineEntry {
m3u8: M3u8,
pub path: Rc<String>,
}
impl OfflineEntry {
pub fn new(m3u8: M3u8, path: Rc<String>) -> Self {
Self { m3u8, path }
}
}
impl Deref for OfflineEntry {
type Target = M3u8;
fn deref(&self) -> &Self::Target {
&self.m3u8
}
}

View File

@ -3,10 +3,19 @@ use std::process::Command;
use std::rc::Rc; use std::rc::Rc;
use colored::Colorize; use colored::Colorize;
use ilovetv::{download_with_progress, get_mut_ref, Configuration, M3u8, 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] #[tokio::main]
async fn main() { async fn main() {
let opt = Opt::from_args();
// Greet the user // Greet the user
[ [
format!( format!(
@ -18,6 +27,11 @@ async fn main() {
format!(" {} is to refresh the local iptvfile.", "r".bold()), format!(" {} is to refresh the local iptvfile.", "r".bold()),
format!(" {} is to quit and save watched fields", "q".bold()), format!(" {} is to quit and save watched fields", "q".bold()),
format!(" {} is to download fields", "d".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 perform a new search", "s".bold()),
format!(" {} is to select all", "a".bold()), format!(" {} is to select all", "a".bold()),
format!(" {} is to toggtle fullscreen for mpv", "f".bold()), format!(" {} is to toggtle fullscreen for mpv", "f".bold()),
@ -30,19 +44,27 @@ async fn main() {
.iter() .iter()
.for_each(|s| println!("{}", &s)); .for_each(|s| println!("{}", &s));
let config = Rc::new(Configuration::new().expect("Failed to write to configfile")); // let gm = GrandMother::new(Configuration::new().expect("Failed to write to configfile"))
// .await
let parser = Parser::new(config.clone()).await; // .unwrap();
// let parser = Parser::new(config.clone()).await;
let mut mpv_fs = false; let mut mpv_fs = false;
let mut search_result: Option<Rc<Vec<&M3u8>>> = None; let mut search_result: Option<Rc<Vec<&M3u8>>> = None;
let mut readline = Readline::new(); let mut readline = Readline::new();
let gm = get_gm(
opt.mode,
&mut readline,
Rc::new(Configuration::new().expect("Failed to write to configfile")),
)
.await
.expect("Failed to retrive online playlist");
loop { loop {
// Dont't perform a search if user has just watched, instead present the previous search // Dont't perform a search if user has just watched, instead present the previous search
if search_result.is_none() { if search_result.is_none() {
let search = readline let search = readline
.input("Search by name [ r/q/f/l ]: ") .input("Search by name [ r/q/f/l/m ]: ")
.to_lowercase(); .to_lowercase();
let mut search = search.trim(); let mut search = search.trim();
@ -53,7 +75,12 @@ async fn main() {
// Refresh playlist // Refresh playlist
"r" => { "r" => {
search_result = None; search_result = None;
refresh(&parser).await; if let Err(e) = gm.refresh_dirty().await {
println!(
"Cannot refresh. This is probably due to offlinemode {:?}",
e
);
};
continue; continue;
} }
// Toggle fullscreen for mpv // Toggle fullscreen for mpv
@ -66,7 +93,7 @@ async fn main() {
continue; continue;
} }
"l" => { "l" => {
search = if let Some(s) = config.last_search.as_ref() { search = if let Some(s) = gm.config.last_search.as_ref() {
s s
} else { } else {
println!("There is no search saved from earlier"); println!("There is no search saved from earlier");
@ -74,19 +101,26 @@ async fn main() {
}; };
} }
"c" => { "c" => {
config.update_last_search_ugly(None); 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; 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() { if search_result.as_ref().unwrap().is_empty() {
println!("Nothing found"); println!("Nothing found");
search_result = None; search_result = None;
continue; 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 // Let them choose which one to stream
@ -95,7 +129,7 @@ async fn main() {
} }
let user_wish = readline 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(); .to_lowercase();
let user_wish = user_wish.trim(); let user_wish = user_wish.trim();
@ -112,7 +146,12 @@ async fn main() {
"r" => { "r" => {
println!("Refreshing local m3u8-file"); println!("Refreshing local m3u8-file");
search_result = None; search_result = None;
refresh(&parser).await; if let Err(e) = gm.refresh_dirty().await {
println!(
"Cannot refresh. This is probably due to offlinemode {:?}",
e
);
};
continue; continue;
} }
"f" => { "f" => {
@ -125,9 +164,41 @@ async fn main() {
} }
// Downloadmode // Downloadmode
"d" => { "d" => {
let to_download = let download_selections =
ask_which_to_download(&mut readline, &search_result.as_ref().unwrap()); 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;
}
continue;
}
// 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 = gm.config.data_dir.join(&to_download.name);
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);
}
if let Err(e) = gm.config.write_datafile() {
println!(
"Failed to information about downloaded entries for offline use {:?}",
e
)
}
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;
} }
_ => {} _ => {}
} }
@ -136,8 +207,16 @@ async fn main() {
match choosen { match choosen {
Ok(k) => { Ok(k) => {
let search_result = search_result.as_ref().unwrap(); let search_result = search_result.as_ref().unwrap();
stream(search_result[k - 1], mpv_fs); let to_play = search_result[k - 1];
parser.save_watched(); 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), Err(e) => println!("Have to be a valid number! {:?}", e),
} }
@ -186,20 +265,24 @@ fn ask_which_to_download<'a>(
) )
} }
async fn download_m3u8(files_to_download: &Vec<&M3u8>) { async fn download_m3u8(file_to_download: &M3u8, path: Option<&str>) {
for m3u8 in files_to_download.iter() { let file_ending_place = file_to_download.link.rfind(".").unwrap();
let file_ending_place = m3u8.link.rfind(".").unwrap(); let potential_file_ending = &file_to_download.link[file_ending_place..];
let potential_file_ending = &m3u8.link[file_ending_place..]; let file_ending = if potential_file_ending.len() > 6 {
let file_ending = if potential_file_ending.len() > 6 { ".mkv"
".mkv" } else {
} else { potential_file_ending
potential_file_ending };
}; let file_name = format!("{}{}", file_to_download.name, file_ending);
let file_name = format!("{}{}", m3u8.name, file_ending); println!("Downloading {}", &file_name);
println!("Downloading {}", &file_name); let path = if let Some(path) = path {
if let Err(e) = download_with_progress(&m3u8.link, Some(&file_name)).await { format!("{}{}", path, file_ending)
eprintln!("Failed to download {}, {:?}", &file_name, e); } else {
} file_name.clone()
};
if let Err(e) = download_with_progress(&file_to_download.link, Some(&path)).await {
eprintln!("Failed to download {}, {:?}", &file_name, e);
} }
} }
@ -208,10 +291,10 @@ async fn download_m3u8(files_to_download: &Vec<&M3u8>) {
* in this context and also the most efficient way. * in this context and also the most efficient way.
* With other words, it's BLAZINGLY FAST * 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) }; let mut m3u8item = unsafe { get_mut_ref(m3u8item) };
m3u8item.watched = true; m3u8item.watched = true;
let mut args: Vec<&str> = vec![&m3u8item.link]; let mut args: Vec<&str> = vec![link];
if launch_in_fullscreen { if launch_in_fullscreen {
args.push("--fs"); args.push("--fs");
} }
@ -221,11 +304,3 @@ fn stream(m3u8item: &M3u8, launch_in_fullscreen: bool) {
.output() .output()
.expect("Could not listen for 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;
}

42
src/offlineparser.rs Normal file
View File

@ -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<Vec<OfflineEntry>>,
}
impl OfflineParser {
pub fn new(config: &Configuration) -> Self {
Self {
m3u8_items: config.offlinefile_content.clone(),
}
}
}
impl Deref for OfflineParser {
type Target = Vec<OfflineEntry>;
fn deref(&self) -> &Self::Target {
&*self.m3u8_items
}
}
impl GetPlayPath for OfflineParser {
fn get_path_to_play(&self, link: Rc<String>) -> Result<Rc<String>, 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()
}
}

91
src/onlineparser.rs Normal file
View File

@ -0,0 +1,91 @@
use std::{ops::Deref, rc::Rc};
use crate::{m3u8::M3u8, GetM3u8, GetPlayPath};
pub struct OnlineParser {
m3u8_items: Vec<M3u8>,
}
impl OnlineParser {
pub async fn new(m3u_content: &str, watched_links: &Vec<&str>) -> Self {
Self {
m3u8_items: Self::parse_m3u8(m3u_content, watched_links),
}
}
pub fn find(&self, name: &str) -> Vec<&M3u8> {
let name = name.to_lowercase();
self.m3u8_items
.iter()
.filter(|item| item.name.to_lowercase().contains(&name) || item.tvg_id.contains(&name))
.collect()
}
pub async fn forcefully_update(&mut self, content: &str) {
let seen_links: &Vec<&str> = &self
.m3u8_items
.iter()
.filter(|x| x.watched)
.map(|x| x.link.as_str())
.collect();
self.m3u8_items = Self::parse_m3u8(content, seen_links);
}
fn parse_m3u8(content: &str, watched_links: &Vec<&str>) -> Vec<M3u8> {
let mut m3u8_items: Vec<M3u8> = Vec::new();
let interesting_lines: Vec<String> = content
.replacen("#EXTM3U\n", "", 1)
.lines()
.map(str::trim)
.map(String::from)
.collect();
for i in (0..interesting_lines.len()).step_by(2) {
let mut items = Vec::new();
for to_find in ["tvg-id", "tvg-name", "tvg-logo", "group-title"] {
let offset: usize = format!("{}=", to_find).bytes().len();
let start: usize =
interesting_lines[i].find(&format!("{}=", to_find)).unwrap() as usize + offset;
let end: usize = interesting_lines[i].rfind("=").unwrap();
items.push(&interesting_lines[i][start..=end])
}
let name_start = interesting_lines[i].rfind(",").unwrap() + 1;
let name = &interesting_lines[i][name_start..];
let link = interesting_lines[i + 1].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(),
tvg_logo: items[2].to_owned(),
group_title: items[3].to_owned(),
name: name.to_owned(),
link: Rc::new(link.to_string()),
watched: is_watched,
};
m3u8_items.push(m3u8_item);
}
m3u8_items
}
}
impl Deref for OnlineParser {
type Target = Vec<M3u8>;
fn deref(&self) -> &Self::Target {
&self.m3u8_items
}
}
impl GetM3u8 for OnlineParser {
fn get_m3u8(&self) -> Vec<&M3u8> {
self.m3u8_items.iter().collect()
}
}
impl GetPlayPath for OnlineParser {
fn get_path_to_play<'a>(&'a self, link: Rc<String>) -> Result<Rc<String>, String> {
Ok(link.clone())
}
}

36
src/opt.rs Normal file
View File

@ -0,0 +1,36 @@
use std::str::FromStr;
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
#[structopt(name = "ilovetv")]
pub struct Opt {
#[structopt(short, long, default_value = "ask")]
/// Possible options: online, offline and ask
pub 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<Self, Self::Err> {
match s.to_lowercase().as_str() {
"online" => Ok(Self::Online),
"offline" => Ok(Self::Offline),
"ask" | "default" => Ok(Self::Ask),
_ => Err("No such enum"),
}
}
}

View File

@ -1,118 +1,32 @@
use std::fs;
use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
use crate::m3u8::M3u8; use crate::M3u8;
use crate::Configuration;
const MAX_TRIES: usize = 4; pub trait GetM3u8 {
fn get_m3u8(&self) -> Vec<&M3u8>;
pub struct Parser {
configuration: Rc<Configuration>,
m3u8_items: Vec<M3u8>,
} }
impl Parser { pub trait WatchedFind {
pub async fn new(configuration: Rc<Configuration>) -> Self { fn find(&self, name: &str) -> Vec<&M3u8>;
let m3u8_items = Self::get_parsed_m3u8(&configuration).await.unwrap(); fn get_watched(&self) -> Vec<&M3u8>;
}
Self { impl<T: ?Sized + GetM3u8> WatchedFind for T {
configuration, fn find(&self, name: &str) -> Vec<&M3u8> {
m3u8_items,
}
}
pub fn find(&self, name: &str) -> Vec<&M3u8> {
let name = name.to_lowercase(); let name = name.to_lowercase();
self.m3u8_items self.get_m3u8()
.iter() .into_iter()
.filter(|item| item.name.to_lowercase().contains(&name) || item.tvg_id.contains(&name)) .filter(|item| item.name.to_lowercase().contains(&name) || item.tvg_id.contains(&name))
.collect() .collect()
} }
pub async fn forcefully_update(&mut self) { fn get_watched(&self) -> Vec<&M3u8> {
let mut counter = 0; self.get_m3u8().into_iter().filter(|x| x.watched).collect()
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);
};
self.m3u8_items = Self::parse_m3u8(content, &self.seen_links);
}
pub fn save_watched(&self) {
let watched_items = self
.m3u8_items
.iter()
.filter(|item| item.watched)
.map(|item| item.link.clone())
.collect::<Vec<String>>();
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);
}
}
fn parse_m3u8(content: String, watched_links: &Vec<String>) -> Vec<M3u8> {
let mut m3u8_items: Vec<M3u8> = Vec::new();
let interesting_lines: Vec<String> = content
.replacen("#EXTM3U\n", "", 1)
.lines()
.map(str::trim)
.map(String::from)
.collect();
for i in (0..interesting_lines.len()).step_by(2) {
let mut items = Vec::new();
for to_find in ["tvg-id", "tvg-name", "tvg-logo", "group-title"] {
let offset: usize = format!("{}=", to_find).bytes().len();
let start: usize =
interesting_lines[i].find(&format!("{}=", to_find)).unwrap() as usize + offset;
let end: usize = interesting_lines[i].rfind("=").unwrap();
items.push(&interesting_lines[i][start..=end])
}
let name_start = interesting_lines[i].rfind(",").unwrap() + 1;
let name = &interesting_lines[i][name_start..];
let link = &interesting_lines[i + 1];
let is_watched = watched_links.contains(link);
let m3u8_item = M3u8 {
tvg_id: items[0].to_owned(),
tvg_name: items[1].to_owned(),
tvg_logo: items[2].to_owned(),
group_title: items[3].to_owned(),
name: name.to_owned(),
link: link.to_string(),
watched: is_watched,
};
m3u8_items.push(m3u8_item);
}
m3u8_items
}
async fn get_parsed_m3u8(config: &Configuration) -> Result<Vec<M3u8>, String> {
Ok(Self::parse_m3u8(
config.get_playlist().await?,
&config.seen_links,
))
} }
} }
pub trait GetPlayPath {
impl Deref for Parser { fn get_path_to_play(&self, link: Rc<String>) -> Result<Rc<String>, String>;
type Target = Configuration;
fn deref(&self) -> &Self::Target {
&self.configuration
}
} }
pub trait Parser: GetM3u8 + GetPlayPath {}
impl<T: GetM3u8 + GetPlayPath> Parser for T {}

99
src/playlist.rs Normal file
View File

@ -0,0 +1,99 @@
use std::{fs, ops::Deref, path::PathBuf, rc::Rc};
use crate::{download_with_progress, downloader::DualWriter, MAX_TRIES};
pub struct Playlist {
pub content: String,
path_to_playlist: Rc<PathBuf>,
url: Option<Rc<String>>,
}
impl Playlist {
pub async fn new(path_to_playlist: Rc<PathBuf>, url: Rc<String>) -> Result<Self, String> {
let mut me = Self {
content: String::new(),
path_to_playlist,
url: Some(url),
};
me.content = me.get_saved_or_download().await?;
Ok(me)
}
fn get_saved(&self) -> Option<String> {
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<String, String> {
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<String, String> {
let mut counter: u8 = 0;
loop {
counter += 1;
let url = self
.url
.as_ref()
.ok_or_else(|| String::from("In offline mode"))?
.clone();
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 = String;
fn deref(&self) -> &Self::Target {
&self.content
}
}