Compare commits

...

49 Commits

Author SHA1 Message Date
loveb
098dc83335
Update README.md 2023-05-15 16:30:48 +02:00
e73f186b0f toggle offline/online mode 2023-03-08 20:52:39 +01:00
f03b676313 error 2023-03-08 20:32:57 +01:00
1d0e709784 sort by links when in offline mode 2023-03-08 19:12:26 +01:00
692d6d08c7 Fix error where file ending wasn't included 2023-03-08 18:35:53 +01:00
2dc4c15aa2 resume download + fix bug where watched entries were not remembered 2023-03-08 18:21:21 +01:00
d1bfb69c0f better . 2023-03-06 13:57:49 +01:00
6642978a0f type Error 2023-03-06 07:52:40 +01:00
71ec8d5aad Use traits from the standard library instead of own methods 2023-03-06 07:47:52 +01:00
09d0938476 Change version and add author 2023-03-06 07:16:32 +01:00
224970bf6c Merge branch 'offline_mode' 2023-03-06 07:11:44 +01:00
8a1ad6c987 use get_ref_mut 2023-03-06 07:04:50 +01:00
2d5f795bfb Fix parsing error when leaving download/offline-mode 2023-03-06 07:03:26 +01:00
bf751c1cee Functionality for switching to online from offline 2023-03-05 20:26:52 +01:00
618e8f97ce consider the traits parser instead 2023-03-05 20:04:37 +01:00
fa12365719 remove unneeded imports 2023-03-05 19:55:46 +01:00
624c70e2ec More refactors 2023-03-05 19:51:21 +01:00
8290b67f25 Early working version 2023-03-05 19:14:26 +01:00
c97761bc00 start with offlineparser and grandmother 2023-03-05 16:35:01 +01:00
fd41100b95 Remove unneeded imports 2023-03-05 12:14:34 +01:00
be3839ae67 grandmother new file 2023-03-05 12:13:42 +01:00
c18a991fef Create new struct GrandMother and refactor to her 2023-03-05 12:10:49 +01:00
2f95755adc start 2023-03-03 13:13:39 +01:00
b9007713f5 saves to downloadmode correctly 2023-03-03 00:36:06 +01:00
3c3acfe0ef Created basic structure 2023-03-02 23:51:37 +01:00
d0d9f34f9e sections 2023-03-02 22:52:35 +01:00
loveb
0c96f4f9a9 ilovetv 2023-03-02 20:06:56 +01:00
d54a367fa0 Switch the name to ilovetv 2023-03-02 20:05:31 +01:00
63c3975f53 Easier to read the source code of the greeting 2023-03-02 18:15:56 +01:00
ad584c9b73 clear in both selections 2023-03-01 22:56:02 +01:00
ec2fba9ae2 Add support for remembering the latest search across sessions 2023-03-01 22:53:09 +01:00
36b79fd01f A lot of refactoring
Made the program a lot more configurable and also cleaned up the parser
struct
2023-03-01 21:59:52 +01:00
86f85011ec remove non-needed dependency 2023-02-28 16:29:28 +01:00
19031035a1 tidying up 2023-02-28 16:22:53 +01:00
2419592f22 breakout downloadmode 2023-02-28 15:37:33 +01:00
35bb1edfc3 Save watched right after something is watched, and use match 2023-02-27 16:24:16 +01:00
d580ca8956 lock 2023-02-27 13:37:31 +01:00
9eb3695b38 toggle mpv fullscreen 2023-02-02 16:41:36 +01:00
60fc85772a print error and also better welcome message 2023-01-19 18:13:38 +01:00
984c426df2 Merge branch 'main' of billenius.com:love/iptvnator 2023-01-19 17:58:44 +01:00
2fe24e04bd if failed to retrive content length, then print error 2023-01-19 17:57:41 +01:00
ecf3df8c15 lowercase 2023-01-19 14:14:35 +01:00
44b406c4b1 1,2,3,4 2023-01-19 12:55:38 +01:00
6565984972 a 2023-01-19 12:55:00 +01:00
38f9ef75b9 name 2023-01-19 12:47:17 +01:00
efdc27e8a9 comment 2023-01-19 12:40:54 +01:00
dfdf3faf25 more options 2023-01-19 12:37:24 +01:00
44d47d3f8d Merge branch 'main' of billenius.com:love/iptvnator_rs 2023-01-19 12:22:35 +01:00
e0ee71dc88 bug 2023-01-19 12:16:50 +01:00
15 changed files with 2527 additions and 350 deletions

