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
11 changed files with 85 additions and 147 deletions

7
.vscode/launch.json vendored
View File

@ -1,7 +0,0 @@
{
// 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": [],
}

2
Cargo.lock generated
View File

@ -462,7 +462,7 @@ dependencies = [
[[package]] [[package]]
name = "ilovetv" name = "ilovetv"
version = "1.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"async-recursion", "async-recursion",
"bytes", "bytes",

View File

@ -1,7 +1,6 @@
[package] [package]
name = "ilovetv" name = "ilovetv"
authors = ["Love Billenius <lovebillenius@disroot.org>"] version = "1.0.0"
version = "1.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -1,10 +1,10 @@
# I Love TV # Iptvnator
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/ilovetv) 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)
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

View File

@ -1,6 +1,6 @@
use std::{ use std::{
error, fmt, fmt,
fs::{File, OpenOptions}, fs::File,
io::{self, Read, Write}, io::{self, Read, Write},
}; };
@ -31,44 +31,7 @@ impl DualWriter {
Ok(()) Ok(())
} }
pub fn len(&self) -> u64 { pub fn get_string(self) -> Result<String, String> {
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()))?
@ -84,30 +47,41 @@ impl TryInto<String> for DualWriter {
} }
}) })
} }
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, Box<dyn error::Error>> { ) -> Result<DualWriter, String> {
let mut dual_writer: DualWriter = file_name.try_into()?; let mut dual_writer = DualWriter::new(file_name).or(Err("Failed to create file".to_owned()))?;
let client = Client::builder().gzip(true).deflate(true).build()?; let client = Client::builder()
let builder = client .gzip(true)
.deflate(true)
.build()
.or(Err("Failed to create client".to_owned()))?;
let resp = client
.get(link) .get(link)
.header("Range", format!("bytes={}-", dual_writer.len())); .send()
.await
let resp = builder.send().await?; .or(Err("Failed to connect server".to_owned()))?;
let content_length = if let Some(got_content_length) = resp.content_length() {
let content_length = resp.content_length().unwrap_or_default(); got_content_length
if content_length == 0 { } else {
println!("File was already downloaded"); panic!("Could not retrive content length from server. {:?}", &resp);
return Ok(dual_writer); };
}
let progress_bar = ProgressBar::new(content_length); 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() 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()) .with_key("eta", |state: &ProgressState, w: &mut dyn fmt::Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap())
.progress_chars("#>-")); .progress_chars("#>-"));
let mut downloaded: u64 = 0; let mut downloaded: u64 = 0;
@ -116,7 +90,9 @@ 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);
dual_writer.write(bytes)?; dual_writer
.write(bytes)
.or(Err("Failed to write to file".to_owned()))?;
progress_bar.set_position(downloaded); progress_bar.set_position(downloaded);
} }

View File

