Reads standard cache and config dirs and does basically what it should
This commit is contained in:
commit
2c52b4fcfa
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "iptvnator_rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
colored = "2.0.0"
|
||||||
|
ctrlc = "3.2.4"
|
||||||
|
directories = "4.0.1"
|
||||||
|
filetime = "0.2.19"
|
||||||
|
reqwest = { version = "0.11.13", features = ["blocking", "deflate", "gzip", "rustls", "rustls-tls"] }
|
0
src/config.rs
Normal file
0
src/config.rs
Normal file
54
src/lib.rs
Normal file
54
src/lib.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
mod m3u8;
|
||||||
|
mod parser;
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
io::{stdin, stdout, Write},
|
||||||
|
process,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use m3u8::M3u8;
|
||||||
|
pub use parser::Parser;
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
use directories;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn aaaaaaaa() {
|
||||||
|
let a = directories::ProjectDirs::from("com", "billenius", "iptvnator_rs").unwrap();
|
||||||
|
let datadir = a.data_dir();
|
||||||
|
let configdir = a.config_dir();
|
||||||
|
println!("{:?}", datadir);
|
||||||
|
println!("{:?}", configdir);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup() -> String {
|
||||||
|
let project_dirs = directories::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");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Hello, I would need an url to your iptv/m3u/m3u8 stream");
|
||||||
|
print!("enter url: ");
|
||||||
|
let mut stdout = stdout().lock();
|
||||||
|
stdout.flush().unwrap();
|
||||||
|
let mut url = String::new();
|
||||||
|
let stdin = stdin();
|
||||||
|
let _ = stdin.read_line(&mut url);
|
||||||
|
print!("Are you sure? (Y/n) ");
|
||||||
|
stdout.flush().unwrap();
|
||||||
|
let mut yn = String::new();
|
||||||
|
let _ = stdin.read_line(&mut yn);
|
||||||
|
if yn.trim() == "n" {
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = fs::create_dir_all(config_dir);
|
||||||
|
if let Err(e) = fs::write(ilovetv_config_file, url.trim()) {
|
||||||
|
eprintln!("{:?}", e);
|
||||||
|
process::exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
url.to_string()
|
||||||
|
}
|
24
src/m3u8.rs
Normal file
24
src/m3u8.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use colored::Colorize;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
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 watched: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for M3u8 {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let colored_name = if self.watched {
|
||||||
|
self.name.bold().green()
|
||||||
|
} else {
|
||||||
|
self.name.bold()
|
||||||
|
};
|
||||||
|
f.write_fmt(format_args!("{}({})", colored_name, self.link))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
76
src/main.rs
Normal file
76
src/main.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use std::io::{self, stdout, Write};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use iptvnator_rs::{setup, M3u8, Parser};
|
||||||
|
fn main() {
|
||||||
|
println!("Welcome to iptvnator_rs, the somewhat faster version and certainly harder to write iptvnator");
|
||||||
|
let p = Parser::new("iptv.m3u8".to_owned(), setup(), "watched.txt".to_owned());
|
||||||
|
|
||||||
|
let stdin = io::stdin();
|
||||||
|
let mut stdout = stdout().lock();
|
||||||
|
let mut search_result: Option<Rc<Vec<&M3u8>>> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
// Dont't perform a search if user has just watched, instead present the previous search
|
||||||
|
if search_result.is_none() {
|
||||||
|
print!("Search by name: ");
|
||||||
|
stdout.flush().unwrap();
|
||||||
|
stdin.read_line(&mut buf).unwrap();
|
||||||
|
buf = buf.trim().to_owned();
|
||||||
|
|
||||||
|
// If they want to quit, let them-
|
||||||
|
if buf.trim() == "q" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
search_result = Some(Rc::new(p.find(&buf)));
|
||||||
|
|
||||||
|
if search_result.as_ref().unwrap().len() == 0 {
|
||||||
|
println!("Nothing found");
|
||||||
|
stdout.flush().unwrap();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let them choose which one to stream
|
||||||
|
for (idx, m3u8_item) in search_result.as_ref().unwrap().iter().enumerate().rev() {
|
||||||
|
println!(" {}: {}", idx + 1, m3u8_item);
|
||||||
|
}
|
||||||
|
print!("Which one do you wish to stream? [q | s]: ");
|
||||||
|
stdout.flush().unwrap();
|
||||||
|
buf = String::new();
|
||||||
|
stdin.read_line(&mut buf).unwrap();
|
||||||
|
|
||||||
|
// If they want to quit, let them-
|
||||||
|
if buf.trim() == "q" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let choosen = buf.trim().parse::<usize>();
|
||||||
|
match choosen {
|
||||||
|
Ok(k) => {
|
||||||
|
let search_result = search_result.as_ref().unwrap().clone();
|
||||||
|
stream(&(search_result[k - 1]))
|
||||||
|
}
|
||||||
|
Err(e) => println!("Have to be a valid number! {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.save_watched();
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.output()
|
||||||
|
.expect("Could not listen for output");
|
||||||
|
}
|
170
src/parser.rs
Normal file
170
src/parser.rs
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::{fs, process};
|
||||||
|
|
||||||
|
use directories::ProjectDirs;
|
||||||
|
|
||||||
|
use crate::m3u8::M3u8;
|
||||||
|
|
||||||
|
const MAX_TRIES: usize = 4;
|
||||||
|
|
||||||
|
pub struct Parser {
|
||||||
|
watched_name: Rc<PathBuf>,
|
||||||
|
m3u8_items: Vec<M3u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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 iptv_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(&iptv_url, &file_name, &watched_name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_watched(&self) {
|
||||||
|
let project_dirs = ProjectDirs::from("com", "billenius", "iptvnator_rs").unwrap();
|
||||||
|
let cache_dir = project_dirs.cache_dir();
|
||||||
|
let watched_cache_file = cache_dir.join(&*self.watched_name);
|
||||||
|
|
||||||
|
let watched_items = self
|
||||||
|
.m3u8_items
|
||||||
|
.iter()
|
||||||
|
.filter(|item| item.watched)
|
||||||
|
.map(|item| item.link.clone())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
let _ = fs::create_dir_all(cache_dir);
|
||||||
|
|
||||||
|
match fs::write(watched_cache_file, watched_items.join("\n")) {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("Saved watched")
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to write downloaded m3u8file {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parsed_content(link: &String, file_name: &PathBuf, 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> = Self::get_stringcontent(link, file_name, 0)
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_stringcontent(link: &String, file_name: &PathBuf, tried: usize) -> String {
|
||||||
|
if !Self::should_update(file_name) {
|
||||||
|
let content = fs::read_to_string(&file_name);
|
||||||
|
if content.is_ok() {
|
||||||
|
return content.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = Self::download(link);
|
||||||
|
if content.is_err() && tried < 4 {
|
||||||
|
println!("Retrying {}/{}", tried + 1, MAX_TRIES);
|
||||||
|
Self::get_stringcontent(link, file_name, tried + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
match content {
|
||||||
|
Ok(s) => {
|
||||||
|
let _ = fs::write(&file_name, s.as_bytes());
|
||||||
|
s
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
println!("Couldn't get m3u8 file!");
|
||||||
|
process::exit(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download(link: &String) -> Result<String, reqwest::Error> {
|
||||||
|
reqwest::blocking::get(link.clone())
|
||||||
|
.and_then(|resp| Ok(resp.text().expect("Could not get m3u8 from server")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Parser {
|
||||||
|
type Target = Vec<M3u8>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.m3u8_items
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user