7
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [],
}

1523
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,18 @@
[package] [package]
name = "iptvnator_rs" name = "ilovetv"
version = "0.1.0" authors = ["Love Billenius <lovebillenius@disroot.org>"]
version = "1.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
async-recursion = "1.0.2"
bytes = "1.3.0" bytes = "1.3.0"
colored = "2.0.0" colored = "2.0.0"
ctrlc = "3.2.4"
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","rc"] }
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"] }
tokio-util = "0.7.4"

View File

@ -1,14 +1,13 @@
# Iptvnator_rs # I Love TV
An iptvclient that is capable of parsing and reading m3uplaylists of essentially arbitrary sizes _blazingly fast_ An iptvclient that is capable of parsing and reading m3uplaylists of essentially arbitrary sizes _blazingly fast_
## Install ## Install
Just clone the repo and run `cargo build --release` to compile the project. Then put it in your `$PATH` or make a shortcut to the binary (target/release/iptvnator_rs) Just clone the repo and run `cargo build --release` to compile the project. Then put it in your `$PATH` or make a shortcut to the binary (target/release/ilovetv)
You will need to install mpv, and have it in your path, otherwise it wont work You will need to install mpv, and have it in your path, otherwise it wont work
## Left to do ## Left to do
- Implement the ctrlc handler so that the program saves watched links before exiting.
- Create a GUI - Create a GUI
- Would be nice to bundle mpv in some form - Would be nice to bundle mpv in some form

View File

@ -0,0 +1,167 @@
use std::{
fs::{self, File},
io::{self, BufReader},
ops::Deref,
path::{Path, PathBuf},
rc::Rc,
};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use serde_json;
use crate::{
get_mut_ref, m3u8::OfflineEntry, Readline, APP_IDENTIFIER, JSON_CONFIG_FILENAME,
STANDARD_OFFLINE_FILENAME, STANDARD_PLAYLIST_FILENAME, STANDARD_SEEN_LINKS_FILENAME,
};
#[derive(Serialize, Deserialize, Debug)]
pub struct Conf {
pub playlist_filename: String,
pub playlist_url: Rc<String>,
pub last_search: Option<String>,
pub seen_links_filename: String,
}
impl Conf {
/**
* Read configurationfile or ask user for link input if it isn't created.
* Will error if it fails to write config file
*/
pub fn new(ilovetv_config_file: &Path) -> Result<Conf, io::Error> {
// Read the configuraionfile if it exists
if ilovetv_config_file.exists() {
let config_file = Self::read_configfile(&ilovetv_config_file);
if let Ok(cfg) = config_file {
return Ok(cfg);
} else {
println!("There are some problem with the configurationfile");
}
}
// Get fresh config with url from user
let playlist_url = Self::user_setup().into();
Ok(Self {
playlist_filename: STANDARD_PLAYLIST_FILENAME.to_owned(),
playlist_url,
last_search: None,
seen_links_filename: STANDARD_SEEN_LINKS_FILENAME.to_owned(),
})
}
fn read_configfile(config_file: &Path) -> Result<Conf, io::Error> {
let reader = BufReader::new(File::open(config_file)?);
let conf: Conf = serde_json::from_reader(reader)?;
Ok(conf)
}
fn write_configfile(&self, path: &Path) -> Result<(), io::Error> {
fs::write(path, serde_json::to_string(&self)?)?;
Ok(())
}
fn user_setup() -> String {
let mut readline = Readline::new();
println!("Hello, I would need an url to your iptv/m3u/m3u8 stream");
loop {
let url = readline.input("enter url: ");
let yn = readline.input("Are you sure? (Y/n) ");
if yn.trim().to_lowercase() != "n" {
break url.trim().to_owned();
}
}
}
}
pub struct Configuration {
pub conf: Conf,
pub playlist_path: Rc<PathBuf>,
pub seen_links_path: PathBuf,
pub seen_links: Vec<String>,
config_file_path: PathBuf,
pub data_dir: PathBuf,
pub offlinefile_content: Rc<Vec<OfflineEntry>>,
}
impl Configuration {
pub fn new() -> Result<Self, io::Error> {
let project_dirs =
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 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 configuration = Conf::new(&config_file_path)?;
fs::write(
&config_file_path,
serde_json::to_string(&configuration).unwrap(),
)?;
// Playlist
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();
// Datadir
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,
playlist_path,
seen_links,
seen_links_path,
config_file_path,
data_dir: offline_dir,
offlinefile_content,
})
}
pub fn update_last_search_ugly(&self, last_search: Option<String>) {
unsafe { get_mut_ref(&self.conf).last_search = last_search }
if let Err(e) = self.write_configfile(&self.config_file_path) {
println!("Failed to write to configfile, {:?}", e);
}
}
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>> {
let reader = BufReader::new(File::open(&path).ok()?);
serde_json::from_reader(reader).ok()
}
fn get_offline_content(datafile: &PathBuf) -> Option<Vec<OfflineEntry>> {
let reader = BufReader::new(File::open(datafile).ok()?);
serde_json::from_reader(reader).ok()
}
}
impl Deref for Configuration {
type Target = Conf;
fn deref(&self) -> &Self::Target {
&self.conf
}
}