@ -1,7 +1,6 @@
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::GetM3u8; use crate::GetM3u8;
use crate::{ use crate::{
get_mut_ref,
parser::{Parser, WatchedFind}, parser::{Parser, WatchedFind},
Configuration, OfflineParser, OnlineParser, Playlist, MAX_TRIES, Configuration, OfflineParser, OnlineParser, Playlist, MAX_TRIES,
}; };
@ -31,11 +30,6 @@ impl GrandMother {
}) })
} }
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> { pub async fn promote_to_online(&mut self) -> Result<(), Error> {
let online_mother = GrandMother::new_online(self.config.clone()).await?; let online_mother = GrandMother::new_online(self.config.clone()).await?;
(self.parser, self.playlist) = (online_mother.parser, online_mother.playlist); (self.parser, self.playlist) = (online_mother.parser, online_mother.playlist);
@ -53,7 +47,8 @@ impl GrandMother {
} }
pub async fn refresh_dirty(&self) -> Result<(), Error> { pub async fn refresh_dirty(&self) -> Result<(), Error> {
unsafe { get_mut_ref(self) }.refresh().await let ptr = self as *const Self as *mut Self;
unsafe { &mut *ptr }.refresh().await
} }
pub async fn refresh(&mut self) -> Result<(), Error> { pub async fn refresh(&mut self) -> Result<(), Error> {
@ -75,15 +70,19 @@ impl GrandMother {
println!("Retrying {}/{}", counter, MAX_TRIES); println!("Retrying {}/{}", counter, MAX_TRIES);
}; };
let watched_links = self.parser.get_watched_links(); let watched_links = self
let watched_links = watched_links.iter().map(|x| x.as_str()).collect(); .parser
.get_watched()
.iter()
.map(|x| x.link.as_str())
.collect();
self.parser = Box::new(OnlineParser::new(&content, &watched_links).await); self.parser = Box::new(OnlineParser::new(&content, &watched_links).await);
Ok(()) Ok(())
} }
pub fn save_watched(&self) { pub fn save_watched(&self) {
let watched_items = self.parser.get_watched_links(); let watched_items = self.parser.get_watched();
let resp = fs::write( let resp = fs::write(
&self.config.seen_links_path, &self.config.seen_links_path,

View File

@ -8,10 +8,7 @@ mod opt;
pub mod parser; pub mod parser;
mod playlist; mod playlist;
use std::{ use std::{io::{stdin, stdout, Stdin, StdoutLock, Write}, rc::Rc};
io::{stdin, stdout, Stdin, StdoutLock, Write},
rc::Rc,
};
use async_recursion::async_recursion; use async_recursion::async_recursion;
pub use config::Configuration; pub use config::Configuration;
@ -68,10 +65,10 @@ pub async fn get_gm(
mode: Mode, mode: Mode,
readline: &mut Readline<'_>, readline: &mut Readline<'_>,
config: Rc<Configuration>, config: Rc<Configuration>,
) -> Result<(GrandMother, bool), String> { ) -> Result<GrandMother, String> {
match mode { match mode {
Mode::Online => Ok((GrandMother::new_online(config).await?, true)), Mode::Online => GrandMother::new_online(config).await,
Mode::Offline => Ok((GrandMother::new_offline(config), false)), Mode::Offline => Ok(GrandMother::new_offline(config)),
Mode::Ask => loop { Mode::Ask => loop {
let input = readline let input = readline
.input("Online/Offline mode? [1/2] ") .input("Online/Offline mode? [1/2] ")

View File

@ -31,7 +31,7 @@ async fn main() {
" {} is to make entries availibe for offline use later on in the program", " {} is to make entries availibe for offline use later on in the program",
"o".bold() "o".bold()
), ),
format!(" {} is to switch between modes (toggle)", "m".bold()), format!(" {} is to switch to online mode", "m".bold()),
format!(" {} is to perform a new search", "s".bold()), format!(" {} is to perform a new search", "s".bold()),
format!(" {} is to select all", "a".bold()), format!(" {} is to select all", "a".bold()),
format!(" {} is to toggtle fullscreen for mpv", "f".bold()), format!(" {} is to toggtle fullscreen for mpv", "f".bold()),
@ -44,10 +44,15 @@ async fn main() {
.iter() .iter()
.for_each(|s| println!("{}", &s)); .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 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( let gm = get_gm(
opt.mode, opt.mode,
&mut readline, &mut readline,
Rc::new(Configuration::new().expect("Failed to write to configfile")), Rc::new(Configuration::new().expect("Failed to write to configfile")),
@ -100,19 +105,10 @@ async fn main() {
continue; continue;
} }
"m" => { "m" => {
if in_online { let result = unsafe { get_mut_ref(&gm) }.promote_to_online().await;
unsafe { get_mut_ref(&gm) }.demote_to_offline(); if let Err(e) = result {
println!("Switched to offline mode"); println!("Failed to switch to onlinemode {:?}", e);
} 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; continue;
} }
_ => {} _ => {}
@ -182,22 +178,7 @@ async fn main() {
ask_which_to_download(&mut readline, &search_result.as_ref().unwrap()); ask_which_to_download(&mut readline, &search_result.as_ref().unwrap());
for to_download in download_selections.iter() { for to_download in download_selections.iter() {
let file_ending = to_download let path = gm.config.data_dir.join(&to_download.name);
.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()); let path = Rc::new(path.to_string_lossy().to_string());
download_m3u8(to_download, Some(&path)).await; download_m3u8(to_download, Some(&path)).await;
let data_entry = OfflineEntry::new((*to_download).clone(), path); let data_entry = OfflineEntry::new((*to_download).clone(), path);
@ -233,6 +214,7 @@ async fn main() {
println!("Not possible to refresh playlist while in offlinemode"); println!("Not possible to refresh playlist while in offlinemode");
continue; continue;
}; };
stream(to_play, &*path_link, mpv_fs); stream(to_play, &*path_link, mpv_fs);
gm.save_watched(); gm.save_watched();
} }

View File

@ -6,12 +6,12 @@ use crate::{m3u8::M3u8, Configuration, GetM3u8, GetPlayPath, OfflineEntry};
#[derive(Serialize)] #[derive(Serialize)]
pub struct OfflineParser { pub struct OfflineParser {
offline_entries: Rc<Vec<OfflineEntry>>, m3u8_items: Rc<Vec<OfflineEntry>>,
} }
impl OfflineParser { impl OfflineParser {
pub fn new(config: &Configuration) -> Self { pub fn new(config: &Configuration) -> Self {
Self { Self {
offline_entries: config.offlinefile_content.clone(), m3u8_items: config.offlinefile_content.clone(),
} }
} }
} }
@ -20,13 +20,13 @@ impl Deref for OfflineParser {
type Target = Vec<OfflineEntry>; type Target = Vec<OfflineEntry>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&*self.offline_entries &*self.m3u8_items
} }
} }
impl GetPlayPath for OfflineParser { impl GetPlayPath for OfflineParser {
fn get_path_to_play(&self, link: Rc<String>) -> Result<Rc<String>, String> { fn get_path_to_play(&self, link: Rc<String>) -> Result<Rc<String>, String> {
for offline_entry in &*self.offline_entries { for offline_entry in &*self.m3u8_items {
if *offline_entry.link == *link { if *offline_entry.link == *link {
return Ok(offline_entry.path.clone()); return Ok(offline_entry.path.clone());
} }
@ -37,8 +37,6 @@ impl GetPlayPath for OfflineParser {
impl GetM3u8 for OfflineParser { impl GetM3u8 for OfflineParser {
fn get_m3u8(&self) -> Vec<&M3u8> { fn get_m3u8(&self) -> Vec<&M3u8> {
let mut items: Vec<&M3u8> = self.offline_entries.iter().map(|x| &**x).collect(); self.m3u8_items.iter().map(|x| &**x).collect()
items.sort_by_key(|x| &x.link);
items
} }
} }

View File

@ -8,7 +8,7 @@ pub trait GetM3u8 {
pub trait WatchedFind { pub trait WatchedFind {
fn find(&self, name: &str) -> Vec<&M3u8>; fn find(&self, name: &str) -> Vec<&M3u8>;
fn get_watched_links(&self) -> Vec<Rc<String>>; fn get_watched(&self) -> Vec<&M3u8>;
} }
impl<T: ?Sized + GetM3u8> WatchedFind for T { impl<T: ?Sized + GetM3u8> WatchedFind for T {
@ -20,12 +20,8 @@ impl<T: ?Sized + GetM3u8> WatchedFind for T {
.collect() .collect()
} }
fn get_watched_links(&self) -> Vec<Rc<String>> { fn get_watched(&self) -> Vec<&M3u8> {
self.get_m3u8() self.get_m3u8().into_iter().filter(|x| x.watched).collect()
.into_iter()
.filter(|x| x.watched)
.map(|x| x.link.clone())
.collect()
} }
} }
pub trait GetPlayPath { pub trait GetPlayPath {

View File

@ -1,8 +1,6 @@
use std::{fs, ops::Deref, path::PathBuf, rc::Rc}; use std::{fs, ops::Deref, path::PathBuf, rc::Rc};
use crate::{download_with_progress, MAX_TRIES}; use crate::{download_with_progress, downloader::DualWriter, MAX_TRIES};
type Error = String;
pub struct Playlist { pub struct Playlist {
pub content: String, pub content: String,
@ -33,23 +31,24 @@ impl Playlist {
fs::metadata(&*self.path_to_playlist) fs::metadata(&*self.path_to_playlist)
.and_then(|metadata| { .and_then(|metadata| {
Ok({ Ok({
metadata let seconds = metadata.modified()?;
.modified()? seconds
.elapsed() .elapsed()
.map(|x| x.as_secs() > 60 * 60 * 24 * 3) .expect("Failed to get systemtime")
.unwrap_or_else(|_| { .as_secs()
println!("Could not get systemtime, trying to download new file"); > 60 * 60 * 24 * 3
true
})
}) })
}) })
.unwrap_or_else(|_| { .map_or_else(
println!("Could not find a saved playlist, Downloading a new one"); |_| {
false println!("Could not find playlist-file, Downloading a new one");
}) false
},
|x| x,
)
} }
pub async fn get_saved_or_download(&self) -> Result<String, Error> { pub async fn get_saved_or_download(&self) -> Result<String, String> {
let content = if let Some(content) = self.get_saved() { let content = if let Some(content) = self.get_saved() {
content content
} else { } else {
@ -67,7 +66,7 @@ impl Playlist {
Ok(content) Ok(content)
} }
pub async fn download(&self) -> Result<String, Error> { pub async fn download(&self) -> Result<String, String> {
let mut counter: u8 = 0; let mut counter: u8 = 0;
loop { loop {
counter += 1; counter += 1;
@ -80,9 +79,8 @@ impl Playlist {
let downloaded = download_with_progress(&url, None) let downloaded = download_with_progress(&url, None)
.await .await
.map(TryInto::try_into); .and_then(DualWriter::get_string);
if let Ok(content) = downloaded {
if let Ok(Ok(content)) = downloaded {
break Ok(content); break Ok(content);
} else if counter > MAX_TRIES { } else if counter > MAX_TRIES {
break Err("Failed to download playlist".to_owned()); break Err("Failed to download playlist".to_owned());