Compare commits

...

36 Commits

Author SHA1 Message Date
71ded77fbc
Fix parsing error when leaving download/offline-mode 2023-03-06 07:03:26 +01:00
37bc5b11c7
Functionality for switching to online from offline 2023-03-05 20:26:52 +01:00
6da49ec983
consider the traits parser instead 2023-03-05 20:04:37 +01:00
0d903f80ff
remove unneeded imports 2023-03-05 19:55:46 +01:00
38b0e405d0
More refactors 2023-03-05 19:51:21 +01:00
81006d4c5c
Early working version 2023-03-05 19:14:26 +01:00
2b30b5335f
start with offlineparser and grandmother 2023-03-05 16:35:01 +01:00
da1e754ec3
Remove unneeded imports 2023-03-05 12:14:34 +01:00
92f2031dad
grandmother new file 2023-03-05 12:13:42 +01:00
c162450bdd
Create new struct GrandMother and refactor to her 2023-03-05 12:10:49 +01:00
af1d257c0a
start 2023-03-03 13:13:39 +01:00
8417969b9d saves to downloadmode correctly 2023-03-03 00:36:06 +01:00
bc6b5cba34 Created basic structure 2023-03-02 23:51:37 +01:00
eb1e4e7ce1 sections 2023-03-02 22:52:35 +01:00
0f611f9aa6 Switch the name to ilovetv 2023-03-02 20:05:31 +01:00
0999f24691 Easier to read the source code of the greeting 2023-03-02 18:15:56 +01:00
d07d4d70b4 clear in both selections 2023-03-01 22:56:02 +01:00
3171e5568d Add support for remembering the latest search across sessions 2023-03-01 22:53:09 +01:00
0d02826b77 A lot of refactoring
Made the program a lot more configurable and also cleaned up the parser
struct
2023-03-01 21:59:52 +01:00
c27d0c4cb9 remove non-needed dependency 2023-02-28 16:29:28 +01:00
41a4af4667 tidying up 2023-02-28 16:22:53 +01:00
6197c52bf8 breakout downloadmode 2023-02-28 15:37:33 +01:00
7d6aa90dad Save watched right after something is watched, and use match 2023-02-27 16:24:16 +01:00
f69a040287 lock 2023-02-27 13:37:31 +01:00
304fa44629 toggle mpv fullscreen 2023-02-02 16:41:36 +01:00
39ced4e0d7 print error and also better welcome message 2023-01-19 18:13:38 +01:00
3a977a8358 Merge branch 'main' of billenius.com:love/iptvnator 2023-01-19 17:58:44 +01:00
5637f62a8b if failed to retrive content length, then print error 2023-01-19 17:57:41 +01:00
dccb4f1c40
lowercase 2023-01-19 14:14:35 +01:00
c7fff46ff3 1,2,3,4 2023-01-19 12:55:38 +01:00
866e56c247 a 2023-01-19 12:55:00 +01:00
bef4208351 name 2023-01-19 12:47:17 +01:00
2dda01e651 comment 2023-01-19 12:40:54 +01:00
f4c184b2c7 more options 2023-01-19 12:37:24 +01:00
608176262d Merge branch 'main' of billenius.com:love/iptvnator_rs 2023-01-19 12:22:35 +01:00
2f5fcdb9dd
bug 2023-01-19 12:16:50 +01:00
14 changed files with 2442 additions and 327 deletions

1523
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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

View File

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

View File

@ -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
View 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);
}
}
}

View File

@ -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");
}
},
}
}

View File

@ -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
}
}

View File

@ -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
View File