View File

@ -1,6 +1,6 @@
use std::{ use std::{
fmt, error, fmt,
fs::File, fs::{File, OpenOptions},
io::{self, Read, Write}, io::{self, Read, Write},
}; };
@ -10,6 +10,8 @@ use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use reqwest::{self, Client}; use reqwest::{self, Client};
use std::cmp; use std::cmp;
use crate::get_mut_ref;
pub enum DualWriter { pub enum DualWriter {
File(File), File(File),
Buffer(Vec<u8>), Buffer(Vec<u8>),
@ -29,7 +31,44 @@ impl DualWriter {
Ok(()) Ok(())
} }
pub fn get_string(self) -> Result<String, String> { pub fn len(&self) -> u64 {
match self {
DualWriter::File(f) => f.metadata().and_then(|x| Ok(x.len())).unwrap_or_else(|e| {
println!("Could not get metadata from file {:?}", e);
0
}),
DualWriter::Buffer(buf) => buf.len() as u64,
}
}
}
impl TryFrom<Option<&str>> for DualWriter {
type Error = io::Error;
fn try_from(file_name: Option<&str>) -> Result<Self, Self::Error> {
Ok(if let Some(file_name) = file_name {
let file = match OpenOptions::new().append(true).open(&file_name) {
Ok(f) => f,
Err(e) => {
let e = e as io::Error;
if e.kind() == io::ErrorKind::NotFound {
File::create(&file_name)?
} else {
return Err(e);
}
}
};
Self::File(file)
} else {
Self::Buffer(Vec::<u8>::new())
})
}
}
impl TryInto<String> for DualWriter {
type Error = String;
fn try_into(self) -> Result<String, Self::Error> {
Ok(match self { Ok(match self {
Self::Buffer(buffer) => { Self::Buffer(buffer) => {
String::from_utf8(buffer).or(Err("Failed to decode buffer".to_owned()))? String::from_utf8(buffer).or(Err("Failed to decode buffer".to_owned()))?
@ -38,44 +77,37 @@ impl DualWriter {
let mut buf = String::new(); let mut buf = String::new();
// Well this is safe since I consume the file anyways // Well this is safe since I consume the file anyways
let ptr = &file as *const File as *mut File; let file = unsafe { get_mut_ref(&file) };
let file = unsafe { &mut *ptr };
file.read_to_string(&mut buf) file.read_to_string(&mut buf)
.or(Err("Failed to read file".to_owned()))?; .or(Err("Failed to read file".to_owned()))?;
buf buf
} }
}) })
} }
pub fn new(file_name: Option<&str>) -> Result<Self, io::Error> {
Ok(if let Some(file_name) = file_name {
Self::File(File::create(&file_name)?)
} else {
Self::Buffer(Vec::<u8>::new())
})
}
} }
pub async fn download_with_progress( pub async fn download_with_progress(
link: &str, link: &str,
file_name: Option<&str>, file_name: Option<&str>,
) -> Result<DualWriter, String> { ) -> Result<DualWriter, Box<dyn error::Error>> {
let mut dw = DualWriter::new(file_name).or(Err("Failed to create file".to_owned()))?; let mut dual_writer: DualWriter = file_name.try_into()?;
let client = Client::builder() let client = Client::builder().gzip(true).deflate(true).build()?;
.gzip(true) let builder = client
.deflate(true)
.build()
.or(Err("Failed to create client".to_owned()))?;
let resp = client
.get(link) .get(link)
.send() .header("Range", format!("bytes={}-", dual_writer.len()));
.await
.or(Err("Failed to connect server".to_owned()))?;
let content_length = resp.content_length().unwrap();
let pb = ProgressBar::new(content_length); let resp = builder.send().await?;
pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})").unwrap()
.with_key("eta", |state: &ProgressState, w: &mut dyn fmt::Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()) let content_length = resp.content_length().unwrap_or_default();
if content_length == 0 {
println!("File was already downloaded");
return Ok(dual_writer);
}
let progress_bar = ProgressBar::new(content_length);
progress_bar.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})").unwrap()
.with_key("eta", |state: &ProgressState, w: &mut dyn fmt::Write| write!(w, "{:.1}m", state.eta().as_secs_f64() / 60.0 ).unwrap())
.progress_chars("#>-")); .progress_chars("#>-"));
let mut downloaded: u64 = 0; let mut downloaded: u64 = 0;
@ -84,11 +116,10 @@ pub async fn download_with_progress(
while let Some(item) = stream.next().await { while let Some(item) = stream.next().await {
let bytes = item.unwrap(); let bytes = item.unwrap();
downloaded = cmp::min(downloaded + (bytes.len() as u64), content_length); downloaded = cmp::min(downloaded + (bytes.len() as u64), content_length);
dw.write(bytes) dual_writer.write(bytes)?;
.or(Err("Failed to write to file".to_owned()))?;
pb.set_position(downloaded); progress_bar.set_position(downloaded);
} }
Ok(dw) Ok(dual_writer)
} }

