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]]
name = "ilovetv"
version = "1.0.0"
version = "1.1.0"
dependencies = [
"async-recursion",
"bytes",

View File

@ -1,6 +1,7 @@
[package]
name = "ilovetv"
version = "1.0.0"
authors = ["Love Billenius <lovebillenius@disroot.org>"]
version = "1.1.0"
edition = "2021"
[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_
## 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
## Left to do

View File

@ -1,6 +1,6 @@
use std::{
fmt,
fs::File,
error, fmt,
fs::{File, OpenOptions},
io::{self, Read, Write},
};
@ -31,7 +31,44 @@ impl DualWriter {
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 {
Self::Buffer(buffer) => {
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(
link: &str,
file_name: Option<&str>,
) -> Result<DualWriter, String> {
let mut dual_writer = DualWriter::new(file_name).or(Err("Failed to create file".to_owned()))?;
) -> Result<DualWriter, Box<dyn error::Error>> {
let mut dual_writer: DualWriter = file_name.try_into()?;
let client = Client::builder()
.gzip(true)
.deflate(true)
.build()
.or(Err("Failed to create client".to_owned()))?;
let resp = client
let client = Client::builder().gzip(true).deflate(true).build()?;
let builder = client
.get(link)
.send()
.await
.or(Err("Failed to connect server".to_owned()))?;
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);
};
.header("Range", format!("bytes={}-", dual_writer.len()));
let resp = builder.send().await?;
let content_length = resp.content_length().unwrap_or_default();
if content_length == 0 {
println!("File was already downloaded");
return Ok(dual_writer);
}
let progress_bar = ProgressBar::new(content_length);
progress_bar.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})").unwrap()
.with_key("eta", |state: &ProgressState, w: &mut dyn fmt::Write| write!(w, "{:.1}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("#>-"));
let mut downloaded: u64 = 0;
@ -90,9 +116,7 @@ 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);
dual_writer
.write(bytes)
.or(Err("Failed to write to file".to_owned()))?;
dual_writer.write(bytes)?;
progress_bar.set_position(downloaded);
}

View File

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

View File

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

View File

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

View File

@ -6,12 +6,12 @@ use crate::{m3u8::M3u8, Configuration, GetM3u8, GetPlayPath, OfflineEntry};
#[derive(Serialize)]
pub struct OfflineParser {
m3u8_items: Rc<Vec<OfflineEntry>>,
offline_entries: Rc<Vec<OfflineEntry>>,
}
impl OfflineParser {
pub fn new(config: &Configuration) -> 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>;
fn deref(&self) -> &Self::Target {
&*self.m3u8_items
&*self.offline_entries
}
}
impl GetPlayPath for OfflineParser {
fn get_path_to_play(&self, link: Rc<String>) -> Result<Rc<String>, String> {
for offline_entry in &*self.m3u8_items {
for offline_entry in &*self.offline_entries {
if *offline_entry.link == *link {
return Ok(offline_entry.path.clone());
}
@ -37,6 +37,8 @@ impl GetPlayPath for OfflineParser {
impl GetM3u8 for OfflineParser {
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 {
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 {
@ -20,8 +20,12 @@ impl<T: ?Sized + GetM3u8> WatchedFind for T {
.collect()
}
fn get_watched(&self) -> Vec<&M3u8> {
self.get_m3u8().into_iter().filter(|x| x.watched).collect()
fn get_watched_links(&self) -> Vec<Rc<String>> {
self.get_m3u8()
.into_iter()
.filter(|x| x.watched)
.map(|x| x.link.clone())
.collect()
}
}
pub trait GetPlayPath {

View File

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