Compare commits
36 Commits
main
...
offline_mo
Author | SHA1 | Date | |
---|---|---|---|
71ded77fbc | |||
37bc5b11c7 | |||
6da49ec983 | |||
0d903f80ff | |||
38b0e405d0 | |||
81006d4c5c | |||
2b30b5335f | |||
da1e754ec3 | |||
92f2031dad | |||
c162450bdd | |||
af1d257c0a | |||
8417969b9d | |||
bc6b5cba34 | |||
eb1e4e7ce1 | |||
0f611f9aa6 | |||
0999f24691 | |||
d07d4d70b4 | |||
3171e5568d | |||
0d02826b77 | |||
c27d0c4cb9 | |||
41a4af4667 | |||
6197c52bf8 | |||
7d6aa90dad | |||
f69a040287 | |||
304fa44629 | |||
39ced4e0d7 | |||
3a977a8358 | |||
5637f62a8b | |||
dccb4f1c40 | |||
c7fff46ff3 | |||
866e56c247 | |||
bef4208351 | |||
2dda01e651 | |||
f4c184b2c7 | |||
608176262d | |||
2f5fcdb9dd |
1523
Cargo.lock
generated
Normal file
1523
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@ -1,17 +1,17 @@
|
||||
[package]
|
||||
name = "iptvnator_rs"
|
||||
version = "0.1.0"
|
||||
name = "ilovetv"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-recursion = "1.0.2"
|
||||
bytes = "1.3.0"
|
||||
colored = "2.0.0"
|
||||
ctrlc = "3.2.4"
|
||||
directories = "4.0.1"
|
||||
futures-util = "0.3.25"
|
||||
indicatif = { version = "0.17.3", features = ["tokio"] }
|
||||
reqwest = { version = "0.11.13", features = ["blocking", "deflate", "gzip", "rustls", "rustls-tls", "stream"] }
|
||||
serde = { version = "1.0.152", features = ["serde_derive","rc"] }
|
||||
serde_json = "1.0.93"
|
||||
structopt = { version = "0.3.26", features = ["color", "suggestions"] }
|
||||
tokio = { version = "1.24.2", features = ["full"] }
|
||||
tokio-util = "0.7.4"
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Iptvnator_rs
|
||||
# Iptvnator
|
||||
|
||||
An iptvclient that is capable of parsing and reading m3uplaylists of essentially arbitrary sizes _blazingly fast_
|
||||
|
||||
@ -9,6 +9,5 @@ You will need to install mpv, and have it in your path, otherwise it wont work
|
||||
|
||||
## Left to do
|
||||
|
||||
- Implement the ctrlc handler so that the program saves watched links before exiting.
|
||||
- Create a GUI
|
||||
- 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
|
||||
}
|
||||
}
|
@ -10,6 +10,8 @@ use indicatif::{ProgressBar, ProgressState, ProgressStyle};
|
||||
use reqwest::{self, Client};
|
||||
use std::cmp;
|
||||
|
||||
use crate::get_mut_ref;
|
||||
|
||||
pub enum DualWriter {
|
||||
File(File),
|
||||
Buffer(Vec<u8>),
|
||||
@ -38,14 +40,14 @@ impl DualWriter {
|
||||
let mut buf = String::new();
|
||||
|
||||
// Well this is safe since I consume the file anyways
|
||||
let ptr = &file as *const File as *mut File;
|
||||
let file = unsafe { &mut *ptr };
|
||||
let file = unsafe { get_mut_ref(&file) };
|
||||
file.read_to_string(&mut buf)
|
||||
.or(Err("Failed to read file".to_owned()))?;
|
||||
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)?)
|
||||
@ -59,7 +61,7 @@ pub async fn download_with_progress(
|
||||
link: &str,
|
||||
file_name: Option<&str>,
|
||||
) -> Result<DualWriter, String> {
|
||||
let mut dw = DualWriter::new(file_name).or(Err("Failed to create file".to_owned()))?;
|
||||
let mut dual_writer = DualWriter::new(file_name).or(Err("Failed to create file".to_owned()))?;
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
@ -71,10 +73,14 @@ pub async fn download_with_progress(
|
||||
.send()
|
||||
.await
|
||||
.or(Err("Failed to connect server".to_owned()))?;
|
||||
let content_length = resp.content_length().unwrap();
|
||||
let content_length = if let Some(got_content_length) = resp.content_length() {
|
||||
got_content_length
|
||||
} else {
|
||||
panic!("Could not retrive content length from server. {:?}", &resp);
|
||||
};
|
||||
|
||||
let pb = ProgressBar::new(content_length);
|
||||
pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})").unwrap()
|
||||
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}s", state.eta().as_secs_f64()).unwrap())
|
||||
.progress_chars("#>-"));
|
||||
|
||||
@ -84,11 +90,12 @@ pub async fn download_with_progress(
|
||||
while let Some(item) = stream.next().await {
|
||||
let bytes = item.unwrap();
|
||||
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)
|
||||
}
|
||||
|
96
src/grandmother.rs
Normal file
96
src/grandmother.rs
Normal file
@ -0,0 +1,96 @@
|
||||
#[allow(unused_imports)]
|
||||
use crate::GetM3u8;
|
||||
use crate::{
|
||||
parser::{Parser, WatchedFind},
|
||||
Configuration, OfflineParser, OnlineParser, Playlist, MAX_TRIES,
|
||||
};
|
||||
use std::{fs, rc::Rc};
|
||||
|
||||
type Error = String;
|
||||
|
||||
pub struct GrandMother {
|
||||
pub parser: Box<dyn Parser>,
|
||||
pub playlist: Option<Playlist>,
|
||||
pub config: Rc<Configuration>,
|
||||
}
|
||||
|
||||
impl GrandMother {
|
||||
pub async fn new_online(config: Rc<Configuration>) -> Result<Self, Error> {
|
||||
let playlist = Playlist::new(config.playlist_path.clone(), config.playlist_url.clone());
|
||||
let seen_links = config.seen_links.iter().map(|x| x.as_str()).collect();
|
||||
let playlist = playlist.await?;
|
||||
let playlist_content = playlist.get_saved_or_download().await?;
|
||||
let parser: Box<dyn Parser> =
|
||||
Box::new(OnlineParser::new(&playlist_content, &seen_links).await);
|
||||
|
||||
Ok(Self {
|
||||
parser,
|
||||
playlist: Some(playlist),
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn promote_to_online(&mut self) -> Result<(), Error> {
|
||||
let online_mother = GrandMother::new_online(self.config.clone()).await?;
|
||||
(self.parser, self.playlist) = (online_mother.parser, online_mother.playlist);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new_offline(config: Rc<Configuration>) -> Self {
|
||||
let parser: Box<dyn Parser> = Box::new(OfflineParser::new(&config));
|
||||
Self {
|
||||
parser,
|
||||
playlist: None,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn refresh_dirty(&self) -> Result<(), Error> {
|
||||
let ptr = self as *const Self as *mut Self;
|
||||
unsafe { &mut *ptr }.refresh().await
|
||||
}
|
||||
|
||||
pub async fn refresh(&mut self) -> Result<(), Error> {
|
||||
let mut counter = 0;
|
||||
let content = loop {
|
||||
counter += 1;
|
||||
let content = self
|
||||
.playlist
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Cannot refresh playlist in offlinemode")?
|
||||
.download()
|
||||
.await;
|
||||
if counter > MAX_TRIES {
|
||||
return Ok(());
|
||||
}
|
||||
if let Ok(content) = content {
|
||||
break content;
|
||||
}
|
||||
println!("Retrying {}/{}", counter, MAX_TRIES);
|
||||
};
|
||||
|
||||
let watched_links = self
|
||||
.parser
|
||||
.get_watched()
|
||||
.iter()
|
||||
.map(|x| x.link.as_str())
|
||||
.collect();
|
||||
self.parser = Box::new(OnlineParser::new(&content, &watched_links).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_watched(&self) {
|
||||
let watched_items = self.parser.get_watched();
|
||||
|
||||
let resp = fs::write(
|
||||
&self.config.seen_links_path,
|
||||
serde_json::to_string(&watched_items).unwrap(),
|
||||
);
|
||||
|
||||
if let Err(e) = resp {
|
||||
eprintln!("Failed to write watched links {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
102
src/lib.rs
102
src/lib.rs
@ -1,46 +1,32 @@
|
||||
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 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 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 {
|
||||
let project_dirs = ProjectDirs::from("com", "billenius", "iptvnator_rs").unwrap();
|
||||
let config_dir = project_dirs.config_dir();
|
||||
let ilovetv_config_file = config_dir.join("iptv_url.txt");
|
||||
if ilovetv_config_file.exists() {
|
||||
return fs::read_to_string(&ilovetv_config_file).expect("Failed to read iptv_url");
|
||||
}
|
||||
|
||||
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 const JSON_CONFIG_FILENAME: &'static str = "config.json";
|
||||
pub const APP_IDENTIFIER: [&'static str; 3] = ["com", "billenius", "ilovetv"];
|
||||
pub const STANDARD_PLAYLIST_FILENAME: &'static str = "playlist.m3u8";
|
||||
pub const STANDARD_SEEN_LINKS_FILENAME: &'static str = "watched_links.json";
|
||||
pub const STANDARD_OFFLINE_FILENAME: &'static str = "ilovetv_offline.json";
|
||||
pub const MAX_TRIES: u8 = 4;
|
||||
|
||||
pub struct Readline<'a> {
|
||||
stdout: StdoutLock<'a>,
|
||||
@ -63,3 +49,41 @@ impl<'a> Readline<'a> {
|
||||
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, String> {
|
||||
match mode {
|
||||
Mode::Online => GrandMother::new_online(config).await,
|
||||
Mode::Offline => Ok(GrandMother::new_offline(config)),
|
||||
Mode::Ask => loop {
|
||||
let input = readline
|
||||
.input("Online/Offline mode? [1/2] ")
|
||||
.trim()
|
||||
.parse::<u8>();
|
||||
if let Ok(num) = input {
|
||||
if num == 1 {
|
||||
return get_gm(Mode::Online, readline, config).await;
|
||||
} else if num == 2 {
|
||||
return get_gm(Mode::Offline, readline, config).await;
|
||||
}
|
||||
println!("Has to be either 1 (Onine) or 2 (Offline)");
|
||||
} else {
|
||||
println!("Has to be a number");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
29
src/m3u8.rs
29
src/m3u8.rs
@ -1,13 +1,18 @@
|
||||
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 tvg_id: String,
|
||||
pub tvg_name: String,
|
||||
pub tvg_logo: String,
|
||||
pub group_title: String,
|
||||
pub name: String,
|
||||
pub link: String,
|
||||
pub link: Rc<String>,
|
||||
pub watched: bool,
|
||||
}
|
||||
|
||||
@ -22,3 +27,23 @@ impl Display for M3u8 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
314
src/main.rs
314
src/main.rs
@ -2,32 +2,125 @@ use std::num::ParseIntError;
|
||||
use std::process::Command;
|
||||
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]
|
||||
async fn main() {
|
||||
println!("Welcome to iptvnator_rs, the port of my iptvprogram written in python, now in rust BLAZINGLY FAST\n");
|
||||
let parser = Parser::new("iptv.m3u8".to_owned(), setup(), "watched.txt".to_owned()).await;
|
||||
let opt = Opt::from_args();
|
||||
|
||||
// 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 to online mode", "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 gm = GrandMother::new(Configuration::new().expect("Failed to write to configfile"))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let parser = Parser::new(config.clone()).await;
|
||||
|
||||
let mut mpv_fs = false;
|
||||
let mut search_result: Option<Rc<Vec<&M3u8>>> = None;
|
||||
let mut readline = Readline::new();
|
||||
let gm = get_gm(
|
||||
opt.mode,
|
||||
&mut readline,
|
||||
Rc::new(Configuration::new().expect("Failed to write to configfile")),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to retrive online playlist");
|
||||
|
||||
loop {
|
||||
// Dont't perform a search if user has just watched, instead present the previous search
|
||||
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-
|
||||
if search.trim() == "q" {
|
||||
break;
|
||||
}
|
||||
|
||||
search_result = Some(Rc::new(parser.find(&search)));
|
||||
|
||||
if search_result.as_ref().unwrap().len() == 0 {
|
||||
println!("Nothing found");
|
||||
// Special commands
|
||||
match search {
|
||||
// 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" => {
|
||||
let result = unsafe { get_mut_ref(&gm) }.promote_to_online().await;
|
||||
if let Err(e) = result {
|
||||
println!("Failed to switch to onlinemode {:?}", e);
|
||||
}
|
||||
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
|
||||
@ -35,98 +128,179 @@ async fn main() {
|
||||
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();
|
||||
|
||||
// If they want to quit, let them-
|
||||
if user_wish == "q" {
|
||||
break;
|
||||
} else if user_wish == "s" {
|
||||
match user_wish {
|
||||
// Quit
|
||||
"q" => break,
|
||||
// Go inte search-mode
|
||||
"s" => {
|
||||
search_result = None;
|
||||
continue;
|
||||
} else if user_wish == "r" {
|
||||
}
|
||||
// Refresh playlist
|
||||
"r" => {
|
||||
println!("Refreshing local m3u8-file");
|
||||
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;
|
||||
} else if user_wish == "d" {
|
||||
let selection = readline.input("Download all or select in comma separated [A]: ");
|
||||
let selection = selection.trim();
|
||||
|
||||
let to_download = loop {
|
||||
break if selection.to_lowercase() == "a" {
|
||||
println!("Downloading all");
|
||||
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");
|
||||
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;
|
||||
}
|
||||
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]);
|
||||
// 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 path = gm.config.data_dir.join(&to_download.name);
|
||||
let path = Rc::new(path.to_string_lossy().to_string());
|
||||
download_m3u8(to_download, Some(&path)).await;
|
||||
let data_entry = OfflineEntry::new((*to_download).clone(), path);
|
||||
gm.config.push_offlinefile_ugly(data_entry);
|
||||
}
|
||||
|
||||
Rc::new(final_selections)
|
||||
};
|
||||
};
|
||||
download_m3u8(to_download).await;
|
||||
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>();
|
||||
match choosen {
|
||||
Ok(k) => {
|
||||
let search_result = search_result.as_ref().unwrap().clone();
|
||||
stream(&(search_result[k - 1]))
|
||||
let search_result = search_result.as_ref().unwrap();
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
parser.save_watched();
|
||||
}
|
||||
|
||||
async fn download_m3u8(files_to_download: Rc<Vec<&M3u8>>) {
|
||||
for m3u8 in files_to_download.iter() {
|
||||
let file_ending_place = m3u8.link.rfind(".").unwrap();
|
||||
let potential_file_ending = &m3u8.link[file_ending_place..];
|
||||
fn ask_which_to_download<'a>(
|
||||
readline: &mut Readline,
|
||||
search_result: &Rc<Vec<&'a M3u8>>,
|
||||
) -> 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 {
|
||||
".mkv"
|
||||
} else {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* I know that this is not how youre supposed to do things, but it's perfectly safe
|
||||
* in this context and also the most efficient way.
|
||||
* With other words, it's BLAZINGLY FAST
|
||||
*/
|
||||
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");
|
||||
}
|
||||
|
||||
fn stream(m3u8item: &M3u8) {
|
||||
// Well I know that this is frowned upon, but it's honestly the most efficient way of doing this
|
||||
let ptr = m3u8item as *const M3u8;
|
||||
let ptr = ptr as *mut M3u8;
|
||||
let mut item = unsafe { &mut *ptr };
|
||||
item.watched = true;
|
||||
|
||||
Command::new("mpv")
|
||||
.arg(&m3u8item.link)
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("Could not listen for output");
|
||||
}
|
||||
|
42
src/offlineparser.rs
Normal file
42
src/offlineparser.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use std::{ops::Deref, rc::Rc};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{m3u8::M3u8, Configuration, GetM3u8, GetPlayPath, OfflineEntry};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OfflineParser {
|
||||
m3u8_items: Rc<Vec<OfflineEntry>>,
|
||||
}
|
||||
impl OfflineParser {
|
||||
pub fn new(config: &Configuration) -> Self {
|
||||
Self {
|
||||
m3u8_items: config.offlinefile_content.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for OfflineParser {
|
||||
type Target = Vec<OfflineEntry>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.m3u8_items
|
||||
}
|
||||
}
|
||||
|
||||
impl GetPlayPath for OfflineParser {
|
||||
fn get_path_to_play(&self, link: Rc<String>) -> Result<Rc<String>, String> {
|
||||
for offline_entry in &*self.m3u8_items {
|
||||
if *offline_entry.link == *link {
|
||||
return Ok(offline_entry.path.clone());
|
||||
}
|
||||
}
|
||||
Err("Not stored for offline use".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl GetM3u8 for OfflineParser {
|
||||
fn get_m3u8(&self) -> Vec<&M3u8> {
|
||||
self.m3u8_items.iter().map(|x| &**x).collect()
|
||||
}
|
||||
}
|
91
src/onlineparser.rs
Normal file
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"),
|
||||
}
|
||||
}
|
||||
}
|
208
src/parser.rs
208
src/parser.rs
@ -1,200 +1,32 @@
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::{fs, process};
|
||||
|
||||
use directories::ProjectDirs;
|
||||
use crate::M3u8;
|
||||
|
||||
use crate::downloader::download_with_progress;
|
||||
use crate::m3u8::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>,
|
||||
pub trait GetM3u8 {
|
||||
fn get_m3u8(&self) -> Vec<&M3u8>;
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub async fn new(file_name: String, iptv_url: String, watched_name: String) -> Self {
|
||||
let project_dirs = ProjectDirs::from("com", "billenius", "iptvnator_rs").unwrap();
|
||||
let cache = project_dirs.cache_dir();
|
||||
let _ = fs::create_dir_all(&cache);
|
||||
|
||||
let file_name = Rc::new(cache.join(file_name));
|
||||
let ilovetv_url = Rc::new(iptv_url);
|
||||
let watched_name = Rc::new(cache.join(watched_name));
|
||||
|
||||
Self {
|
||||
watched_name: watched_name.clone(),
|
||||
m3u8_items: Self::get_parsed_content(&ilovetv_url, &file_name, &watched_name).await,
|
||||
ilovetv_url,
|
||||
file_name,
|
||||
}
|
||||
pub trait WatchedFind {
|
||||
fn find(&self, name: &str) -> Vec<&M3u8>;
|
||||
fn get_watched(&self) -> Vec<&M3u8>;
|
||||
}
|
||||
|
||||
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))
|
||||
impl<T: ?Sized + GetM3u8> WatchedFind for T {
|
||||
fn find(&self, name: &str) -> Vec<&M3u8> {
|
||||
let name = name.to_lowercase();
|
||||
self.get_m3u8()
|
||||
.into_iter()
|
||||
.filter(|item| item.name.to_lowercase().contains(&name) || item.tvg_id.contains(&name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn should_update(file_name: &PathBuf) -> bool {
|
||||
fs::metadata(&file_name)
|
||||
.and_then(|metadata| {
|
||||
Ok({
|
||||
let seconds = metadata.modified()?;
|
||||
seconds
|
||||
.elapsed()
|
||||
.expect("Failed to get systemtime")
|
||||
.as_secs()
|
||||
> 60 * 60 * 24 * 3
|
||||
})
|
||||
})
|
||||
.map_or_else(
|
||||
|_| {
|
||||
println!("Could not find playlist-file, Downloading a new one");
|
||||
false
|
||||
},
|
||||
|x| x,
|
||||
)
|
||||
fn get_watched(&self) -> Vec<&M3u8> {
|
||||
self.get_m3u8().into_iter().filter(|x| x.watched).collect()
|
||||
}
|
||||
}
|
||||
pub trait GetPlayPath {
|
||||
fn get_path_to_play(&self, link: Rc<String>) -> Result<Rc<String>, String>;
|
||||
}
|
||||
|
||||
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 Parser: GetM3u8 + GetPlayPath {}
|
||||
impl<T: GetM3u8 + GetPlayPath> Parser for T {}
|
||||
|
99
src/playlist.rs
Normal file
99
src/playlist.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use std::{fs, ops::Deref, path::PathBuf, rc::Rc};
|
||||
|
||||
use crate::{download_with_progress, downloader::DualWriter, MAX_TRIES};
|
||||
|
||||
pub struct Playlist {
|
||||
pub content: String,
|
||||
path_to_playlist: Rc<PathBuf>,
|
||||
url: Option<Rc<String>>,
|
||||
}
|
||||
|
||||
impl Playlist {
|
||||
pub async fn new(path_to_playlist: Rc<PathBuf>, url: Rc<String>) -> Result<Self, String> {
|
||||
let mut me = Self {
|
||||
content: String::new(),
|
||||
path_to_playlist,
|
||||
url: Some(url),
|
||||
};
|
||||
me.content = me.get_saved_or_download().await?;
|
||||
|
||||
Ok(me)
|
||||
}
|
||||
|
||||
fn get_saved(&self) -> Option<String> {
|
||||
if !self.should_update() {
|
||||
return fs::read_to_string(&*self.path_to_playlist).ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn should_update(&self) -> bool {
|
||||
fs::metadata(&*self.path_to_playlist)
|
||||
.and_then(|metadata| {
|
||||
Ok({
|
||||
let seconds = metadata.modified()?;
|
||||
seconds
|
||||
.elapsed()
|
||||
.expect("Failed to get systemtime")
|
||||
.as_secs()
|
||||
> 60 * 60 * 24 * 3
|
||||
})
|
||||
})
|
||||
.map_or_else(
|
||||
|_| {
|
||||
println!("Could not find playlist-file, Downloading a new one");
|
||||
false
|
||||
},
|
||||
|x| x,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_saved_or_download(&self) -> Result<String, String> {
|
||||
let content = if let Some(content) = self.get_saved() {
|
||||
content
|
||||
} else {
|
||||
let downloaded = self.download().await?;
|
||||
if let Err(e) = fs::write(&*self.path_to_playlist, &downloaded) {
|
||||
println!(
|
||||
"Failed to save downloaded playlist to file, {:?}, path: '{}'",
|
||||
e,
|
||||
&self.path_to_playlist.as_os_str().to_str().unwrap()
|
||||
);
|
||||
}
|
||||
downloaded
|
||||
};
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub async fn download(&self) -> Result<String, String> {
|
||||
let mut counter: u8 = 0;
|
||||
loop {
|
||||
counter += 1;
|
||||
|
||||
let url = self
|
||||
.url
|
||||
.as_ref()
|
||||
.ok_or_else(|| String::from("In offline mode"))?
|
||||
.clone();
|
||||
|
||||
let downloaded = download_with_progress(&url, None)
|
||||
.await
|
||||
.and_then(DualWriter::get_string);
|
||||
if let Ok(content) = downloaded {
|
||||
break Ok(content);
|
||||
} else if counter > MAX_TRIES {
|
||||
break Err("Failed to download playlist".to_owned());
|
||||
}
|
||||
println!("Retrying {}/{}", counter + 1, MAX_TRIES);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Playlist {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.content
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user