97
src/grandmother.rs Normal file
View File

@ -0,0 +1,97 @@
#[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 fn demote_to_offline(&mut self) {
let offline_mother = GrandMother::new_offline(self.config.clone());
(self.parser, self.playlist) = (offline_mother.parser, offline_mother.playlist);
}
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_links();
let watched_links = watched_links.iter().map(|x| x.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_links();
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,46 +1,35 @@
mod m3u8;
mod parser;
use std::{
fs,
io::{stdin, stdout, Stdin, StdoutLock, Write},
process,
};
pub use m3u8::M3u8;
pub use parser::Parser;
mod config; mod config;
mod downloader; mod downloader;
use directories::ProjectDirs; 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 fn setup() -> String { pub const JSON_CONFIG_FILENAME: &'static str = "config.json";
let project_dirs = ProjectDirs::from("com", "billenius", "iptvnator_rs").unwrap(); pub const APP_IDENTIFIER: [&'static str; 3] = ["com", "billenius", "ilovetv"];
let config_dir = project_dirs.config_dir(); pub const STANDARD_PLAYLIST_FILENAME: &'static str = "playlist.m3u8";
let ilovetv_config_file = config_dir.join("iptv_url.txt"); pub const STANDARD_SEEN_LINKS_FILENAME: &'static str = "watched_links.json";
if ilovetv_config_file.exists() { pub const STANDARD_OFFLINE_FILENAME: &'static str = "ilovetv_offline.json";
return fs::read_to_string(&ilovetv_config_file).expect("Failed to read iptv_url"); pub const MAX_TRIES: u8 = 4;
}
let mut readline = Readline::new();
println!("Hello, I would need an url to your iptv/m3u/m3u8 stream");
let url = loop {
let url = readline.input("enter url: ");
let yn = readline.input("Are you sure? (Y/n) ");
if yn.trim().to_lowercase() != "n" {
break url.trim().to_string();
}
};
let _ = fs::create_dir_all(config_dir);
if let Err(e) = fs::write(ilovetv_config_file, &url) {
eprintln!("{:?}", e);
process::exit(-1);
}
url
}
pub struct Readline<'a> { pub struct Readline<'a> {
stdout: StdoutLock<'a>, stdout: StdoutLock<'a>,
@ -63,3 +52,41 @@ impl<'a> Readline<'a> {
buffer buffer
} }
} }
/**
* I know that this isn't considered true rusty code, but the places it's used in is
* safe. For this I'll leave the funciton as unsafe, so to better see it's uses.
* This solution makes the uses BLAZINGLY FAST which moreover is the most rusty you can get.
*/
pub unsafe fn get_mut_ref<T>(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: Rc<Configuration>,
) -> Result<(GrandMother, bool), String> {
match mode {
Mode::Online => Ok((GrandMother::new_online(config).await?, true)),
Mode::Offline => Ok((GrandMother::new_offline(config), false)),
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

@ -2,32 +2,129 @@ use std::num::ParseIntError;
use std::process::Command; use std::process::Command;
use std::rc::Rc; use std::rc::Rc;
use iptvnator_rs::{download_with_progress, setup, M3u8, Parser, Readline}; use colored::Colorize;
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() {
println!("Welcome to iptvnator_rs, the port of my iptvprogram written in python, now in rust BLAZINGLY FAST\n"); let opt = Opt::from_args();
let parser = Parser::new("iptv.m3u8".to_owned(), setup(), "watched.txt".to_owned()).await;
// Greet the user
[
format!(
"Welcome to {}, a {} iptv client written in rust\n",
"ilovetv".bold(),
"BLAZINGLY FAST".italic()
),
"There will be some options along the way".to_owned(),
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 between modes (toggle)", "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()),
format!(
" {} is to redo the last search (mainly for use in the last session)",
"l".bold()
),
format!(" {} is to clean the latest search", "c".bold()),
]
.iter()
.for_each(|s| println!("{}", &s));
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, mut in_online) = 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.input("Search by name: "); let search = readline
.input("Search by name [ r/q/f/l/m ]: ")
.to_lowercase();
let mut search = search.trim();
// If they want to quit, let them- // Special commands
if search.trim() == "q" { match search {
break; // Quit
} "q" => break,
// Refresh playlist
search_result = Some(Rc::new(parser.find(&search))); "r" => {
search_result = None;
if search_result.as_ref().unwrap().len() == 0 { if let Err(e) = gm.refresh_dirty().await {
println!("Nothing found"); println!(
"Cannot refresh. This is probably due to offlinemode {:?}",
e
);
};
continue; continue;
} }
// Toggle fullscreen for mpv
"f" => {
mpv_fs = !mpv_fs;
println!(
"Toggled mpv to {}launch in fullscreen",
if mpv_fs { "" } else { "not " }
);
continue;
}
"l" => {
search = if let Some(s) = gm.config.last_search.as_ref() {
s
} else {
println!("There is no search saved from earlier");
continue;
};
}
"c" => {
gm.config.update_last_search_ugly(None);
continue;
}
"m" => {
if in_online {
unsafe { get_mut_ref(&gm) }.demote_to_offline();
println!("Switched to offline mode");
} else {
let result = unsafe { get_mut_ref(&gm) }.promote_to_online().await;
if let Err(e) = result {
println!("Failed to switch to onlinemode {:?}", e);
} else {
println!("Switched to online mode");
continue;
}
}
in_online = !in_online;
continue;
}
_ => {}
}
search_result = Some(Rc::new(gm.parser.find(search)));
if search_result.as_ref().unwrap().is_empty() {
println!("Nothing found");
search_result = None;
continue;
}
gm.config.update_last_search_ugly(Some(search.to_owned()));
} }
// Let them choose which one to stream // Let them choose which one to stream
@ -35,98 +132,193 @@ async fn main() {
println!(" {}: {}", idx + 1, m3u8_item); println!(" {}: {}", idx + 1, m3u8_item);
} }
let user_wish = readline.input("Which one do you wish to stream? [ q/s/r/d ]: "); let user_wish = readline
.input("Which one do you wish to stream? [ q/f/s/r/d/o/m ]: ")
.to_lowercase();
let user_wish = user_wish.trim(); let user_wish = user_wish.trim();
// If they want to quit, let them- // If they want to quit, let them-
if user_wish == "q" { match user_wish {
break; // Quit
} else if user_wish == "s" { "q" => break,
// Go inte search-mode
"s" => {
search_result = None; search_result = None;
continue; continue;
} else if user_wish == "r" { }
// Refresh playlist
"r" => {
println!("Refreshing local m3u8-file"); println!("Refreshing local m3u8-file");
search_result = None; search_result = None;
if let Err(e) = gm.refresh_dirty().await {
println!(
"Cannot refresh. This is probably due to offlinemode {:?}",
e
);
};
continue;
}
"f" => {
mpv_fs = !mpv_fs;
println!(
"Toggled mpv to {}launch in fullscreen",
if mpv_fs { "" } else { "not " }
);
continue;
}
// Downloadmode
"d" => {
let download_selections =
ask_which_to_download(&mut readline, &search_result.as_ref().unwrap());
// I know that this is also frowned upon, but it is perfectly safe right here, for to_download in download_selections.iter() {
// even though the borrowchecker complains download_m3u8(to_download, None).await;
{
let ptr = &parser as *const Parser as *mut Parser;
let p = unsafe { &mut *ptr };
p.forcefully_update().await;
} }
continue; continue;
} else if user_wish == "d" { }
let selection = readline.input("Download all or select in comma separated [A]: "); // Save to offlinemode
let selection = selection.trim(); "o" => {
let download_selections =
ask_which_to_download(&mut readline, &search_result.as_ref().unwrap());
let to_download = loop { for to_download in download_selections.iter() {
break if selection.to_lowercase() == "a" { let file_ending = to_download
println!("Downloading all"); .name
search_result.as_ref().unwrap().clone() .rfind(".")
.map(|dot_idx| {
if dot_idx < 6 {
&to_download.name[dot_idx..]
} else { } else {
let selections = selection ".mkv"
.split(",") }
.map(|x| x.trim().parse::<usize>()) })
.collect::<Vec<Result<usize, ParseIntError>>>(); .unwrap_or_else(|| ".mkv");
for selection in selections.iter() { let path = gm
if selection.is_err() { .config
println!("Not a valid number"); .data_dir
.join(format!("{}{}", &to_download.name, file_ending));
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; 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);
} }
let selections = selections.into_iter().map(|x| x.unwrap() - 1); continue;
let mut final_selections = Vec::new();
for selection in selections {
final_selections.push((search_result.as_ref().unwrap())[selection]);
} }
_ => {}
Rc::new(final_selections)
};
};
download_m3u8(to_download).await;
} }
let choosen = user_wish.parse::<usize>(); let choosen = user_wish.parse::<usize>();
match choosen { match choosen {
Ok(k) => { Ok(k) => {
let search_result = search_result.as_ref().unwrap().clone(); let search_result = search_result.as_ref().unwrap();
stream(&(search_result[k - 1])) 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), Err(e) => println!("Have to be a valid number! {:?}", e),
} }
} }
parser.save_watched();
} }
async fn download_m3u8(files_to_download: Rc<Vec<&M3u8>>) { fn ask_which_to_download<'a>(
for m3u8 in files_to_download.iter() { readline: &mut Readline,
let file_ending_place = m3u8.link.rfind(".").unwrap(); search_result: &Rc<Vec<&'a M3u8>>,
let potential_file_ending = &m3u8.link[file_ending_place..]; ) -> Rc<Vec<&'a M3u8>> {
let selections = loop {
// Ask for userinput
let selection = readline
.input("Download all or select in comma separated [a | 1,2,3,4]: ")
.to_lowercase();
let selection = selection.trim();
// Download all
if selection == "a" {
println!("Downloading all");
return search_result.clone();
}
// Convert to numbers
let selections = selection
.split(",")
.map(|x| x.trim().parse::<usize>())
.collect::<Vec<Result<usize, ParseIntError>>>();
// Ask again if any number wasn't a valid number
let wrong_input = selections.iter().any(|x| x.is_err());
if wrong_input {
println!("Invalid input. Has to be either {}, a number or a sequence of numbers separated by commas","a".bold());
continue;
}
break selections;
};
Rc::new(
selections
.into_iter()
.map(|x| x.unwrap() - 1) // Since all numbers are valid, remap them
.map(|x| search_result[x]) // We don't want the numbers, but the &M3u8 in those positions
.collect(),
)
}
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 { let file_ending = if potential_file_ending.len() > 6 {
".mkv" ".mkv"
} else { } else {
potential_file_ending potential_file_ending
}; };
let file_name = format!("{}{}", m3u8.name, file_ending); let file_name = format!("{}{}", file_to_download.name, file_ending);
println!("Downloading {}", &file_name); println!("Downloading {}", &file_name);
if let Err(e) = download_with_progress(&m3u8.link, Some(&file_name)).await { let path = if let Some(path) = path {
format!("{}{}", path, file_ending)
} 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); eprintln!("Failed to download {}, {:?}", &file_name, e);
} }
}
} }
fn stream(m3u8item: &M3u8) { /**
// Well I know that this is frowned upon, but it's honestly the most efficient way of doing this * I know that this is not how youre supposed to do things, but it's perfectly safe
let ptr = m3u8item as *const M3u8; * in this context and also the most efficient way.
let ptr = ptr as *mut M3u8; * With other words, it's BLAZINGLY FAST
let mut item = unsafe { &mut *ptr }; */
item.watched = true; 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![link];
if launch_in_fullscreen {
args.push("--fs");
}
Command::new("mpv") Command::new("mpv")
.arg(&m3u8item.link) .args(args)
.output() .output()
.expect("Could not listen for output"); .expect("Could not listen for output");
} }

