Compare commits
49 Commits
offline_mo
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
098dc83335 | ||
e73f186b0f | |||
f03b676313 | |||
1d0e709784 | |||
692d6d08c7 | |||
2dc4c15aa2 | |||
d1bfb69c0f | |||
6642978a0f | |||
71ec8d5aad | |||
09d0938476 | |||
224970bf6c | |||
8a1ad6c987 | |||
2d5f795bfb | |||
bf751c1cee | |||
618e8f97ce | |||
fa12365719 | |||
624c70e2ec | |||
8290b67f25 | |||
c97761bc00 | |||
fd41100b95 | |||
be3839ae67 | |||
c18a991fef | |||
2f95755adc | |||
b9007713f5 | |||
3c3acfe0ef | |||
d0d9f34f9e | |||
|
0c96f4f9a9 | ||
d54a367fa0 | |||
63c3975f53 | |||
ad584c9b73 | |||
ec2fba9ae2 | |||
36b79fd01f | |||
86f85011ec | |||
19031035a1 | |||
2419592f22 | |||
35bb1edfc3 | |||
d580ca8956 | |||
9eb3695b38 | |||
60fc85772a | |||
984c426df2 | |||
2fe24e04bd | |||
ecf3df8c15 | |||
44b406c4b1 | |||
6565984972 | |||
38f9ef75b9 | |||
efdc27e8a9 | |||
dfdf3faf25 | |||
44d47d3f8d | |||
e0ee71dc88 |
7
.vscode/launch.json
vendored
Normal file
7
.vscode/launch.json
vendored
Normal 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
1523
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@ -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"
|
|
||||||
|
@ -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
|
||||||
|
167
src/config.rs
167
src/config.rs
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
97
src/grandmother.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
src/lib.rs
105
src/lib.rs
@ -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");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
29
src/m3u8.rs
29
src/m3u8.rs
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
352
src/main.rs
352
src/main.rs
@ -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
|
||||||
|
"r" => {
|
||||||
|
search_result = None;
|
||||||
|
if let Err(e) = gm.refresh_dirty().await {
|
||||||
|
println!(
|
||||||
|
"Cannot refresh. This is probably due to offlinemode {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
};
|
||||||
|
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)));
|
||||||
|
|
||||||
search_result = Some(Rc::new(parser.find(&search)));
|
if search_result.as_ref().unwrap().is_empty() {
|
||||||
|
|
||||||
if search_result.as_ref().unwrap().len() == 0 {
|
|
||||||
println!("Nothing found");
|
println!("Nothing found");
|
||||||
|
search_result = None;
|
||||||
continue;
|
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,
|
||||||
search_result = None;
|
// Go inte search-mode
|
||||||
continue;
|
"s" => {
|
||||||
} else if user_wish == "r" {
|
search_result = None;
|
||||||
println!("Refreshing local m3u8-file");
|
continue;
|
||||||
search_result = None;
|
|
||||||
|
|
||||||
// I know that this is also frowned upon, but it is perfectly safe right here,
|
|
||||||
// even though the borrowchecker complains
|
|
||||||
{
|
|
||||||
let ptr = &parser as *const Parser as *mut Parser;
|
|
||||||
let p = unsafe { &mut *ptr };
|
|
||||||
p.forcefully_update().await;
|
|
||||||
}
|
}
|
||||||
continue;
|
// Refresh playlist
|
||||||
} else if user_wish == "d" {
|
"r" => {
|
||||||
let selection = readline.input("Download all or select in comma separated [A]: ");
|
println!("Refreshing local m3u8-file");
|
||||||
let selection = selection.trim();
|
search_result = None;
|
||||||
|
if let Err(e) = gm.refresh_dirty().await {
|
||||||
let to_download = loop {
|
println!(
|
||||||
break if selection.to_lowercase() == "a" {
|
"Cannot refresh. This is probably due to offlinemode {:?}",
|
||||||
println!("Downloading all");
|
e
|
||||||
search_result.as_ref().unwrap().clone()
|
);
|
||||||
} else {
|
|
||||||
let selections = selection
|
|
||||||
.split(",")
|
|
||||||
.map(|x| x.trim().parse::<usize>())
|
|
||||||
.collect::<Vec<Result<usize, ParseIntError>>>();
|
|
||||||
|
|
||||||
for selection in selections.iter() {
|
|
||||||
if selection.is_err() {
|
|
||||||
println!("Not a valid number");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let selections = selections.into_iter().map(|x| x.unwrap() - 1);
|
|
||||||
let mut final_selections = Vec::new();
|
|
||||||
for selection in selections {
|
|
||||||
final_selections.push((search_result.as_ref().unwrap())[selection]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Rc::new(final_selections)
|
|
||||||
};
|
};
|
||||||
};
|
continue;
|
||||||
download_m3u8(to_download).await;
|
}
|
||||||
|
"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());
|
||||||
|
|
||||||
|
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 file_ending = to_download
|
||||||
|
.name
|
||||||
|
.rfind(".")
|
||||||
|
.map(|dot_idx| {
|
||||||
|
if dot_idx < 6 {
|
||||||
|
&to_download.name[dot_idx..]
|
||||||
|
} else {
|
||||||
|
".mkv"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| ".mkv");
|
||||||
|
|
||||||
|
let path = gm
|
||||||
|
.config
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 file_ending = if potential_file_ending.len() > 6 {
|
let selections = loop {
|
||||||
".mkv"
|
// Ask for userinput
|
||||||
} else {
|
let selection = readline
|
||||||
potential_file_ending
|
.input("Download all or select in comma separated [a | 1,2,3,4]: ")
|
||||||
};
|
.to_lowercase();
|
||||||
let file_name = format!("{}{}", m3u8.name, file_ending);
|
let selection = selection.trim();
|
||||||
println!("Downloading {}", &file_name);
|
|
||||||
if let Err(e) = download_with_progress(&m3u8.link, Some(&file_name)).await {
|
// Download all
|
||||||
eprintln!("Failed to download {}, {:?}", &file_name, e);
|
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 {
|
||||||
|
".mkv"
|
||||||
|
} else {
|
||||||
|
potential_file_ending
|
||||||
|
};
|
||||||
|
let file_name = format!("{}{}", file_to_download.name, file_ending);
|
||||||
|
println!("Downloading {}", &file_name);
|
||||||
|
let path = if let Some(path) = path {
|
||||||
|
format!("{}{}", path, file_ending)
|
||||||
|
} else {
|
||||||
|
file_name.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = download_with_progress(&file_to_download.link, Some(&path)).await {
|
||||||
|
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
44
src/offlineparser.rs
Normal 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
91
src/onlineparser.rs
Normal 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
36
src/opt.rs
Normal 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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
214
src/parser.rs
214
src/parser.rs
@ -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
101
src/playlist.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user