Compare commits

..

49 Commits

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

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

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

2
Cargo.lock generated
View File

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

View File

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

View File

@ -1,10 +1,10 @@
# Iptvnator # I Love TV
An iptvclient that is capable of parsing and reading m3uplaylists of essentially arbitrary sizes _blazingly fast_ An iptvclient that is capable of parsing and reading m3uplaylists of essentially arbitrary sizes _blazingly fast_
## Install ## Install
Just clone the repo and run `cargo build --release` to compile the project. Then put it in your `$PATH` or make a shortcut to the binary (target/release/iptvnator_rs) Just clone the repo and run `cargo build --release` to compile the project. Then put it in your `$PATH` or make a shortcut to the binary (target/release/ilovetv)
You will need to install mpv, and have it in your path, otherwise it wont work You will need to install mpv, and have it in your path, otherwise it wont work
## Left to do ## Left to do

View File

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

View File

@ -1,6 +1,7 @@
#[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,
}; };
@ -30,6 +31,11 @@ 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);
@ -47,8 +53,7 @@ impl GrandMother {
} }
pub async fn refresh_dirty(&self) -> Result<(), Error> { pub async fn refresh_dirty(&self) -> Result<(), Error> {
let ptr = self as *const Self as *mut Self; unsafe { get_mut_ref(self) }.refresh().await
unsafe { &mut *ptr }.refresh().await
} }
pub async fn refresh(&mut self) -> Result<(), Error> { pub async fn refresh(&mut self) -> Result<(), Error> {
@ -70,19 +75,15 @@ impl GrandMother {
println!("Retrying {}/{}", counter, MAX_TRIES); println!("Retrying {}/{}", counter, MAX_TRIES);
}; };
let watched_links = self let watched_links = self.parser.get_watched_links();
.parser let watched_links = watched_links.iter().map(|x| x.as_str()).collect();
.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(); let watched_items = self.parser.get_watched_links();
let resp = fs::write( let resp = fs::write(
&self.config.seen_links_path, &self.config.seen_links_path,

View File

@ -8,7 +8,10 @@ mod opt;
pub mod parser; pub mod parser;
mod playlist; mod playlist;
use std::{io::{stdin, stdout, Stdin, StdoutLock, Write}, rc::Rc}; use std::{
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;
@ -65,10 +68,10 @@ pub async fn get_gm(
mode: Mode, mode: Mode,
readline: &mut Readline<'_>, readline: &mut Readline<'_>,
config: Rc<Configuration>, config: Rc<Configuration>,
) -> Result<GrandMother, String> { ) -> Result<(GrandMother, bool), String> {
match mode { match mode {
Mode::Online => GrandMother::new_online(config).await, Mode::Online => Ok((GrandMother::new_online(config).await?, true)),
Mode::Offline => Ok(GrandMother::new_offline(config)), Mode::Offline => Ok((GrandMother::new_offline(config), false)),
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 to online mode", "m".bold()), format!(" {} is to switch between modes (toggle)", "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,15 +44,10 @@ 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 = get_gm( let (gm, mut in_online) = 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")),
@ -105,10 +100,19 @@ async fn main() {
continue; continue;
} }
"m" => { "m" => {
if in_online {
unsafe { get_mut_ref(&gm) }.demote_to_offline();
println!("Switched to offline mode");
} else {
let result = unsafe { get_mut_ref(&gm) }.promote_to_online().await; let result = unsafe { get_mut_ref(&gm) }.promote_to_online().await;
if let Err(e) = result { if let Err(e) = result {
println!("Failed to switch to onlinemode {:?}", e); println!("Failed to switch to onlinemode {:?}", e);
} else {
println!("Switched to online mode");
continue;
} }
}
in_online = !in_online;
continue; continue;
} }
_ => {} _ => {}
@ -178,7 +182,22 @@ 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 path = gm.config.data_dir.join(&to_download.name); let file_ending = to_download
.name
.rfind(".")
.map(|dot_idx| {
if dot_idx < 6 {
&to_download.name[dot_idx..]
} else {
".mkv"
}
})
.unwrap_or_else(|| ".mkv");
let path = gm
.config
.data_dir
.join(format!("{}{}", &to_download.name, file_ending));
let path = Rc::new(path.to_string_lossy().to_string()); 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);
@ -214,7 +233,6 @@ 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 {
m3u8_items: Rc<Vec<OfflineEntry>>, offline_entries: Rc<Vec<OfflineEntry>>,
} }
impl OfflineParser { impl OfflineParser {
pub fn new(config: &Configuration) -> Self { pub fn new(config: &Configuration) -> Self {
Self { Self {
m3u8_items: config.offlinefile_content.clone(), offline_entries: 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.m3u8_items &*self.offline_entries
} }
} }
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.m3u8_items { for offline_entry in &*self.offline_entries {
if *offline_entry.link == *link { if *offline_entry.link == *link {
return Ok(offline_entry.path.clone()); return Ok(offline_entry.path.clone());
} }
@ -37,6 +37,8 @@ impl GetPlayPath for OfflineParser {
impl GetM3u8 for OfflineParser { impl GetM3u8 for OfflineParser {
fn get_m3u8(&self) -> Vec<&M3u8> { fn get_m3u8(&self) -> Vec<&M3u8> {
self.m3u8_items.iter().map(|x| &**x).collect() let mut items: Vec<&M3u8> = self.offline_entries.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(&self) -> Vec<&M3u8>; fn get_watched_links(&self) -> Vec<Rc<String>>;
} }
impl<T: ?Sized + GetM3u8> WatchedFind for T { impl<T: ?Sized + GetM3u8> WatchedFind for T {
@ -20,8 +20,12 @@ impl<T: ?Sized + GetM3u8> WatchedFind for T {
.collect() .collect()
} }
fn get_watched(&self) -> Vec<&M3u8> { fn get_watched_links(&self) -> Vec<Rc<String>> {
self.get_m3u8().into_iter().filter(|x| x.watched).collect() self.get_m3u8()
.into_iter()
.filter(|x| x.watched)
.map(|x| x.link.clone())
.collect()
} }
} }
pub trait GetPlayPath { pub trait GetPlayPath {

View File

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