44
src/offlineparser.rs Normal file
View File

@ -0,0 +1,44 @@
use std::{ops::Deref, rc::Rc};
use serde::Serialize;
use crate::{m3u8::M3u8, Configuration, GetM3u8, GetPlayPath, OfflineEntry};
#[derive(Serialize)]
pub struct OfflineParser {
offline_entries: Rc<Vec<OfflineEntry>>,
}
impl OfflineParser {
pub fn new(config: &Configuration) -> Self {
Self {
offline_entries: config.offlinefile_content.clone(),
}
}
}
impl Deref for OfflineParser {
type Target = Vec<OfflineEntry>;
fn deref(&self) -> &Self::Target {
&*self.offline_entries
}
}
impl GetPlayPath for OfflineParser {
fn get_path_to_play(&self, link: Rc<String>) -> Result<Rc<String>, String> {
for offline_entry in &*self.offline_entries {
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> {
let mut items: Vec<&M3u8> = self.offline_entries.iter().map(|x| &**x).collect();
items.sort_by_key(|x| &x.link);
items
}
}

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,200 +1,36 @@
use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use std::{fs, process};
use directories::ProjectDirs; use crate::M3u8;
use crate::downloader::download_with_progress; pub trait GetM3u8 {
use crate::m3u8::M3u8; fn get_m3u8(&self) -> Vec<&M3u8>;
const MAX_TRIES: usize = 4;
pub struct Parser {
watched_name: Rc<PathBuf>,
m3u8_items: Vec<M3u8>,
ilovetv_url: Rc<String>,
file_name: Rc<PathBuf>,
} }
impl Parser { pub trait WatchedFind {
pub async fn new(file_name: String, iptv_url: String, watched_name: String) -> Self { fn find(&self, name: &str) -> Vec<&M3u8>;
let project_dirs = ProjectDirs::from("com", "billenius", "iptvnator_rs").unwrap(); fn get_watched_links(&self) -> Vec<Rc<String>>;
let cache = project_dirs.cache_dir(); }
let _ = fs::create_dir_all(&cache);
let file_name = Rc::new(cache.join(file_name)); impl<T: ?Sized + GetM3u8> WatchedFind for T {
let ilovetv_url = Rc::new(iptv_url); fn find(&self, name: &str) -> Vec<&M3u8> {
let watched_name = Rc::new(cache.join(watched_name)); let name = name.to_lowercase();
self.get_m3u8()
Self { .into_iter()
watched_name: watched_name.clone(), .filter(|item| item.name.to_lowercase().contains(&name) || item.tvg_id.contains(&name))
m3u8_items: Self::get_parsed_content(&ilovetv_url, &file_name, &watched_name).await,
ilovetv_url,
file_name,
}
}
pub fn find(&self, name: &str) -> Vec<&M3u8> {
let name = name.to_uppercase();
self.m3u8_items
.iter()
.filter(|item| item.name.to_uppercase().contains(&name) || item.tvg_id.contains(&name))
.collect() .collect()
} }
fn should_update(file_name: &PathBuf) -> bool { fn get_watched_links(&self) -> Vec<Rc<String>> {
fs::metadata(&file_name) self.get_m3u8()
.and_then(|metadata| { .into_iter()
Ok({ .filter(|x| x.watched)
let seconds = metadata.modified()?; .map(|x| x.link.clone())
seconds .collect()
.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 forcefully_update(&mut self) {
let mut counter = 0;
let content = loop {
counter += 1;
let content = Self::download(&self.ilovetv_url).await.ok();
if counter > MAX_TRIES {
return;
} else if content.is_some() {
break content.unwrap();
}
println!("Retrying {}/{}", counter, MAX_TRIES);
};
let _ = fs::write(&*self.file_name, &content);
self.m3u8_items = Self::parse_m3u8(content, &self.watched_name.clone());
}
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 _ = fs::create_dir_all(&*self.watched_name.parent().unwrap());
match fs::write(&*self.watched_name, watched_items.join("\n")) {
Ok(_) => {
println!("Saved watched")
}
Err(e) => {
eprintln!("Failed to write downloaded m3u8file {:?}", e);
}
}
}
async fn get_parsed_content(
link: &String,
file_name: &PathBuf,
watched_name: &PathBuf,
) -> Vec<M3u8> {
Self::parse_m3u8(
Self::get_stringcontent(link, file_name)
.await
.expect("Failed to retrieve playlist"),
watched_name,
)
}
fn parse_m3u8(content: String, watched_name: &PathBuf) -> Vec<M3u8> {
let saved_watches = fs::read_to_string(&watched_name);
let saved_watches = if saved_watches.is_ok() {
saved_watches.unwrap()
} else {
String::from("")
};
let watched: Vec<String> = saved_watches.lines().map(String::from).collect();
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.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_stringcontent(link: &String, file_name: &PathBuf) -> Result<String, String> {
if !Self::should_update(file_name) {
let content = fs::read_to_string(&file_name);
if content.is_ok() {
return Ok(content.unwrap());
}
}
let mut counter: usize = 0;
let content = loop {
counter += 1;
if let Ok(content) = Self::download(link).await {
break Ok(content);
} else if counter > MAX_TRIES {
break Err("".to_owned());
}
println!("Retrying {}/{}", counter + 1, MAX_TRIES);
};
match content {
Ok(s) => {
let _ = fs::write(&file_name, s.as_bytes());
Ok(s)
}
Err(_) => {
println!("Couldn't get m3u8 file!");
process::exit(-1);
}
}
}
async fn download(link: &String) -> Result<String, String> {
Ok(download_with_progress(link, None)
.await?
.get_string()
.unwrap())
} }
} }
pub trait GetPlayPath {
fn get_path_to_play(&self, link: Rc<String>) -> Result<Rc<String>, String>;
}
pub trait Parser: GetM3u8 + GetPlayPath {}
impl<T: GetM3u8 + GetPlayPath> Parser for T {}

101
src/playlist.rs Normal file
View File

@ -0,0 +1,101 @@
use std::{fs, ops::Deref, path::PathBuf, rc::Rc};
use crate::{download_with_progress, MAX_TRIES};
type Error = String;
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({
metadata
.modified()?
.elapsed()
.map(|x| x.as_secs() > 60 * 60 * 24 * 3)
.unwrap_or_else(|_| {
println!("Could not get systemtime, trying to download new file");
true
})
})
})
.unwrap_or_else(|_| {
println!("Could not find a saved playlist, Downloading a new one");
false
})
}
pub async fn get_saved_or_download(&self) -> Result<String, Error> {
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, Error> {
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
.map(TryInto::try_into);
if let Ok(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
}
}