@ -0,0 +1,42 @@
use std::{ops::Deref, rc::Rc};
use serde::Serialize;
use crate::{m3u8::M3u8, Configuration, GetM3u8, GetPlayPath, OfflineEntry};
#[derive(Serialize)]
pub struct OfflineParser {
m3u8_items: Rc<Vec<OfflineEntry>>,
}
impl OfflineParser {
pub fn new(config: &Configuration) -> Self {
Self {
m3u8_items: config.offlinefile_content.clone(),
}
}
}
impl Deref for OfflineParser {
type Target = Vec<OfflineEntry>;
fn deref(&self) -> &Self::Target {
&*self.m3u8_items
}
}
impl GetPlayPath for OfflineParser {
fn get_path_to_play(&self, link: Rc<String>) -> Result<Rc<String>, String> {
for offline_entry in &*self.m3u8_items {
if *offline_entry.link == *link {
return Ok(offline_entry.path.clone());
}
}
Err("Not stored for offline use".to_owned())
}
}
impl GetM3u8 for OfflineParser {
fn get_m3u8(&self) -> Vec<&M3u8> {
self.m3u8_items.iter().map(|x| &**x).collect()
}
}

91
src/onlineparser.rs Normal file
View File

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

36
src/opt.rs Normal file
View File

@ -0,0 +1,36 @@
use std::str::FromStr;
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
#[structopt(name = "ilovetv")]
pub struct Opt {
#[structopt(short, long, default_value = "ask")]
/// Possible options: online, offline and ask
pub mode: Mode,
}
#[derive(Debug)]
pub enum Mode {
Online,
Offline,
Ask,
}
impl Default for Mode {
fn default() -> Self {
Self::Ask
}
}
impl FromStr for Mode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"online" => Ok(Self::Online),
"offline" => Ok(Self::Offline),
"ask" | "default" => Ok(Self::Ask),
_ => Err("No such enum"),
}
}
}

View File

@ -1,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
View File

@ -0,0 +1,99 @@
use std::{fs, ops::Deref, path::PathBuf, rc::Rc};
use crate::{download_with_progress, downloader::DualWriter, MAX_TRIES};
pub struct Playlist {
pub content: String,
path_to_playlist: Rc<PathBuf>,
url: Option<Rc<String>>,
}
impl Playlist {
pub async fn new(path_to_playlist: Rc<PathBuf>, url: Rc<String>) -> Result<Self, String> {
let mut me = Self {
content: String::new(),
path_to_playlist,
url: Some(url),
};
me.content = me.get_saved_or_download().await?;
Ok(me)
}
fn get_saved(&self) -> Option<String> {
if !self.should_update() {
return fs::read_to_string(&*self.path_to_playlist).ok();
}
None
}
fn should_update(&self) -> bool {
fs::metadata(&*self.path_to_playlist)
.and_then(|metadata| {
Ok({
let seconds = metadata.modified()?;
seconds
.elapsed()
.expect("Failed to get systemtime")
.as_secs()
> 60 * 60 * 24 * 3
})
})
.map_or_else(
|_| {
println!("Could not find playlist-file, Downloading a new one");
false
},
|x| x,
)
}
pub async fn get_saved_or_download(&self) -> Result<String, String> {
let content = if let Some(content) = self.get_saved() {
content
} else {
let downloaded = self.download().await?;
if let Err(e) = fs::write(&*self.path_to_playlist, &downloaded) {
println!(
"Failed to save downloaded playlist to file, {:?}, path: '{}'",
e,
&self.path_to_playlist.as_os_str().to_str().unwrap()
);
}
downloaded
};
Ok(content)
}
pub async fn download(&self) -> Result<String, String> {
let mut counter: u8 = 0;
loop {
counter += 1;
let url = self
.url
.as_ref()
.ok_or_else(|| String::from("In offline mode"))?
.clone();
let downloaded = download_with_progress(&url, None)
.await
.and_then(DualWriter::get_string);
if let Ok(content) = downloaded {
break Ok(content);
} else if counter > MAX_TRIES {
break Err("Failed to download playlist".to_owned());
}
println!("Retrying {}/{}", counter + 1, MAX_TRIES);
}
}
}
impl Deref for Playlist {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.content
}
}