Create new struct GrandMother and refactor to her
This commit is contained in:
parent
af1d257c0a
commit
c162450bdd
@ -10,6 +10,6 @@ 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"
|
||||||
tokio = { version = "1.24.2", features = ["full"] }
|
tokio = { version = "1.24.2", features = ["full"] }
|
||||||
|
120
src/config.rs
120
src/config.rs
@ -1,8 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{self, BufReader},
|
io::{self, BufReader},
|
||||||
ops::{Deref, DerefMut},
|
ops::Deref,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
@ -10,20 +11,15 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use crate::{
|
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)]
|
#[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,
|
||||||
}
|
}
|
||||||
@ -45,7 +41,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(),
|
||||||
@ -83,7 +79,7 @@ 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,
|
||||||
@ -115,7 +111,7 @@ impl Configuration {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Playlist
|
// 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_path = cache_dir.join(&configuration.seen_links_filename);
|
||||||
let seen_links = Self::get_watched(&seen_links_path).unwrap_or_default();
|
let seen_links = Self::get_watched(&seen_links_path).unwrap_or_default();
|
||||||
|
|
||||||
@ -156,12 +152,6 @@ impl Configuration {
|
|||||||
serde_json::from_reader(reader).ok()
|
serde_json::from_reader(reader).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn just_download(&self) -> Result<String, String> {
|
|
||||||
download_with_progress(&self.playlist_url, None)
|
|
||||||
.await?
|
|
||||||
.get_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_datafile_content(datafile: &PathBuf) -> Option<Vec<DataEntry>> {
|
fn get_datafile_content(datafile: &PathBuf) -> Option<Vec<DataEntry>> {
|
||||||
let reader = BufReader::new(File::open(datafile).ok()?);
|
let reader = BufReader::new(File::open(datafile).ok()?);
|
||||||
serde_json::from_reader(reader).ok()
|
serde_json::from_reader(reader).ok()
|
||||||
@ -175,95 +165,3 @@ impl Deref for Configuration {
|
|||||||
&self.conf
|
&self.conf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Playlist {
|
|
||||||
pub entries: Vec<M3u8>,
|
|
||||||
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<String> {
|
|
||||||
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<String, String> {
|
|
||||||
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<String, String> {
|
|
||||||
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<M3u8>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.entries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for Playlist {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.entries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
17
src/lib.rs
17
src/lib.rs
@ -1,14 +1,23 @@
|
|||||||
|
mod config;
|
||||||
|
mod downloader;
|
||||||
mod m3u8;
|
mod m3u8;
|
||||||
mod parser;
|
mod parser;
|
||||||
|
mod playlist;
|
||||||
|
|
||||||
use std::io::{stdin, stdout, Stdin, StdoutLock, Write};
|
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 m3u8::{DataEntry, M3u8};
|
||||||
pub use parser::Parser;
|
pub use parser::Parser;
|
||||||
mod config;
|
pub use playlist::{GrandMother, Playlist};
|
||||||
pub use config::Configuration;
|
|
||||||
mod downloader;
|
|
||||||
pub use downloader::download_with_progress;
|
|
||||||
|
|
||||||
|
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>,
|
||||||
stdin: Stdin,
|
stdin: Stdin,
|
||||||
|
38
src/main.rs
38
src/main.rs
@ -4,7 +4,8 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use ilovetv::{
|
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]
|
#[tokio::main]
|
||||||
@ -32,9 +33,10 @@ 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;
|
||||||
@ -55,7 +57,7 @@ async fn main() {
|
|||||||
// Refresh playlist
|
// Refresh playlist
|
||||||
"r" => {
|
"r" => {
|
||||||
search_result = None;
|
search_result = None;
|
||||||
refresh(&parser).await;
|
gm.refresh_dirty().await;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Toggle fullscreen for mpv
|
// Toggle fullscreen for mpv
|
||||||
@ -68,7 +70,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");
|
||||||
@ -76,19 +78,19 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
"c" => {
|
"c" => {
|
||||||
config.update_last_search_ugly(None);
|
gm.config.update_last_search_ugly(None);
|
||||||
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
|
||||||
@ -114,7 +116,7 @@ async fn main() {
|
|||||||
"r" => {
|
"r" => {
|
||||||
println!("Refreshing local m3u8-file");
|
println!("Refreshing local m3u8-file");
|
||||||
search_result = None;
|
search_result = None;
|
||||||
refresh(&parser).await;
|
gm.refresh_dirty().await;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
"f" => {
|
"f" => {
|
||||||
@ -140,14 +142,14 @@ async fn main() {
|
|||||||
ask_which_to_download(&mut readline, &search_result.as_ref().unwrap());
|
ask_which_to_download(&mut readline, &search_result.as_ref().unwrap());
|
||||||
|
|
||||||
for to_download in download_selections.iter() {
|
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();
|
let path = path.to_string_lossy().to_string();
|
||||||
download_m3u8(to_download, Some(&path)).await;
|
download_m3u8(to_download, Some(&path)).await;
|
||||||
let data_entry = DataEntry::new((*to_download).clone(), path);
|
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!(
|
println!(
|
||||||
"Failed to information about downloaded entries for offline use {:?}",
|
"Failed to information about downloaded entries for offline use {:?}",
|
||||||
e
|
e
|
||||||
@ -162,7 +164,7 @@ async fn main() {
|
|||||||
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);
|
stream(search_result[k - 1], mpv_fs);
|
||||||
parser.save_watched();
|
gm.save_watched();
|
||||||
}
|
}
|
||||||
Err(e) => println!("Have to be a valid number! {:?}", e),
|
Err(e) => println!("Have to be a valid number! {:?}", e),
|
||||||
}
|
}
|
||||||
@ -251,11 +253,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;
|
|
||||||
}
|
|
||||||
|
@ -1,24 +1,15 @@
|
|||||||
use std::fs;
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::m3u8::M3u8;
|
use crate::m3u8::M3u8;
|
||||||
use crate::Configuration;
|
|
||||||
|
|
||||||
const MAX_TRIES: usize = 4;
|
|
||||||
|
|
||||||
pub struct Parser {
|
pub struct Parser {
|
||||||
configuration: Rc<Configuration>,
|
|
||||||
m3u8_items: Vec<M3u8>,
|
m3u8_items: Vec<M3u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parser {
|
impl Parser {
|
||||||
pub async fn new(configuration: Rc<Configuration>) -> Self {
|
pub async fn new(m3u_content: &str, watched_links: &Vec<&str>) -> Self {
|
||||||
let m3u8_items = Self::get_parsed_m3u8(&configuration).await.unwrap();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
configuration,
|
m3u8_items: Self::parse_m3u8(m3u_content, watched_links),
|
||||||
m3u8_items,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,41 +21,34 @@ impl Parser {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn forcefully_update(&mut self) {
|
/*
|
||||||
let mut counter = 0;
|
* I know that this is also frowned upon, but it is perfectly safe right here,
|
||||||
let content = loop {
|
* even though the borrowchecker complains
|
||||||
counter += 1;
|
*/
|
||||||
let content = self.download_playlist().await;
|
// async fn refresh(&self) {
|
||||||
if counter > MAX_TRIES {
|
// unsafe { get_mut_ref(&self.pla) }.forcefully_update().await;
|
||||||
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 async fn forcefully_update(&mut self, content: &str) {
|
||||||
}
|
let seen_links: &Vec<&str> = &self
|
||||||
|
|
||||||
pub fn save_watched(&self) {
|
|
||||||
let watched_items = self
|
|
||||||
.m3u8_items
|
.m3u8_items
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|item| item.watched)
|
.filter(|x| x.watched)
|
||||||
.map(|item| item.link.clone())
|
.map(|x| x.link.as_str())
|
||||||
.collect::<Vec<String>>();
|
.collect();
|
||||||
|
|
||||||
let resp = fs::write(
|
self.m3u8_items = Self::parse_m3u8(content, seen_links);
|
||||||
&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> {
|
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<M3u8> {
|
||||||
let mut m3u8_items: Vec<M3u8> = Vec::new();
|
let mut m3u8_items: Vec<M3u8> = Vec::new();
|
||||||
let interesting_lines: Vec<String> = content
|
let interesting_lines: Vec<String> = content
|
||||||
.replacen("#EXTM3U\n", "", 1)
|
.replacen("#EXTM3U\n", "", 1)
|
||||||
@ -85,8 +69,8 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
let name_start = interesting_lines[i].rfind(",").unwrap() + 1;
|
let name_start = interesting_lines[i].rfind(",").unwrap() + 1;
|
||||||
let name = &interesting_lines[i][name_start..];
|
let name = &interesting_lines[i][name_start..];
|
||||||
let link = &interesting_lines[i + 1];
|
let link = interesting_lines[i + 1].as_str();
|
||||||
let is_watched = watched_links.contains(link);
|
let is_watched = watched_links.contains(&link);
|
||||||
let m3u8_item = M3u8 {
|
let m3u8_item = M3u8 {
|
||||||
tvg_id: items[0].to_owned(),
|
tvg_id: items[0].to_owned(),
|
||||||
tvg_name: items[1].to_owned(),
|
tvg_name: items[1].to_owned(),
|
||||||
@ -100,19 +84,12 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
m3u8_items
|
m3u8_items
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_parsed_m3u8(config: &Configuration) -> Result<Vec<M3u8>, String> {
|
|
||||||
Ok(Self::parse_m3u8(
|
|
||||||
config.get_playlist().await?,
|
|
||||||
&config.seen_links,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for Parser {
|
impl Deref for Parser {
|
||||||
type Target = Configuration;
|
type Target = Vec<M3u8>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.configuration
|
&self.m3u8_items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
165
src/playlist.rs
Normal file
165
src/playlist.rs
Normal file
@ -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<PathBuf>,
|
||||||
|
url: 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,
|
||||||
|
};
|
||||||
|
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 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<Self, String> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user