61 Commits
v0.5 ... img

Author SHA1 Message Date
Zastian Pretorius
e7438cb664 added better anime history 2023-01-17 15:08:57 +00:00
Zastian Pretorius
6b58d42af7 dynamic image sizing 2023-01-16 22:16:24 +00:00
Zastian Pretorius
7fc96dce60 added state check to navigation for images 2023-01-16 21:18:22 +00:00
Zastian Pretorius
d716fa1d49 made image path dinamic 2023-01-11 01:00:17 +00:00
Zastian Pretorius
4d0112ed86 added basic image support 2023-01-11 00:50:19 +00:00
Zastian Pretorius
26a8cb5e77 thanks coolanx for the amazing regex 2023-01-10 16:22:37 +00:00
Zastian Pretorius
929cca2ace figed scraper regex 2023-01-10 16:12:30 +00:00
Zastian Pretorius
cc4f530764 fixed my shit regex 2023-01-07 22:25:54 +00:00
Zastian Pretorius
b5bac31d64 version bump 2023-01-07 03:09:01 +00:00
Zastian Pretorius
1092bc4f75 Merge branch 'main' of github.com:mrfluffy-dev/kami 2023-01-07 02:58:00 +00:00
Zastian Pretorius
f39deb5321 she be working again 2023-01-07 02:56:52 +00:00
mrfluffy
e22047b142 I am back baby 2023-01-06 21:21:15 +00:00
mrfluffy
ea91edf8bd Update README.org 2022-12-23 21:41:59 +00:00
Zastian Pretorius
e2b7f53da1 added Chrome cast 2022-11-22 20:20:49 +00:00
Zastian Pretorius
061d2dfb7c did an oopsie 2022-11-04 14:43:10 +00:00
Zastian Pretorius
3abedb7360 added new providers 2022-11-04 14:37:07 +00:00
Zastian Pretorius
1fc1b7e867 fixed anime provider 2022-11-03 22:58:03 +00:00
Zastian Pretorius
a5f0ef0e40 I did an oopsie 2022-10-31 20:22:18 +00:00
Zastian Pretorius
6d09eb7e6b re formating in ln 2022-10-30 01:07:37 +00:00
Zastian Pretorius
ca8c8c12e8 added redirecting to light novels 2022-10-30 01:07:20 +00:00
Zastian Pretorius
52dab31fe3 added redirecting to anime 2022-10-30 01:06:59 +00:00
Zastian Pretorius
ed0e3dd50f added anime tracking 2022-10-29 20:11:12 +01:00
Zastian Pretorius
abe79ba96f Merge branch 'main' of github.com:mrfluffy-dev/kami 2022-10-29 18:17:36 +01:00
Zastian Pretorius
8363c57f73 added light novel tracking 2022-10-29 18:16:09 +01:00
mrfluffy
857a5daf5a Merge pull request #9 from justchokingaround/patch-1
docs: windows copy to path
2022-10-27 14:50:31 +01:00
Zastian Pretorius
e9de97e4da fixed config dir problem 2022-10-26 22:15:05 +01:00
Zastian Pretorius
1e02417c15 added condision check so that watching older episodes don't update anilist 2022-10-20 17:30:07 +01:00
Zastian Pretorius
d436e69a64 fixed the unlikely ocurence of slow gogo uploads 2022-10-18 17:09:45 +01:00
Zastian Pretorius
115d7e790f optimised episode range detection 2022-10-17 22:55:49 +01:00
Zastian Pretorius
cc0945e29a made anilist progress update if there is only one episode 2022-10-11 12:12:35 +01:00
Zastian Pretorius
3ff0a47ff7 coolansx you are amazing(make stacking even better) 2022-10-11 12:06:32 +01:00
Zastian Pretorius
8ad12ebcca implemented way better anilist tracking 2022-10-11 02:01:53 +01:00
Zastian Pretorius
c63bb81f40 fuck off gogo 2022-10-06 14:06:17 +01:00
Zastian Pretorius
759a8e48b3 fixed yet anothe gogo url change 2022-09-26 15:51:36 +01:00
Zastian Pretorius
e025163448 fixed link again 2022-09-09 23:24:43 +01:00
Zastian Pretorius
ae10585507 fixed ln flickering 2022-09-03 12:39:50 +01:00
Zastian Pretorius
2e9874b486 fixed tui flikering 2022-09-03 11:20:47 +01:00
chokerman
67eed7353a docs: windows copy to path 2022-09-03 10:38:22 +02:00
Zastian Pretorius
2193956e8b fixed bug to see last episode of anime 2022-08-22 21:29:30 +01:00
Zastian Pretorius
ebfb9b8ea6 changed gogoanime form .lu to .ee 2022-08-19 14:03:02 +01:00
Zastian Pretorius
8afe0fad98 made kami be able to handel uncensored anime 2022-08-16 22:27:18 +01:00
Zastian Pretorius
f5b37da2d1 fixed ln ui braking in tiling window managers 2022-08-11 18:24:50 +01:00
Zastian Pretorius
a7ddb5f09d fixed tiling window managers braking in anime 2022-08-11 18:23:41 +01:00
Zastian Pretorius
2256fd4000 fixed even more movie related problems 2022-08-11 18:20:01 +01:00
Zastian Pretorius
20ab508dad swithed to i to search in ln 2022-08-11 17:38:27 +01:00
Zastian Pretorius
b67369d568 fixed anilist token 2022-08-11 17:32:54 +01:00
Zastian Pretorius
95a6da290c fixed movies 2022-08-11 17:23:53 +01:00
Zastian Pretorius
8aaeaa756d bug fix 2022-08-11 17:11:04 +01:00
mrfluffy
0d2ff78743 Merge pull request #8 from mrfluffy-dev/tui
Tui
2022-08-11 17:04:29 +01:00
Zastian Pretorius
e79cdb07f5 fixed bug where season to of anime caused a crash 2022-08-11 16:52:54 +01:00
Zastian Pretorius
570a8dfc4b added help info foe ln page selection 2022-08-11 16:34:45 +01:00
Zastian Pretorius
3361f9ad93 did some error handeling on anime 2022-08-11 16:15:20 +01:00
Zastian Pretorius
3fb4ad1e93 added ln tui and removed old interface 2022-08-11 16:15:00 +01:00
Zastian Pretorius
2b998923d2 compleate switch to tui for anime 2022-08-10 18:27:32 +01:00
Zastian Pretorius
180b1dfb32 basic tui implementation 2022-08-10 15:24:45 +01:00
Zastian Pretorius
c5fb7b056a added the print back to show yopur currenrt episode 2022-08-08 22:56:59 +01:00
Zastian Pretorius
333b638d06 added resume functon for anime 2022-08-08 22:50:48 +01:00
Zastian Pretorius
77415b22b4 changed index from 1 to 0 2022-08-08 22:50:19 +01:00
Zastian Pretorius
84aa95aa6e changed readme and updated version number 2022-08-08 18:11:31 +01:00
Zastian Pretorius
62de278e8d added anilist tracking 2022-08-08 14:49:30 +01:00
Zastian Pretorius
f57e3f2df6 You are welcome 2022-08-03 12:45:49 +01:00
15 changed files with 2370 additions and 435 deletions

919
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
name = "kami"
author = "mrfluffy-dev"
license = "GPL-3.0"
version = "0.5.0"
version = "0.6.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -14,3 +14,9 @@ isahc = "1.7.2"
base64 = "0.13"
termsize = "0.1.6"
dirs = "4.0"
serde_json = "1.0.83"
tui = "0.18.0"
crossterm = "0.24.0"
unicode-width = "0.1.9"
rust_cast = "0.17.0"
viuer = { version = "0.6", features = ["sixel"] }

View File

