|
@@ -1,5 +1,6 @@
|
|
|
use crate::{
|
|
|
error::NtitledError,
|
|
|
+ ffmpeg::{SubtitleMeta, VideoMeta},
|
|
|
ost::{
|
|
|
data::{Episode, File, GuessIt, Movie, Subtitle, SubtitleAttrs, TvShow},
|
|
|
hash::compute_hash,
|
|
@@ -34,74 +35,9 @@ pub async fn get_directory(
|
|
|
|
|
|
let files = scan_dir_contents(path)?;
|
|
|
|
|
|
- let response = files.into_iter().fold(String::new(), |mut acc, el| {
|
|
|
- match el {
|
|
|
- FileType::Directory(Directory { name, path }) => {
|
|
|
- let Some((_, path)) = path.split_once(&state.base_path) else {
|
|
|
- return acc;
|
|
|
- };
|
|
|
-
|
|
|
- let path_enc = urlencoding::encode(&path[1..]);
|
|
|
- // Used by frontend
|
|
|
- let ty = "dir";
|
|
|
-
|
|
|
- write!(
|
|
|
- acc,
|
|
|
- r#"
|
|
|
- <a href="/dir/{path_enc}" class="file-entry {ty}">
|
|
|
- <h2>{name} 📁</h2>
|
|
|
- </a>
|
|
|
- "#
|
|
|
- )
|
|
|
- .unwrap();
|
|
|
- }
|
|
|
- FileType::File(crate::File {
|
|
|
- ref name,
|
|
|
- path,
|
|
|
- has_subs,
|
|
|
- }) => {
|
|
|
- let Some((_, path)) = path.split_once(&state.base_path) else {
|
|
|
- return acc;
|
|
|
- };
|
|
|
-
|
|
|
- let name = if let Some((name, _)) = name.rsplit_once('.') {
|
|
|
- name
|
|
|
- } else {
|
|
|
- name
|
|
|
- };
|
|
|
-
|
|
|
- // Used by frontend
|
|
|
- let ty = "file";
|
|
|
-
|
|
|
- let get = format!(
|
|
|
- "hx-get=/subtitles?name={}&path={}",
|
|
|
- urlencoding::encode(name),
|
|
|
- urlencoding::encode(&path[1..])
|
|
|
- );
|
|
|
-
|
|
|
- let id = gen_el_id();
|
|
|
- let guess_id = gen_el_id();
|
|
|
-
|
|
|
- let has_subs = if has_subs { " ✔" } else { "" };
|
|
|
-
|
|
|
- let template = state.env.get_template("file_container").unwrap();
|
|
|
- let template = template
|
|
|
- .render(context! {
|
|
|
- ty => ty,
|
|
|
- id => id,
|
|
|
- name => name,
|
|
|
- has_subs => has_subs,
|
|
|
- guess_id => guess_id,
|
|
|
- get => get
|
|
|
- })
|
|
|
- .unwrap();
|
|
|
-
|
|
|
- write!(acc, "{template}").unwrap();
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- acc
|
|
|
- });
|
|
|
+ let response = files
|
|
|
+ .into_iter()
|
|
|
+ .fold(String::new(), |acc, el| write_dir_html(&state, acc, el));
|
|
|
|
|
|
let template = state.env.get_template("index").unwrap();
|
|
|
let template = template.render(context! {divs => response}).unwrap();
|
|
@@ -131,7 +67,7 @@ pub async fn search_subtitles(
|
|
|
|
|
|
let search = state.client.search(&query.name, Some(&hash)).await?;
|
|
|
|
|
|
- let response = search.data.into_iter().fold(String::new(), |mut acc, el| {
|
|
|
+ let mut response = search.data.into_iter().fold(String::new(), |mut acc, el| {
|
|
|
|
|
|
let hashmatch = el.attributes.moviehash_match
|
|
|
.is_some_and(|is| is)
|
|
@@ -143,15 +79,19 @@ pub async fn search_subtitles(
|
|
|
let SubtitleResponse {
|
|
|
title,
|
|
|
original_title,
|
|
|
- imdb_id,
|
|
|
year,
|
|
|
download_count,
|
|
|
parent_title,
|
|
|
episode_number,
|
|
|
season_number,
|
|
|
- files
|
|
|
+ files,
|
|
|
+ language,
|
|
|
} = response;
|
|
|
|
|
|
+ if language.as_ref().is_some_and(|lang| lang != "en") {
|
|
|
+ return acc;
|
|
|
+ }
|
|
|
+
|
|
|
let original_title = original_title
|
|
|
.map(|title| format!("({title}) "))
|
|
|
.unwrap_or_default();
|
|
@@ -167,13 +107,14 @@ pub async fn search_subtitles(
|
|
|
let File { file_id, ..} = file;
|
|
|
let path = urlencoding::encode(&path);
|
|
|
write!(acc, r#"
|
|
|
- File ID: {file_id}
|
|
|
- <button hx-target="closest div" hx-swap="innerHTML" hx-get="/download?file_id={file_id}&full_path={path}">Download</button>"#).unwrap();
|
|
|
+ <button class="download-button" hx-target="closest div" hx-swap="innerHTML" hx-get="/download?file_id={file_id}&full_path={path}">Download</button>"#).unwrap();
|
|
|
acc
|
|
|
});
|
|
|
|
|
|
write!(files, "</div>").unwrap();
|
|
|
|
|
|
+ let language = language.unwrap_or("?".to_string());
|
|
|
+
|
|
|
write!(
|
|
|
acc,
|
|
|
r##"
|
|
@@ -184,7 +125,9 @@ pub async fn search_subtitles(
|
|
|
</div>
|
|
|
|
|
|
<div class="sub-container-info">
|
|
|
- <p>IMDB: {imdb_id}, Year: {year}, Downloads: {download_count}</p>
|
|
|
+ <p class="info">Language: {language}</p>
|
|
|
+ <p class="info">Downloads: {download_count}</p>
|
|
|
+ <p class="info">Year: {year}</p>
|
|
|
{files}
|
|
|
</div>
|
|
|
|
|
@@ -196,6 +139,10 @@ pub async fn search_subtitles(
|
|
|
acc
|
|
|
});
|
|
|
|
|
|
+ if response.is_empty() {
|
|
|
+ response = "No subtitles found :(".to_string();
|
|
|
+ }
|
|
|
+
|
|
|
Ok(response)
|
|
|
}
|
|
|
#[derive(Debug, Deserialize)]
|
|
@@ -220,7 +167,7 @@ pub async fn download_subtitles(
|
|
|
|
|
|
let path = ext
|
|
|
.map(|ext| query.full_path.replace(&format!(".{ext}"), ".srt"))
|
|
|
- .unwrap_or(format!("{}.srt", full_path));
|
|
|
+ .unwrap_or(format!("{full_path}.srt"));
|
|
|
|
|
|
state.client.download_subtitles(file_id, &path).await?;
|
|
|
Ok(String::from("Successfully downloaded subtitles"))
|
|
@@ -261,17 +208,150 @@ pub async fn guess_it(
|
|
|
Ok(response)
|
|
|
}
|
|
|
|
|
|
+/// Write contents of a directory as HTML
|
|
|
+fn write_dir_html(state: &State, mut acc: String, file: FileType) -> String {
|
|
|
+ match file {
|
|
|
+ FileType::Directory(Directory { name, path }) => {
|
|
|
+ let Some((_, path)) = path.split_once(&state.base_path) else {
|
|
|
+ return acc;
|
|
|
+ };
|
|
|
+
|
|
|
+ let path_enc = urlencoding::encode(&path[1..]);
|
|
|
+ // Used by frontend
|
|
|
+ let ty = "dir";
|
|
|
+
|
|
|
+ write!(
|
|
|
+ acc,
|
|
|
+ r#"
|
|
|
+ <a href="/dir/{path_enc}" class="file-entry {ty}">
|
|
|
+ <h2>{name} 📁</h2>
|
|
|
+ </a>
|
|
|
+ "#
|
|
|
+ )
|
|
|
+ .unwrap();
|
|
|
+ }
|
|
|
+ FileType::File(crate::File {
|
|
|
+ ref name,
|
|
|
+ path,
|
|
|
+ subs_file,
|
|
|
+ subs_embedded,
|
|
|
+ video_meta,
|
|
|
+ sub_meta,
|
|
|
+ }) => {
|
|
|
+ let Some((_, path)) = path.split_once(&state.base_path) else {
|
|
|
+ return acc;
|
|
|
+ };
|
|
|
+
|
|
|
+ let name = if let Some((name, _)) = name.rsplit_once('.') {
|
|
|
+ name
|
|
|
+ } else {
|
|
|
+ name
|
|
|
+ };
|
|
|
+
|
|
|
+ // Used by frontend
|
|
|
+ let ty = "file";
|
|
|
+
|
|
|
+ let get = format!(
|
|
|
+ "hx-get=/subtitles?name={}&path={}",
|
|
|
+ urlencoding::encode(name),
|
|
|
+ urlencoding::encode(&path[1..])
|
|
|
+ );
|
|
|
+
|
|
|
+ let id = gen_el_id();
|
|
|
+ let guess_id = gen_el_id();
|
|
|
+
|
|
|
+ // Checkmark
|
|
|
+ let has_subs = if subs_file || subs_embedded {
|
|
|
+ " ✔"
|
|
|
+ } else {
|
|
|
+ ""
|
|
|
+ };
|
|
|
+
|
|
|
+ let video_meta = video_meta.into_iter().fold(String::new(), |mut acc, el| {
|
|
|
+ let VideoMeta {
|
|
|
+ codec,
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ closed_captions,
|
|
|
+ avg_frame_rate,
|
|
|
+ ..
|
|
|
+ } = el;
|
|
|
+ write!(
|
|
|
+ acc,
|
|
|
+ r#"
|
|
|
+ <p>Codec: {codec}</p>
|
|
|
+ <p>W: {width} H: {height}</p>
|
|
|
+ <p>Framerate: {avg_frame_rate}</p>
|
|
|
+ <p>Captions: {closed_captions}</p>
|
|
|
+ "#
|
|
|
+ )
|
|
|
+ .unwrap();
|
|
|
+ acc
|
|
|
+ });
|
|
|
+
|
|
|
+ let sub_meta = sub_meta.map(|subs| {
|
|
|
+ subs.into_iter().fold(String::new(), |mut acc, el| {
|
|
|
+ let SubtitleMeta {
|
|
|
+ codec,
|
|
|
+ duration,
|
|
|
+ tags,
|
|
|
+ ..
|
|
|
+ } = el;
|
|
|
+ let language = if let Some(l) = tags.get("language") {
|
|
|
+ l.to_string()
|
|
|
+ } else {
|
|
|
+ "none".to_string()
|
|
|
+ };
|
|
|
+ let duration = if let Some(l) = duration {
|
|
|
+ l.to_string()
|
|
|
+ } else {
|
|
|
+ "none".to_string()
|
|
|
+ };
|
|
|
+ write!(
|
|
|
+ acc,
|
|
|
+ r#"
|
|
|
+ <p>Language: {language:?}</p>
|
|
|
+ <p>Codec: {codec}</p>
|
|
|
+ <p>Duration: {duration}</p>
|
|
|
+ <br>
|
|
|
+ "#
|
|
|
+ )
|
|
|
+ .unwrap();
|
|
|
+ acc
|
|
|
+ })
|
|
|
+ });
|
|
|
+ let template = state.env.get_template("file_container").unwrap();
|
|
|
+ let template = template
|
|
|
+ .render(context! {
|
|
|
+ ty => ty,
|
|
|
+ id => id,
|
|
|
+ name => name,
|
|
|
+ has_subs => has_subs,
|
|
|
+ guess_id => guess_id,
|
|
|
+ get => get,
|
|
|
+ video_meta => video_meta,
|
|
|
+ sub_meta => sub_meta
|
|
|
+ })
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ write!(acc, "{template}").unwrap();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ acc
|
|
|
+}
|
|
|
+
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
pub struct SubtitleResponse {
|
|
|
title: String,
|
|
|
original_title: Option<String>,
|
|
|
- imdb_id: usize,
|
|
|
year: usize,
|
|
|
download_count: usize,
|
|
|
parent_title: Option<String>,
|
|
|
episode_number: Option<usize>,
|
|
|
season_number: Option<usize>,
|
|
|
files: Vec<File>,
|
|
|
+ language: Option<String>,
|
|
|
}
|
|
|
|
|
|
impl From<Subtitle> for SubtitleResponse {
|
|
@@ -280,6 +360,7 @@ impl From<Subtitle> for SubtitleResponse {
|
|
|
feature_details,
|
|
|
download_count,
|
|
|
files,
|
|
|
+ language,
|
|
|
..
|
|
|
} = value.attributes;
|
|
|
|
|
@@ -289,27 +370,25 @@ impl From<Subtitle> for SubtitleResponse {
|
|
|
title,
|
|
|
original_title,
|
|
|
year,
|
|
|
- imdb_id,
|
|
|
..
|
|
|
} = item;
|
|
|
|
|
|
Self {
|
|
|
title,
|
|
|
original_title: Some(original_title),
|
|
|
- imdb_id,
|
|
|
year,
|
|
|
download_count,
|
|
|
parent_title: None,
|
|
|
episode_number: None,
|
|
|
season_number: None,
|
|
|
files,
|
|
|
+ language,
|
|
|
}
|
|
|
}
|
|
|
crate::ost::data::Feature::Episode(item) => {
|
|
|
let Episode {
|
|
|
year,
|
|
|
title,
|
|
|
- imdb_id,
|
|
|
parent_title,
|
|
|
episode_number,
|
|
|
season_number,
|
|
@@ -318,13 +397,13 @@ impl From<Subtitle> for SubtitleResponse {
|
|
|
Self {
|
|
|
title,
|
|
|
original_title: None,
|
|
|
- imdb_id,
|
|
|
year,
|
|
|
download_count,
|
|
|
parent_title,
|
|
|
episode_number,
|
|
|
season_number,
|
|
|
files,
|
|
|
+ language,
|
|
|
}
|
|
|
}
|
|
|
crate::ost::data::Feature::Movie(item) => {
|
|
@@ -332,7 +411,6 @@ impl From<Subtitle> for SubtitleResponse {
|
|
|
title,
|
|
|
original_title,
|
|
|
year,
|
|
|
- imdb_id,
|
|
|
parent_title,
|
|
|
episode_number,
|
|
|
season_number,
|
|
@@ -341,13 +419,13 @@ impl From<Subtitle> for SubtitleResponse {
|
|
|
Self {
|
|
|
title,
|
|
|
original_title: Some(original_title),
|
|
|
- imdb_id,
|
|
|
year,
|
|
|
download_count,
|
|
|
parent_title: Some(parent_title),
|
|
|
episode_number,
|
|
|
season_number,
|
|
|
files,
|
|
|
+ language,
|
|
|
}
|
|
|
}
|
|
|
}
|