@@ -1,15 +1,19 @@
#+title: Readme
#+OPTIONS: toc:2
* Table of content
1. [[#Why-use-kami][Why use kami]]
2. [[#Dependencies][Dependencies]]
3. [[#Install][Install]]
1. [[#IMPORTANT][IMPORTANT]]
2. [[#Why-use-kami][Why use kami]]
3. [[#Dependencies][Dependencies]]
4. [[#Install][Install]]
- [[#LinuxMac][Linux/mac]]
- [[#Windows][Windows]]
4. [[#Honorable-mentions][Honorable mentions]]
5. [[#Honorable-mentions][Honorable mentions]]
* IMPORTANT
remove all contents of ~$HOME/.config/kami/an_progress.json~ new version is not compatibal with old progress file.
* Why use kami
well its a fast and easy way to watch anime and read light novels right in your terminal no need to open a browser.
Well its a fast and easy way to watch anime and read light novels right in your terminal no need to open a browser.
Also rust is fast as fuck boiiiii.
It can keep your anime tracking up to date with anilist.
* Dependencies
1. [[https://github.com/sharkdp/bat][bat]]
2. [[https://mpv.io/][mpv]]
@@ -76,11 +80,12 @@ git clone https://github.com/mrfluffy-dev/kami.git && cd kami
#+begin_src shell
cargo build --release
#+end_src
7. copy kami to path
7. copy kami to path (for this to work, you need to use git bash and you need to run git bash in administrator mode0
#+begin_src
cp target/release/kami.exe /usr/bin/kami
#+end_src
8. open kami by using ~kami~
* Honorable mentions
- [[https://github.com/pystardust/ani-cli][ani-cli]] just a bunch of fucking nice people.
- [[https://docs.rs/][rust docs]] honestly its just so useful.
- [[https://github.com/pystardust/ani-cli][ani-cli]] Just a bunch of fucking nice people.
- [[https://docs.rs/][rust docs]] Honestly its just so useful.
- [[https://github.com/DemonKingSwarn/flix-cli][flix-cli]] For forcing me to make a release.

View File

@@ -1,93 +1,465 @@
use crate::main;
use crate::open_video;
use crate::{anime_ep_range, anime_link, anime_names};
use crate::{int_input, string_input};
use colored::Colorize;
//use crate
pub fn anime_stream(search: String, episode: u32) {
let query = if search != "" {
search
} else {
string_input("Search anime: ")
};
use crate::{
get_an_history, get_an_progress, get_anime_id, get_user_anime_progress, update_anime_progress,
write_an_progress,
};
use crate::{get_anime_link, get_animes, get_image};
use crate::{open_cast, open_video};
let anime_list = anime_names(&query);
let mut count = 0;
print!("\x1B[2J\x1B[1;1H");
anime_list.iter().for_each(|anime| {
if count % 2 == 0 {
println!(
"({})\t{}",
format_args!("{}", count.to_string().blue()),
format_args!("{}", anime.blue())
);
} else {
println!(
"({})\t{}",
format_args!("{}", count.to_string().yellow()),
format_args!("{}", anime.yellow())
);
}
count += 1;
});
let mut anime_num: usize = usize::MAX;
while anime_num == usize::max_value() || anime_num > anime_list.len() {
anime_num = int_input("Enter anime number: ");
if anime_num > anime_list.len() {
println!("Invalid anime number");
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph},
Frame, Terminal,
};
use unicode_width::UnicodeWidthStr;
use viuer::{print_from_file, terminal_size, Config};
use super::scraper::get_anime_info;
enum InputMode {
Normal,
Editing,
}
struct StatefulList<T> {
state: ListState,
items: Vec<T>,
}
impl<T> StatefulList<T> {
fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items,
}
}
let title = &anime_list[anime_num];
let ep_range = anime_ep_range(title);
// if there is only one episode, then don't ask user to choose episode
if ep_range == 1 {
let link = anime_link(title, 1);
open_video(link);
main();
} else {
let mut ep_num: usize = usize::MAX;
if episode > ep_range.into() {
println!("Invalid episode number");
main();
} else if episode != 0 {
ep_num = episode as usize;
} else {
println!("select episode 1-{}: ", ep_range);
while ep_num == usize::max_value() || ep_num > ep_range as usize {
ep_num = int_input("Enter episode number: ");
if ep_num > ep_range as usize {
println!("Invalid episode number");
fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
fn unselect(&mut self) {
self.state.select(None);
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn iter(&self) -> impl Iterator<Item = &T> {
self.items.iter()
}
}
struct App {
/// Current value of the input box
input: String,
animes: (Vec<String>, Vec<String>, Vec<String>),
image: String,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
messages: StatefulList<String>,
title: String,
link: String,
ep: u64,
progress: i32,
anime_id: i32,
token: String,
provider: String,
cast: (bool, String),
}
impl<'a> App {
fn default() -> App {
App {
input: String::new(),
animes: get_an_history(),
image: String::new(),
input_mode: InputMode::Normal,
messages: StatefulList::with_items(Vec::new()),
title: String::new(),
link: String::new(),
ep: 0,
progress: 0,
anime_id: 0,
token: String::new(),
provider: String::new(),
cast: (false, "0".to_string()),
}
loop {
let link = anime_link(title, ep_num as u64);
open_video(link);
println!("{}", "n: next episode".green());
println!("{}", "p: previous episode".yellow());
println!("{}", "s: search another anime".green());
println!("{}", "q: quit".red());
let input = string_input("Enter command: ");
if input == "n" {
if ep_num == ep_range as usize {
println!("No more episodes");
} else {
ep_num += 1;
}
} else if input == "p" {
if ep_num == 1 {
println!("No previous episodes");
} else {
ep_num -= 1;
}
} else if input == "s" {
//remove all the arguments
anime_stream("".to_string(), 0);
} else if input == "q" {
std::process::exit(0);
} else {
println!("Invalid command");
}
}
pub fn anime_ui(
token: String,
provider: String,
cast: (bool, String),
) -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let mut app = App::default();
app.token = token;
app.provider = provider;
app.cast = cast;
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
let mut ep_select = false;
fn change_image(app: &App) {
//save as f32
let (width, height) = terminal_size().to_owned();
let width = width as f32;
let height = height as f32;
let sixel_support = viuer::is_sixel_supported();
let config = match sixel_support {
true => Config {
x: ((width / 2.0) + 1.0).round() as u16,
y: 2,
width: Some((width / 1.3).round() as u32),
height: Some((height * 1.5) as u32),
restore_cursor: true,
..Default::default()
},
false => Config {
x: ((width / 2.0) + 1.0).round() as u16,
y: 2,
width: Some(((width / 2.0) - 4.0).round() as u32),
height: Some((height / 1.3).round() as u32),
restore_cursor: true,
..Default::default()
},
};
let config_path = dirs::config_dir().unwrap().join("kami");
let image_path = config_path.join("tmp.jpg");
get_image(&app.image, &image_path.to_str().unwrap());
print_from_file(image_path, &config).expect("Image printing failed.");
}
app.messages.items.clear();
for anime in &app.animes.1 {
app.messages.push(anime.to_string());
}
app.input_mode = InputMode::Normal;
loop {
terminal.draw(|f| ui(f, &mut app))?;
if let Event::Key(key) = event::read()? {
match app.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('i') => {
app.input_mode = InputMode::Editing;
}
KeyCode::Char('q') => {
return Ok(());
}
KeyCode::Left => app.messages.unselect(),
KeyCode::Char('h') => app.messages.unselect(),
KeyCode::Down => match ep_select {
true => {
app.messages.next();
}
false => {
app.messages.next();
let selected = app.messages.state.selected();
app.image = app.animes.2[selected.unwrap()].clone();
change_image(&app);
}
},
KeyCode::Char('j') => match ep_select {
true => {
app.messages.next();
}
false => {
app.messages.next();
let selected = app.messages.state.selected();
app.image = app.animes.2[selected.unwrap()].clone();
change_image(&app);
}
},
KeyCode::Up => match ep_select {
true => {
app.messages.previous();
}
false => {
app.messages.previous();
let selected = app.messages.state.selected();
app.image = app.animes.2[selected.unwrap()].clone();
change_image(&app);
}
},
KeyCode::Char('k') => match ep_select {
true => {
app.messages.previous();
}
false => {
app.messages.previous();
let selected = app.messages.state.selected();
app.image = app.animes.2[selected.unwrap()].clone();
change_image(&app);
}
},
//if KeyCode::Enter => {
KeyCode::Enter => {
if ep_select == false {
app.progress = 0;
let selected = app.messages.state.selected();
app.title = app.messages.items[selected.unwrap()].clone();
app.link = app.animes.0[selected.unwrap()].clone();
let anime_info = get_anime_info(&app.animes.0[selected.unwrap()]);
app.anime_id = get_anime_id(anime_info.0);
app.messages.items.clear();
if app.token == "local" || app.anime_id == 0 {
app.progress = get_an_progress(&app.title) as i32;
app.messages.state.select(Some(app.progress as usize));
} else {
app.progress =
get_user_anime_progress(app.anime_id, app.token.as_str());
app.messages.state.select(Some(app.progress as usize));
}
if anime_info.1 == 1 {
let link = get_anime_link(&app.link, 1);
if !app.cast.0 {
open_video((link, format!("{} Episode 1", &app.title)));
} else {
open_cast(
(link, format!("{} Episode 1", &app.title)),
&app.cast.1,
)
}
let selected = app.messages.state.selected();
let image_url = app.animes.2[selected.unwrap()].clone();
if app.token == "local" || app.anime_id == 0 {
write_an_progress((&app.title, &app.link, &image_url), &1);
} else {
update_anime_progress(app.anime_id, 1, app.token.as_str());
write_an_progress((&app.title, &app.link, &image_url), &1);
}
} else {
for ep in 1..anime_info.1 + 1 {
app.messages.push(format!("Episode {}", ep));
}
ep_select = true;
}
} else {
let selected = app.messages.state.selected();
app.ep = app
.messages
.iter()
.nth(selected.unwrap())
.unwrap()
.replace("Episode ", "")
.parse::<u64>()
.unwrap();
let link = get_anime_link(&app.link, app.ep);
if !app.cast.0 {
open_video((link, format!("{} Episode {}", &app.title, app.ep)));
} else {
open_cast(
(link, format!("{} Episode {}", &app.title, app.ep)),
&app.cast.1,
)
}
let image_url = &app.image;
if app.ep > app.progress as u64 {
if app.token == "local" || app.anime_id == 0 {
write_an_progress((&app.title, &app.link, &image_url), &app.ep);
} else {
update_anime_progress(
app.anime_id,
app.ep as usize,
app.token.as_str(),
);
write_an_progress((&app.title, &app.link, &image_url), &app.ep);
}
app.progress = app.ep as i32;
}
}
}
_ => {}
},
InputMode::Editing => match key.code {
KeyCode::Enter => {
//push app.input into app.messages with '
app.animes = get_animes(app.input.drain(..).collect());
app.messages.items.clear();
for anime in &app.animes.1 {
app.messages.push(anime.to_string());
}
ep_select = false;
app.input_mode = InputMode::Normal;
}
KeyCode::Char(c) => {
app.input.push(c);
}
KeyCode::Backspace => {
app.input.pop();
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
_ => {}
},
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Min(1),
Constraint::Length(1),
Constraint::Length(3),
]
.as_ref(),
)
.split(f.size());
let block = Block::default()
.borders(Borders::ALL)
.title("kami")
.border_type(BorderType::Rounded);
f.render_widget(block, f.size());
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
let (msg, style) = match app.input_mode {
InputMode::Normal => (
vec![
Span::raw("Press "),
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit, "),
Span::styled("i", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to search."),
],
Style::default().add_modifier(Modifier::RAPID_BLINK),
),
InputMode::Editing => (
vec![
Span::raw("Press "),
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to stop editing, "),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to select."),
],
Style::default(),
),
};
let messages: Vec<ListItem> = app
.messages
.iter()
.enumerate()
.map(|(i, m)| {
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
ListItem::new(content)
})
.collect();
let messages = List::new(messages)
.block(
Block::default()
.borders(Borders::ALL)
.title("list")
.border_type(BorderType::Rounded),
)
.style(Style::default().fg(Color::White))
.highlight_style(
Style::default()
.bg(Color::Rgb(183, 142, 241))
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">>");
f.render_stateful_widget(messages, top_chunks[0], &mut app.messages.state);
let block = Block::default()
.borders(Borders::ALL)
.title("info")
.border_type(BorderType::Rounded);
f.render_widget(block, top_chunks[1]);
let mut text = Text::from(Spans::from(msg));
text.patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[1]);
let input = Paragraph::new(app.input.as_ref())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Rgb(183, 142, 241)),
})
.block(Block::default().borders(Borders::all()).title("Input"));
f.render_widget(input, chunks[2]);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
{}
InputMode::Editing => {
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
f.set_cursor(
// Put cursor past the end of the input text
chunks[2].x + app.input.width() as u16 + 1,
// Move one line down, from the border to the input line
chunks[2].y + 1,
)
}
}
}

View File

@@ -1,3 +1,4 @@
pub mod anime;
pub mod player;
pub mod scraper;
pub mod anime;
pub mod trackers;

View File

@@ -1,3 +1,13 @@
extern crate rust_cast;
use rust_cast::{
channels::{
media::{Media, StreamType},
receiver::CastDeviceApp,
},
CastDevice,
};
use std::str::FromStr;
pub fn open_video(link: (String, String)) {
let title = link.1;
let title = title.replace("-", " ");
@@ -9,5 +19,64 @@ pub fn open_video(link: (String, String)) {
.expect("failed to open mpv");
// clear terminal
print!("\x1b[2J\x1b[1;1H");
}
const DEFAULT_DESTINATION_ID: &str = "receiver-0";
fn play_media(
device: &CastDevice,
app_to_run: &CastDeviceApp,
media: String,
media_type: String,
media_stream_type: StreamType,
) {
let app = device.receiver.launch_app(app_to_run).unwrap();
device
.connection
.connect(app.transport_id.as_str())
.unwrap();
let _status = device
.media
.load(
app.transport_id.as_str(),
app.session_id.as_str(),
&Media {
content_id: media,
content_type: media_type,
stream_type: media_stream_type,
duration: None,
metadata: None,
},
)
.unwrap();
}
pub fn open_cast(link: (String, String), ip: &str) {
let cast_device = match CastDevice::connect_without_host_verification(ip, 8009) {
Ok(cast_device) => cast_device,
Err(err) => panic!("Could not establish connection with Cast Device: {:?}", err),
};
cast_device
.connection
.connect(DEFAULT_DESTINATION_ID.to_string())
.unwrap();
cast_device.heartbeat.ping().unwrap();
// Play media and keep connection.
let media_stream_type = match "none" {
value @ "buffered" | value @ "live" | value @ "none" => {
StreamType::from_str(value).unwrap()
}
_ => panic!("Unsupported stream type!"),
};
play_media(
&cast_device,
&CastDeviceApp::from_str("default").unwrap(),
link.0.to_string(),
"".to_string(),
media_stream_type,
);
}

View File

@@ -1,92 +1,96 @@
use base64::{decode, encode};
use isahc::config::Configurable;
use isahc::{ReadResponseExt, Request, RequestExt};
use regex::Regex;
use std::fs::File;
use std::io::prelude::*;
//use serde_json::json;
pub fn get_anime_html(url: &str) -> String {
let req = Request::builder()
.uri(url)
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header(
"user-agent",
"Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/100.0",
)
.body(())
.unwrap();
req.send().unwrap().text().unwrap()
}
pub fn get_ep_location(url: &str) -> String {
let request = Request::builder()
.method("HEAD")
.uri(url)
.header(
"user-agent",
"Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/100.0",
)
.body(())
.unwrap();
let response = request.send().unwrap();
let headers = response.headers();
let location = headers.get("location").unwrap();
location.to_str().unwrap().to_string()
pub fn get_post(id: &str) -> String {
let resp = Request::builder()
.method("POST")
.uri("https://yugen.to/api/embed/")
.header("x-requested-with", "XMLHttpRequest")
.body(id)
.unwrap()
.send()
.unwrap()
.text();
let resp: String = resp.as_ref().unwrap().to_string();
resp
}
pub fn anime_names(query: &str) -> Vec<String> {
let url = format!("https://gogoanime.lu//search.html?keyword={}", query);
//relpace all spaces with %20
let url = url.replace(' ', "%20");
let html = get_anime_html(&url);
let re = Regex::new(r#"(?m)/category/([^"]*)"#).unwrap();
let mut anime_list: Vec<String> = Vec::new();
pub fn get_animes(query: String) -> (Vec<String>, Vec<String>, Vec<String>) {
let query = query.replace(" ", "+");
let html = get_anime_html(&format!("https://yugen.to/search/?q={}", query));
let re = Regex::new(r#"href="(/anime[^"]*)""#).unwrap();
let mut animes_links = Vec::new();
for cap in re.captures_iter(&html) {
anime_list.push(cap.get(1).unwrap().as_str().trim().to_string());
animes_links.push(cap[1].to_string());
}
anime_list.dedup();
anime_list
let re = Regex::new(r#"/" title="([^"]*)""#).unwrap();
let mut animes_names = Vec::new();
for cap in re.captures_iter(&html) {
animes_names.push(cap[1].to_string());
}
let re = Regex::new(r#"data-src="([^"]*)"#).unwrap();
let mut animes_images = Vec::new();
for cap in re.captures_iter(&html) {
animes_images.push(cap[1].to_string());
}
(animes_links, animes_names, animes_images)
}
pub fn anime_ep_range(anime_name: &str) -> u16 {
let url = format!("https://gogoanime.lu/category/{}", anime_name);
let re = Regex::new(r#"(?m)\s<a href="\#" class="active" ep_start = (.*?)</a>"#).unwrap();
let episodes = re
.captures_iter(&get_anime_html(&url))
.next()
.unwrap()
.get(1)
.unwrap()
.as_str()
.trim()
.to_string();
episodes
.split('-')
.nth(1)
.unwrap_or("0")
.parse::<u16>()
.unwrap_or(0)
}
pub fn anime_link(title: &str, ep: u64) -> (String, String) {
let url = format!("https://animixplay.to/v1/{}", title);
pub fn get_anime_info(url: &str) -> (i32, u16) {
let url = format!("https://yugen.to{}watch", url);
let html = get_anime_html(&url);
let re = Regex::new(r#"(?m)\?id=([^&]+)"#).unwrap();
let id1 = re
.captures_iter(&html)
.nth(ep as usize - 1)
.unwrap()
.get(1)
.unwrap()
.as_str()
.trim()
.to_string();
let title = format!("{} Episode {}", title.replace('-', " "), ep);
let encoded_id1 = encode(&id1);
let anime_id = encode(format!("{}LTXs3GrU8we9O{}", id1, encoded_id1));
let html = format!("https://animixplay.to/api/live{}", anime_id);
let url = get_ep_location(&html);
let url = url.split('#').nth(1).unwrap();
let url = std::str::from_utf8(&decode(url).unwrap())
.unwrap()
.to_string();
(url, title)
//print html and exit
let re = Regex::new(r#""mal_id":(\d*)"#).unwrap();
let mal_id = re.captures(&html).unwrap()[1].parse().unwrap();
let re =
Regex::new(r#"Episodes</div><span class="description" style="font-size: \d*px;">(\d*)"#)
.unwrap();
let episodes = re.captures(&html).unwrap()[1].parse().unwrap();
(mal_id, episodes)
}
pub fn get_anime_link(url: &str, episode: u64) -> String {
let url = &format!(
"https://yugen.to/watch{}{}/",
url.replace("/anime", ""),
episode
);
let html = get_anime_html(url);
let re = Regex::new(r#"/e/([^/]*)"#).unwrap();
let capture = re.captures(&html).unwrap();
let id = &capture[1];
let id = format!("id={}%3D&ac=0", id);
let json = get_post(&id);
let re = Regex::new(r#"hls": \["(.*)","#).unwrap();
let capture = re.captures(&json).unwrap();
let link = &capture[1];
//return the link
link.to_string()
}
pub fn get_image(url: &str, path: &str) {
let url = url;
let mut response = isahc::get(url).unwrap();
let mut buffer = Vec::new();
response.copy_to(&mut buffer).unwrap();
let mut file = File::create(path).unwrap();
file.write_all(&buffer).unwrap();
}

282
src/anime/trackers.rs Normal file
View File

@@ -0,0 +1,282 @@
use crate::string_input;
use isahc::{ReadResponseExt, Request, RequestExt};
use serde_json::json;
use std::fs;
pub fn get_token() -> String {
//if not on windows create folder ~/.config/kami
let config_path = dirs::config_dir().unwrap().join("kami");
if !config_path.exists() {
fs::create_dir_all(&config_path).unwrap();
}
let token_path = config_path.join("token.txt");
if !token_path.exists() {
//create empty file
fs::File::create(&token_path).unwrap();
}
//read token from file
let token = fs::read_to_string(&token_path).unwrap();
if token.is_empty() {
//ask user if they want to add a token or track locally
let input = string_input(
"would you want to link anilist(sellecting no will track anime localy)? (y/n)",
);
if input == "y" {
println!("please go to the below link and copy and past the token below");
println!(
"https://anilist.co/api/v2/oauth/authorize?client_id=9121&response_type=token"
);
let token = string_input("token: ");
fs::write(&token_path, token).unwrap();
} else if input == "n" {
let token = "local";
fs::write(&token_path, token).unwrap();
} else {
println!("invalid input");
std::process::exit(1);
}
}
let token = fs::read_to_string(&token_path).unwrap();
token
}
pub fn get_anime_id(mal_id: i32) -> i32 {
const QUERY: &str = "
query ($id: Int, $search: Int) {
Media (id: $id, idMal: $search, type: ANIME) {
id
title {
native
romaji
english
}
}
}
";
let json = json!({
"query": QUERY,
"variables": {
"search": mal_id
}
});
let resp = Request::builder()
.method("POST")
.uri("https://graphql.anilist.co/")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(json.to_string())
.unwrap()
.send()
.unwrap()
.text();
let regex = regex::Regex::new(r#"id":(.*?),"#).unwrap();
let resp: String = resp.as_ref().unwrap().to_string();
//if error let id = 0
let id = match regex.captures(&resp) {
Some(captures) => captures[1].parse::<i32>().unwrap(),
None => 0,
};
// let id = regex
// .captures(&resp)
// .unwrap()
// .get(1)
// .unwrap()
// .as_str()
// .parse::<i32>()
// .unwrap();
id
}
//get the user id from the token
fn get_user_id(token: &str) -> i32 {
const QUERY: &str = "query {
Viewer {
id
}
}";
let json = json!({ "query": QUERY });
let resp = Request::builder()
.method("POST")
.uri("https://graphql.anilist.co/")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {}", token))
.body(json.to_string())
.unwrap()
.send()
.unwrap()
.text();
//println!("{}", resp);
let regex = regex::Regex::new(r#"id":(.*?)}"#).unwrap();
let resp: String = resp.as_ref().unwrap().to_string();
let id = regex
.captures(&resp)
.unwrap()
.get(1)
.unwrap()
.as_str()
.parse::<i32>()
.unwrap();
id
}
pub fn get_user_anime_progress(anime_id: i32, token: &str) -> i32 {
let user_id = get_user_id(&token);
const QUERY: &str = "query ($user_id: Int, $media_id: Int) {
MediaList (userId: $user_id, mediaId: $media_id, type: ANIME) {
progress
}
}";
let json = json!({
"query": QUERY,
"variables": {
"user_id": user_id,
"media_id": anime_id,
}
});
let resp = Request::builder()
.method("POST")
.uri("https://graphql.anilist.co/")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {}", token))
.body(json.to_string())
.unwrap()
.send()
.unwrap()
.text();
let regex = regex::Regex::new(r#"progress":(.*?)}"#).unwrap();
let resp: String = resp.as_ref().unwrap().to_string();
if resp.contains("errors") {
0
} else {
let progress = regex
.captures(&resp)
.unwrap()
.get(1)
.unwrap()
.as_str()
.parse::<i32>()
.unwrap();
progress
}
}
pub fn update_anime_progress(anime_id: i32, progress: usize, token: &str) {
const UPDATE: &str = "
mutation ($mediaId: Int, $status: MediaListStatus, $progress: Int) {
SaveMediaListEntry (mediaId: $mediaId, status: $status, progress: $progress) {
id
status
progress
}
}
";
let json = json!({
"query": UPDATE,
"variables": {
"mediaId": anime_id,
"status": "CURRENT",
"progress": progress
}
});
let _resp = Request::builder()
.method("POST")
.uri("https://graphql.anilist.co/")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {}", token))
.body(json.to_string())
.unwrap()
.send()
.unwrap()
.text();
}
// local tracking
pub fn get_an_json() -> serde_json::Value {
let config_path = dirs::config_dir().unwrap().join("kami");
if !config_path.exists() {
fs::create_dir_all(&config_path).unwrap();
}
let json_path = config_path.join("an_progress.json");
if !json_path.exists() {
fs::File::create(&json_path).unwrap();
}
let json = fs::read_to_string(&json_path).unwrap();
let json: serde_json::Value = serde_json::from_str(&json).unwrap_or(serde_json::Value::Null);
json
}
pub fn write_an_progress(anime: (&str, &str, &str), progress: &u64) {
let config_path = dirs::config_dir().unwrap().join("kami");
let json_path = config_path.join("an_progress.json");
let json = fs::read_to_string(&json_path).unwrap();
let mut json: serde_json::Value =
serde_json::from_str(&json).unwrap_or(serde_json::Value::Null);
let mut title_json = serde_json::Map::new();
title_json.insert(
"progress".to_string(),
serde_json::Value::from(progress.clone()),
);
title_json.insert("link".to_string(), serde_json::Value::from(anime.1));
title_json.insert("image".to_string(), serde_json::Value::from(anime.2));
title_json.insert(
"updated".to_string(),
serde_json::Value::from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
),
);
//insert title_json into json
if json[anime.0].is_null() {
json[anime.0] = serde_json::Value::from(title_json);
} else {
json[anime.0]["progress"] = serde_json::Value::from(progress.clone());
json[anime.0]["link"] = serde_json::Value::from(anime.1);
json[anime.0]["image"] = serde_json::Value::from(anime.2);
json[anime.0]["updated"] = serde_json::Value::from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
);
}
let json = serde_json::to_string_pretty(&json).unwrap();
fs::write(&json_path, json).unwrap();
}
pub fn get_an_history() -> (Vec<String>, Vec<String>, Vec<String>) {
//get the titles, links, and images from the json
let json = get_an_json();
let mut titles = vec![];
let mut links = vec![];
let mut images = vec![];
let mut last_updated = vec![];
//if the json is empty, return empty vectors
if json.is_null() {
return (titles, links, images);
}
for (key, value) in json.as_object().unwrap() {
titles.push(key.to_string());
links.push(value["link"].as_str().unwrap().to_string());
images.push(value["image"].as_str().unwrap().to_string());
println!("{}", value["updated"].as_u64().unwrap());
last_updated.push(value["updated"].as_u64().unwrap());
}
let mut indices: Vec<usize> = (0..last_updated.len()).collect();
indices.sort_by(|&a, &b| last_updated[b].cmp(&last_updated[a]));
titles = indices.iter().map(|&i| titles[i].clone()).collect();
links = indices.iter().map(|&i| links[i].clone()).collect();
images = indices.iter().map(|&i| images[i].clone()).collect();
(links, titles, images)
}
pub fn get_an_progress(title: &str) -> i32 {
let json = get_an_json();
let selected = json[title]["progress"].as_u64().unwrap_or(0);
selected as i32
}

View File

@@ -1,37 +1,388 @@
use crate::{chapter_selector, get_full_text, open_bat, search_ln};
use crate::ln::open_text::*;
use crate::ln::scraper::*;
use crate::ln::tracker::*;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::fs::File;
use std::io::Write;
pub fn ln_read(search: &str, chapter: u32) {
//convert search in to Option<&str>
let ln_url = search_ln(&search);
let chapter = chapter as f64;
let mut selected_page = 1;
if chapter != 0.0 {
selected_page = (chapter / 48.0).ceil() as u32;
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph},
Frame, Terminal,
};
use unicode_width::UnicodeWidthStr;
enum InputMode {
Normal,
Editing,
}
struct StatefulList<T> {
state: ListState,
items: Vec<T>,
}
impl<T> StatefulList<T> {
fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items,
}
}
loop {
//make empty tuple called chapter_url with (String, u32, u32)
let chapter_url = chapter_selector(&ln_url, selected_page);
selected_page = chapter_url.1;
let full_text = get_full_text(&chapter_url.0);
if cfg!(target_os = "windows") {
use dirs::home_dir;
let mut home = format!("{:?}", home_dir()).replace("\\\\", "/");
home.drain(0..6);
home.drain(home.len() - 2..home.len());
let mut file = File::create(format!("{}/AppData/Roaming/log_e", home))
.expect("Unable to create file");
file.write_all(full_text.as_bytes())
.expect("Unable to write to file");
file.sync_all().expect("Unable to sync file");
} else {
let mut file = File::create("/tmp/log_e").expect("Unable to create file");
file.write_all(full_text.as_bytes())
.expect("Unable to write to file");
file.sync_all().expect("Unable to sync file");
fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
//open temp.txt in cat for user to read
let _com = open_bat();
print!("\x1B[2J\x1B[1;1H");
self.state.select(Some(i));
}
fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
fn unselect(&mut self) {
self.state.select(None);
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn iter(&self) -> impl Iterator<Item = &T> {
self.items.iter()
}
}
struct App {
/// Current value of the input box
input: String,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
messages: StatefulList<String>,
ln_titles: Vec<String>,
ln_links: Vec<String>,
title: String,
ln_id: String,
ln_chapters: Vec<String>,
ln_chapters_links: Vec<String>,
last_page: String,
current_page: String,
current_page_number: u32,
}
impl<'a> App {
fn default() -> App {
App {
input: String::new(),
input_mode: InputMode::Normal,
messages: StatefulList::with_items(Vec::new()),
ln_titles: Vec::new(),
ln_links: Vec::new(),
title: String::new(),
ln_id: String::new(),
ln_chapters: Vec::new(),
ln_chapters_links: Vec::new(),
last_page: String::new(),
current_page: String::new(),
current_page_number: 0,
}
}
}
pub fn ln_ui(chapter: u32) -> Result<(), Box<dyn Error>> {
// setup terminal
let _ = get_ln_json();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let mut app = App::default();
let chapter = chapter as f64;
app.current_page_number = 1;
if chapter != 0.0 {
app.current_page_number = (chapter / 48.0).ceil() as u32;
}
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
let mut chapter_select = false;
loop {
terminal.draw(|f| ui(f, &mut app))?;
if let Event::Key(key) = event::read()? {
match app.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('i') => {
app.input_mode = InputMode::Editing;
}
KeyCode::Char('q') => {
terminal.clear()?;
return Ok(());
}
KeyCode::Left => app.messages.unselect(),
KeyCode::Char('h') => {
if app.current_page_number > 0 {
app.current_page_number -= 1;
}
app.current_page = get_ln_next_page(&app.ln_id, &app.current_page_number);
app.ln_chapters = get_ln_chapters(&app.current_page);
app.ln_chapters_links = get_ln_chapters_urls(&app.current_page);
app.messages.items.clear();
for chapter in app.ln_chapters.iter() {
app.messages.push(chapter.to_string());
}
}
KeyCode::Down => app.messages.next(),
KeyCode::Char('j') => app.messages.next(),
KeyCode::Up => app.messages.previous(),
KeyCode::Char('k') => app.messages.previous(),
KeyCode::Char('l') => {
if app.current_page_number < app.last_page.parse::<u32>().unwrap() {
app.current_page_number += 1;
}
app.current_page = get_ln_next_page(&app.ln_id, &app.current_page_number);
app.ln_chapters = get_ln_chapters(&app.current_page);
app.ln_chapters_links = get_ln_chapters_urls(&app.current_page);
app.messages.items.clear();
for chapter in app.ln_chapters.iter() {
app.messages.push(chapter.to_string());
}
}
//if KeyCode::Enter => {
KeyCode::Enter => {
if chapter_select == false {
let selected = app.messages.state.selected();
app.title = app
.messages
.iter()
.nth(selected.unwrap())
.unwrap()
.to_string();
if app.current_page_number == 1 {
let progress = get_ln_progress(&app.title);
app.current_page_number = progress.0;
app.messages.state.select(Some(progress.1));
}
let link = app.ln_links[selected.unwrap()].to_string();
let html = get_html(&link);
app.ln_id = get_ln_id(&html).to_string();
app.last_page = get_ln_last_page(&html);
app.current_page =
get_ln_next_page(&app.ln_id.to_string(), &app.current_page_number);
app.ln_chapters = get_ln_chapters(&app.current_page);
app.ln_chapters_links = get_ln_chapters_urls(&app.current_page);
app.messages.items.clear();
for chapter in app.ln_chapters.iter() {
app.messages.push(chapter.to_string());
}
chapter_select = true;
} else {
let selected = app.messages.state.selected();
let chapter_url = app.ln_chapters_links[selected.unwrap()].to_string();
let full_text = get_full_text(&chapter_url);
if cfg!(target_os = "windows") {
use dirs::home_dir;
let mut home = format!("{:?}", home_dir()).replace("\\\\", "/");
home.drain(0..6);
home.drain(home.len() - 2..home.len());
let mut file =
File::create(format!("{}/AppData/Roaming/log_e", home))
.expect("Unable to create file");
file.write_all(full_text.as_bytes())
.expect("Unable to write to file");
file.sync_all().expect("Unable to sync file");
} else {
let mut file =
File::create("/tmp/log_e").expect("Unable to create file");
file.write_all(full_text.as_bytes())
.expect("Unable to write to file");
file.sync_all().expect("Unable to sync file");
};
terminal.clear()?;
let _ = open_bat();
write_ln_progress(
&app.title,
&app.current_page_number,
&app.messages.state.selected().unwrap(),
);
terminal.clear()?;
}
}
_ => {}
},
InputMode::Editing => match key.code {
KeyCode::Enter => {
//push app.input into app.messages with '1
let search: String = app.input.drain(..).collect();
let search = search.replace(" ", "+");
let url = "https://readlightnovels.net/?s=".to_string();
let url = format!("{}{}", url, search.trim()).trim().to_string();
let html = get_html(&url);
let ln_list = get_ln_list(html.as_str());
app.ln_titles = get_ln_titles(&ln_list);
app.ln_links = get_ln_urls(&ln_list);
app.messages.items.clear();
//remove index 0 of app.ln_titles and app.ln_links
app.ln_titles.remove(0);
app.ln_links.remove(0);
for ln in &app.ln_titles {
app.messages.push(ln.to_string());
}
chapter_select = false;
app.input_mode = InputMode::Normal;
}
KeyCode::Char(c) => {
app.input.push(c);
}
KeyCode::Backspace => {
app.input.pop();
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
_ => {}
},
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Min(1),
Constraint::Length(1),
Constraint::Length(3),
]
.as_ref(),
)
.split(f.size());
let block = Block::default()
.borders(Borders::ALL)
.title("kami")
.border_type(BorderType::Rounded);
f.render_widget(block, f.size());
let (msg, style) = match app.input_mode {
InputMode::Normal => (
vec![
Span::raw("Press "),
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit, "),
Span::styled("i", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to search, "),
Span::styled("h", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to go to the previous page, "),
Span::styled("l", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to go to the next page."),
],
Style::default().add_modifier(Modifier::RAPID_BLINK),
),
InputMode::Editing => (
vec![
Span::raw("Press "),
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to stop editing, "),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to select."),
],
Style::default(),
),
};
let messages: Vec<ListItem> = app
.messages
.iter()
.enumerate()
.map(|(i, m)| {
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
ListItem::new(content)
})
.collect();
let messages = List::new(messages)
.block(Block::default().borders(Borders::ALL).title("list"))
.style(Style::default().fg(Color::White))
.highlight_style(
Style::default()
.bg(Color::Rgb(183, 142, 241))
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">>");
f.render_stateful_widget(messages, chunks[0], &mut app.messages.state);
let mut text = Text::from(Spans::from(msg));
text.patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[1]);
let input = Paragraph::new(app.input.as_ref())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Rgb(183, 142, 241)),
})
.block(Block::default().borders(Borders::all()).title("Input"));
f.render_widget(input, chunks[2]);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
{}
InputMode::Editing => {
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
f.set_cursor(
// Put cursor past the end of the input text
chunks[2].x + app.input.width() as u16 + 1,
// Move one line down, from the border to the input line
chunks[2].y + 1,
)
}
}
}

View File

@@ -1,68 +0,0 @@
use colored::Colorize;
use crate::{
helpers::take_input::string_input,
ln::{
self,
scraper::{get_ln_chapters_urls, get_ln_id, get_ln_last_page},
search::get_ln_chapters,
},
main, page_selector,
};
pub fn chapter_selector(ln_url: &str, mut selected_page: u32) -> (String, u32) {
loop {
let ln_html = ln::scraper::get_html(ln_url);
let ln_id = get_ln_id(&ln_html);
let ln_last_page = get_ln_last_page(&ln_html);
let ln_page_html = page_selector(&ln_id, selected_page);
let ln_chapters = get_ln_chapters(&ln_page_html);
let ln_chapters_urls = get_ln_chapters_urls(&ln_page_html);
let mut count = 0;
ln_chapters.into_iter().for_each(|chaprer| {
if count % 2 == 0 {
println!(
"({})\t{}",
count.to_string().blue(),
format_args!("{}", chaprer.blue())
);
} else {
println!(
"({})\t{}",
count.to_string().yellow(),
format_args!("{}", chaprer.yellow())
);
}
count += 1;
});
println!("{}\t{}", "n:".green(), "Go to next page".green());
println!("{}\t{}", "b:".yellow(), "Go to previous page".yellow());
println!("{}\t{}", "s:".green(), "Search another title".green());
println!("{}\t{}", "q:".red(), "quit".red());
let chapter_number = string_input("Which chapter do you want to read? ");
if chapter_number == "n" && selected_page < ln_last_page.parse::<u32>().unwrap() {
selected_page += 1;
print!("\x1B[2J\x1B[1;1H");
} else if chapter_number == "b" && selected_page > 1 {
selected_page -= 1;
print!("\x1B[2J\x1B[1;1H");
} else if chapter_number == "q" {
print!("\x1B[2J\x1B[1;1H");
std::process::exit(0);
} else if chapter_number == "s" {
main();
} else {
let chapter_number = chapter_number.trim().to_string();
let mut _chapter_number_int = 0;
if chapter_number.parse::<u32>().is_ok() {
_chapter_number_int = chapter_number.parse::<u32>().unwrap();
} else {
println!("{}", "Invalid option".red());
continue;
}
let chapter_url = &ln_chapters_urls[_chapter_number_int as usize];
let chapter_url = chapter_url.trim().to_string();
return (chapter_url, selected_page);
}
}
}

View File

@@ -1,5 +1,4 @@
pub mod menu;
pub mod ln;
pub mod open_text;
pub mod scraper;
pub mod search;
pub mod ln;
pub mod tracker;

View File

@@ -1,24 +1,26 @@
use isahc::config::Configurable;
use isahc::{ReadResponseExt, Request, RequestExt};
use regex::Regex;
use crate::helpers::fixing_text::remove_after_dash;
use crate::helpers::fixing_text::fix_html_encoding;
//gets the full html of the page
pub fn get_html(url: &str) -> String {
let req = Request::builder()
.uri(url)
.header(
"user-agent",
"Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/100.0",
)
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36")
.body(())
.unwrap();
req.send().unwrap().text().unwrap()
let mut res = req.send().unwrap();
let html = res.text().unwrap();
html
}
//using isahc::prelude::* make a php reqest to get the next page of the ln
pub fn get_ln_next_page(ln_id: &str, page: &str) -> String {
pub fn get_ln_next_page(ln_id: &str, page: &u32) -> String {
let url = "https://readlightnovels.net/wp-admin/admin-ajax.php".to_string();
let form = format!(
"action=tw_ajax&type=pagination&id={}.html&page={}",
@@ -28,14 +30,15 @@ pub fn get_ln_next_page(ln_id: &str, page: &str) -> String {
let req = Request::builder()
.method("POST")
.uri(url)
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header(
"user-agent",
"Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/100.0",
)
.body(form)
.unwrap();
req.send().unwrap().text().unwrap()
let resp = req.send().unwrap().text().unwrap();
resp
}
pub fn get_full_text(chapter_url: &str) -> String {
@@ -102,3 +105,47 @@ pub fn get_ln_text(chapter_url: &str) -> Vec<String> {
fix_html_encoding(&ln_text)
}
//gets the list of ln's from the html and returns it as a vector of the ln's name and href
pub fn get_ln_list(html: &str) -> Vec<String> {
let re = Regex::new(r#"(?m)^\s*(<a href="[^"]*" title="[^"]*")"#).unwrap();
let mut ln_list: Vec<String> = Vec::new();
for cap in re.captures_iter(html) {
ln_list.push(cap.get(1).unwrap().as_str().trim().to_string());
}
ln_list
}
//gets the titles of the ln's from the html and returns it as a vector of the ln's name
pub fn get_ln_titles(ln_list: &Vec<String>) -> Vec<String> {
let re = Regex::new(r#"(?m)^\s*<a href="[^"]*" title="([^"]*)""#).unwrap();
let mut ln_title: Vec<String> = Vec::new();
for ln in ln_list {
for cap in re.captures_iter(ln) {
ln_title.push(cap.get(1).unwrap().as_str().to_string());
}
}
ln_title
}
//gets the urls of the ln's from the html and returns it as a vector of the ln's href
pub fn get_ln_urls(ln_list: &Vec<String>) -> Vec<String> {
let re = Regex::new(r#"(?m)^\s*<a href="([^"]*)""#).unwrap();
let mut ln_url: Vec<String> = Vec::new();
for ln in ln_list {
for cap in re.captures_iter(ln) {
ln_url.push(cap.get(1).unwrap().as_str().to_string());
}
}
ln_url
}
//gets the chapter titles from the html and returns it as a vector of the chapter's name
pub fn get_ln_chapters(html: &str) -> Vec<String> {
let re = Regex::new(r#"title=(.*?)>"#).unwrap();
let mut ln_list: Vec<String> = Vec::new();
for cap in re.captures_iter(html) {
ln_list.push(cap.get(1).unwrap().as_str().trim().to_string());
}
ln_list = remove_after_dash(&ln_list);
ln_list
}

View File

@@ -1,97 +0,0 @@
use crate::helpers::{fixing_text::remove_after_dash, take_input::string_input};
use colored::Colorize;
use regex::Regex;
pub fn search_ln(search: &str) -> String {
let mut _is_n = false;
print!("\x1B[2J\x1B[1;1H");
while !_is_n {
//if search is None, take input from user
let search_path = if search == "" {
string_input("What ln do you want to read? ")
} else {
search.to_string()
};
let search_path = search_path.replace(' ', "+");
let url = "https://readlightnovels.net/?s=".to_string();
let url = format!("{}{}", url, search_path.trim()).trim().to_string();
let html = crate::ln::scraper::get_html(&url).trim().to_string();
let ln_list = get_ln_list(&html);
//remove first element of ln_list
let ln_list = ln_list
.iter()
.skip(1)
.map(|x| x.to_string())
.collect::<Vec<String>>();
let ln_titles = get_ln_titles(&ln_list);
let ln_urls = get_ln_urls(&ln_list);
let mut count = 0;
ln_titles.into_iter().for_each(|ln| {
if count % 2 == 0 {
println!("({})\t{}", count.to_string().blue(), format_args!("{}", ln.blue()));
} else {
println!("({})\t{}", count.to_string().yellow(), format_args!("{}", ln.yellow()));
}
count += 1;
});
println!("{}\t{}","s:".green(), "Search another title".green());
let ln_number = string_input("Enter an option: ");
if ln_number != "s" && ln_number.parse::<usize>().is_ok() {
let ln_number = ln_number.trim().to_string();
let ln_number = ln_number.parse::<usize>().unwrap();
let ln_url = &ln_urls[ln_number];
let ln_url = ln_url.trim().to_string();
_is_n = true;
print!("\x1B[2J\x1B[1;1H");
return ln_url;
} else {
print!("invalid input");
}
print!("\x1B[2J\x1B[1;1H");
}
"".to_string()
}
//gets the list of ln's from the html and returns it as a vector of the ln's name and href
fn get_ln_list(html: &str) -> Vec<String> {
let re = Regex::new(r#"(?m)^\s*(<a href="[^"]*" title="[^"]*")"#).unwrap();
let mut ln_list: Vec<String> = Vec::new();
for cap in re.captures_iter(html) {
ln_list.push(cap.get(1).unwrap().as_str().trim().to_string());
}
ln_list
}
//gets the titles of the ln's from the html and returns it as a vector of the ln's name
fn get_ln_titles(ln_list: &Vec<String>) -> Vec<String> {
let re = Regex::new(r#"(?m)^\s*<a href="[^"]*" title="([^"]*)""#).unwrap();
let mut ln_title: Vec<String> = Vec::new();
for ln in ln_list {
for cap in re.captures_iter(ln) {
ln_title.push(cap.get(1).unwrap().as_str().to_string());
}
}
ln_title
}
//gets the urls of the ln's from the html and returns it as a vector of the ln's href
fn get_ln_urls(ln_list: &Vec<String>) -> Vec<String> {
let re = Regex::new(r#"(?m)^\s*<a href="([^"]*)""#).unwrap();
let mut ln_url: Vec<String> = Vec::new();
for ln in ln_list {
for cap in re.captures_iter(ln) {
ln_url.push(cap.get(1).unwrap().as_str().to_string());
}
}
ln_url
}
//gets the chapter titles from the html and returns it as a vector of the chapter's name
pub fn get_ln_chapters(html: &str) -> Vec<String> {
let re = Regex::new(r#"title=(.*?)>"#).unwrap();
let mut ln_list: Vec<String> = Vec::new();
for cap in re.captures_iter(html) {
ln_list.push(cap.get(1).unwrap().as_str().trim().to_string());
}
ln_list = remove_after_dash(&ln_list);
ln_list
}

50
src/ln/tracker.rs Normal file
View File

@@ -0,0 +1,50 @@
use serde_json;
use std::fs;
//
//
pub fn get_ln_json() -> serde_json::Value {
let config_path = dirs::config_dir().unwrap().join("kami");
if !config_path.exists() {
fs::create_dir_all(&config_path).unwrap();
}
let json_path = config_path.join("ln_progress.json");
if !json_path.exists() {
fs::File::create(&json_path).unwrap();
}
let json = fs::read_to_string(&json_path).unwrap();
let json: serde_json::Value = serde_json::from_str(&json).unwrap_or(serde_json::Value::Null);
json
}
pub fn write_ln_progress(title: &str, current_page: &u32, selected: &usize) {
let config_path = dirs::config_dir().unwrap().join("kami");
let json_path = config_path.join("ln_progress.json");
let json = fs::read_to_string(&json_path).unwrap();
let mut json: serde_json::Value =
serde_json::from_str(&json).unwrap_or(serde_json::Value::Null);
let mut title_json = serde_json::Map::new();
title_json.insert(
"current_page".to_string(),
serde_json::Value::from(current_page.clone()),
);
title_json.insert(
"selected".to_string(),
serde_json::Value::from(selected.clone()),
);
//insert title_json into json
if json[title].is_null() {
json[title] = serde_json::Value::from(title_json);
} else {
json[title]["current_page"] = serde_json::Value::from(current_page.clone());
json[title]["selected"] = serde_json::Value::from(selected.clone());
}
let json = serde_json::to_string_pretty(&json).unwrap();
fs::write(&json_path, json).unwrap();
}
pub fn get_ln_progress(title: &str) -> (u32, usize) {
let json = get_ln_json();
let current_page = json[title]["current_page"].as_u64().unwrap_or(1) as u32;
let selected = json[title]["selected"].as_u64().unwrap_or(0) as usize;
(current_page, selected)
}

View File

@@ -2,68 +2,69 @@ mod anime;
mod helpers;
mod ln;
use anime::anime::anime_stream;
use anime::anime::anime_ui;
use colored::Colorize;
use ln::{scraper::get_ln_next_page, ln::ln_read};
use ln::search::search_ln;
//use ln::ui::ln_ui;
use ln::ln::ln_ui;
use crate::anime::{
player::open_video,
scraper::{anime_ep_range, anime_link, anime_names},
};
use crate::anime::{player::*, scraper::*, trackers::*};
use crate::get_token;
use crate::helpers::take_input::{int_input, string_input};
use crate::ln::{menu::chapter_selector, open_text::open_bat, scraper::get_full_text};
fn main() {
let mut help = false;
let mut anime = false;
let mut ln = false;
let mut chapter: u32 = 0;
let mut episode: u32 = 0;
//let search = option string
let mut search = String::new();
let mut count = 0;
let mut provider: String = "gogo".to_string();
let mut cast = (false, "0".to_string());
for arg in std::env::args() {
if arg == "--help" || arg == "-h" {
help = true;
}
if arg == "--anime" || arg == "-a" {
anime = true;
//look at the next argument and see if it is a search term
if let Some(arg) = std::env::args().nth(count + 1) {
if !arg.starts_with("-") {
search = arg;
}
}
}
if arg == "--provider" || arg == "-r" {
if let Some(arg) = std::env::args().nth(count + 1) {
//get the next argument and see if it is = to gogo of vrv
if arg == "vrv" {
provider = "vrv".to_string();
count += 1;
} else if arg == "gogo" {
provider = "gogo".to_string();
count += 1;
} else {
provider = "gogo".to_string();
}
} else {
provider = "vrv".to_string();
}
}
if arg == "--cast" || arg == "-C" {
if let Some(arg) = std::env::args().nth(count + 1) {
cast = (true, String::from(arg))
} else {
println!("{}", "please provide a ip address".red())
}
}
if arg == "--ln" || arg == "-l" {
ln = true;
//if let Some(arg) = std::env::args().nth(count + 1) and that arg does not start with a '-' set search to that arg
if let Some(arg) = std::env::args().nth(count + 1) {
if !arg.starts_with("-") {
search = arg;
}
}
}
if arg == "--chapter" || arg == "-c" {
if let Some(arg) = std::env::args().nth(count + 1) {
chapter = arg.parse::<u32>().unwrap();
}else{
} else {
chapter = 0;
}
}
if arg == "--episode" || arg == "-e" {
if let Some(arg) = std::env::args().nth(count + 1) {
episode = arg.parse::<u32>().unwrap();
}else{
episode = 0;
}
}
count += 1;
}
if help == true{
if help == true {
print_help();
}
if anime == false && ln == false {
@@ -71,50 +72,64 @@ fn main() {
println!("2: Light Novel");
let a = int_input("pick your poison: ");
match a{
1 => anime = true,
2 => ln = true,
_=>println!("invalid option. ")
};
match a {
1 => anime = true,
2 => ln = true,
_ => println!("invalid option. "),
};
}
if anime == true && ln == true {
println!("you can only use one of the arguments at a time");
std::process::exit(0);
}
if ln == true {
ln_read(&search, chapter);
//ln_read(&search, chapter);
_ = ln_ui(chapter);
} else if anime == true {
anime_stream(search, episode);
//anime_stream(search, episode, resume);
let token = get_token();
_ = anime_ui(token, provider, cast);
} else {
println!("Invalid argument");
}
}
fn page_selector(ln_id: &str, selected_page: u32) -> String {
get_ln_next_page(ln_id, &selected_page.to_string())
}
fn print_help(){
fn print_help() {
println!("anime:\t\t{}", format_args!("{}", "-a --anime".red()));
println!("{}", "after this^^^ argument you can enter a search term".green());
println!("{}", "for exaple kami -a \"one piece\"");
//print blank line
println!("");
println!("episode:\t{}", format_args!("{}", "-e --episode".red()));
println!("{}", "after this^^^ argument you can enter a chapter number".green());
println!("{}", "for exaple kami -c 200");
//print blank line
println!(
"cast:\t\t{}",
format_args!("{} {}", "-C --cast".red(), "<IP Adress>".green())
);
println!("");
println!("light novel:\t{}", format_args!("{}", "-l --ln".red()));
println!("{}", "after this^^^ argument you can enter a search term".green());
println!("{}", "for exaple kami -l \"one piece\"");
//print blank line
println!("");
println!("chapter:\t{}", format_args!("{}", "-c --chapter".red()));
println!("{}", "after this^^^ argument you can enter a chapter number".green());
println!(
"{}",
"after this^^^ argument you can enter a chapter number".green()
);
println!("{}", "for exaple kami -c 200");
//print blank line
println!("");
println!("provider:\t{}", format_args!("{}", "-r --provider".red()));
println!(
"{}",
"after this^^^ argument you can enter a provider".green()
);
println!(
"if no provider is entered it will default to {}",
"vrv".green()
);
println!(
"if the -r argument is not used it will default to {}",
"gogo".green()
);
println!("the providers are {} or {}", "gogo".green(), "vrv".green());
println!("");
println!("help:\t\t{}", format_args!("{}", "-h --help".red()));
//kill the program
std::process::exit(0);