|
@@ -1,19 +1,19 @@
|
|
use crate::{
|
|
use crate::{
|
|
error::NtitledError,
|
|
error::NtitledError,
|
|
- ffmpeg::{SubtitleStream, VideoStream},
|
|
|
|
|
|
+ htmx::{DirHtmx, FileHtmx, SubtitleResponseHtmx},
|
|
ost::{
|
|
ost::{
|
|
- data::{Episode, File, GuessIt, Movie, Subtitle, SubtitleAttrs, TvShow},
|
|
|
|
|
|
+ data::{GuessIt, SubtitleResponse},
|
|
hash::compute_hash,
|
|
hash::compute_hash,
|
|
},
|
|
},
|
|
- scan_dir_contents, Directory, FileType, State,
|
|
|
|
|
|
+ scan_dir_contents, FileType, State,
|
|
};
|
|
};
|
|
use axum::{
|
|
use axum::{
|
|
extract::Query,
|
|
extract::Query,
|
|
http::{header, StatusCode},
|
|
http::{header, StatusCode},
|
|
response::IntoResponse,
|
|
response::IntoResponse,
|
|
};
|
|
};
|
|
|
|
+use htmxpress::HtmxElement;
|
|
use minijinja::context;
|
|
use minijinja::context;
|
|
-use rand::{distributions, Rng};
|
|
|
|
use serde::Deserialize;
|
|
use serde::Deserialize;
|
|
use std::{ffi::OsStr, fmt::Write, path::Path};
|
|
use std::{ffi::OsStr, fmt::Write, path::Path};
|
|
use tracing::info;
|
|
use tracing::info;
|
|
@@ -37,7 +37,29 @@ pub async fn get_directory(
|
|
|
|
|
|
let response = files
|
|
let response = files
|
|
.into_iter()
|
|
.into_iter()
|
|
- .fold(String::new(), |acc, el| write_dir_html(&state, acc, el));
|
|
|
|
|
|
+ .enumerate()
|
|
|
|
+ .fold(String::new(), |mut acc, (i, entry)| {
|
|
|
|
+ match entry {
|
|
|
|
+ FileType::Directory(mut dir) => {
|
|
|
|
+ let Some((_, path)) = dir.path.split_once(&state.base_path) else {
|
|
|
|
+ return acc;
|
|
|
|
+ };
|
|
|
|
+ dir.path = path[1..].to_string();
|
|
|
|
+ let dir: DirHtmx = dir.into();
|
|
|
|
+ let _ = write!(acc, "{}", dir.to_htmx());
|
|
|
|
+ }
|
|
|
|
+ FileType::File(mut file) => {
|
|
|
|
+ let Some((_, path)) = file.path.split_once(&state.base_path) else {
|
|
|
|
+ return acc;
|
|
|
|
+ };
|
|
|
|
+ file.path = path[1..].to_string();
|
|
|
|
+ let file = FileHtmx::new(i, file);
|
|
|
|
+ let _ = write!(acc, "{}", file.to_htmx());
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ acc
|
|
|
|
+ });
|
|
|
|
|
|
let template = state.env.get_template("index").unwrap();
|
|
let template = state.env.get_template("index").unwrap();
|
|
let template = template.render(context! {divs => response}).unwrap();
|
|
let template = template.render(context! {divs => response}).unwrap();
|
|
@@ -62,80 +84,19 @@ pub async fn search_subtitles(
|
|
state: axum::extract::State<State>,
|
|
state: axum::extract::State<State>,
|
|
query: axum::extract::Query<SubtitleSearch>,
|
|
query: axum::extract::Query<SubtitleSearch>,
|
|
) -> Result<String, NtitledError> {
|
|
) -> Result<String, NtitledError> {
|
|
- let path = format!("{}/{}", state.base_path, query.path);
|
|
|
|
|
|
+ let path = format!("{}/{}", state.base_path, &query.path);
|
|
|
|
+
|
|
|
|
+ info!("Computing hash for {}", &path);
|
|
|
|
+
|
|
let hash = compute_hash(&path)?;
|
|
let hash = compute_hash(&path)?;
|
|
|
|
|
|
|
|
+ info!("Searching subtitles for {}", &query.name);
|
|
let search = state.client.search(&query.name, Some(&hash)).await?;
|
|
let search = state.client.search(&query.name, Some(&hash)).await?;
|
|
|
|
|
|
let mut 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)
|
|
|
|
- .then_some("🎯")
|
|
|
|
- .unwrap_or_default();
|
|
|
|
-
|
|
|
|
- let response = SubtitleResponse::from(el);
|
|
|
|
-
|
|
|
|
- let SubtitleResponse {
|
|
|
|
- title,
|
|
|
|
- original_title,
|
|
|
|
- year,
|
|
|
|
- download_count,
|
|
|
|
- parent_title,
|
|
|
|
- episode_number,
|
|
|
|
- season_number,
|
|
|
|
- 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();
|
|
|
|
-
|
|
|
|
- let parent_title = parent_title.map(|title| format!("{title} -")).unwrap_or_default();
|
|
|
|
-
|
|
|
|
- let episode_id = match (episode_number, season_number) {
|
|
|
|
- (Some(ep), Some(season)) => format!("S{season}E{ep}"),
|
|
|
|
- (_, _) => String::new(),
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- let mut files = files.into_iter().fold(String::from("<div class=\"sub-files\">"),|mut acc, file| {
|
|
|
|
- let File { file_id, ..} = file;
|
|
|
|
- let path = urlencoding::encode(&path);
|
|
|
|
- write!(acc, r#"
|
|
|
|
- <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##"
|
|
|
|
- <div class="sub-container">
|
|
|
|
-
|
|
|
|
- <div class="sub-container-title">
|
|
|
|
- <h3>{parent_title} {title} {original_title}{episode_id} {hashmatch}</h3>
|
|
|
|
- </div>
|
|
|
|
-
|
|
|
|
- <div class="sub-container-info">
|
|
|
|
- <p class="info">Language: {language}</p>
|
|
|
|
- <p class="info">Downloads: {download_count}</p>
|
|
|
|
- <p class="info">Year: {year}</p>
|
|
|
|
- {files}
|
|
|
|
- </div>
|
|
|
|
-
|
|
|
|
- </div>
|
|
|
|
- "##
|
|
|
|
- )
|
|
|
|
- .unwrap();
|
|
|
|
-
|
|
|
|
|
|
+ let el = SubtitleResponse::from(el);
|
|
|
|
+ let el = SubtitleResponseHtmx::new(path.clone(), el).to_htmx();
|
|
|
|
+ let _ = write!(acc, "{el}");
|
|
acc
|
|
acc
|
|
});
|
|
});
|
|
|
|
|
|
@@ -147,7 +108,7 @@ pub async fn search_subtitles(
|
|
}
|
|
}
|
|
#[derive(Debug, Deserialize)]
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DownloadQuery {
|
|
pub struct DownloadQuery {
|
|
- file_id: usize,
|
|
|
|
|
|
+ file_id: String,
|
|
full_path: String,
|
|
full_path: String,
|
|
}
|
|
}
|
|
|
|
|
|
@@ -155,9 +116,9 @@ pub async fn download_subtitles(
|
|
state: axum::extract::State<State>,
|
|
state: axum::extract::State<State>,
|
|
query: axum::extract::Query<DownloadQuery>,
|
|
query: axum::extract::Query<DownloadQuery>,
|
|
) -> Result<String, NtitledError> {
|
|
) -> Result<String, NtitledError> {
|
|
- let file_id = query.file_id;
|
|
|
|
-
|
|
|
|
|
|
+ let file_id = urlencoding::decode(&query.file_id)?.parse()?;
|
|
let full_path = urlencoding::decode(&query.full_path)?.to_string();
|
|
let full_path = urlencoding::decode(&query.full_path)?.to_string();
|
|
|
|
+
|
|
let file = full_path.split('/').last();
|
|
let file = full_path.split('/').last();
|
|
if let Some(file) = file {
|
|
if let Some(file) = file {
|
|
info!("Downloading subtitles for {file}");
|
|
info!("Downloading subtitles for {file}");
|
|
@@ -207,234 +168,3 @@ pub async fn guess_it(
|
|
|
|
|
|
Ok(response)
|
|
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 VideoStream {
|
|
|
|
- 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.into_iter().fold(String::new(), |mut acc, el| {
|
|
|
|
- let SubtitleStream {
|
|
|
|
- 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>,
|
|
|
|
- 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 {
|
|
|
|
- fn from(value: Subtitle) -> Self {
|
|
|
|
- let SubtitleAttrs {
|
|
|
|
- feature_details,
|
|
|
|
- download_count,
|
|
|
|
- files,
|
|
|
|
- language,
|
|
|
|
- ..
|
|
|
|
- } = value.attributes;
|
|
|
|
-
|
|
|
|
- match feature_details {
|
|
|
|
- crate::ost::data::Feature::TvShow(item) => {
|
|
|
|
- let TvShow {
|
|
|
|
- title,
|
|
|
|
- original_title,
|
|
|
|
- year,
|
|
|
|
- ..
|
|
|
|
- } = item;
|
|
|
|
-
|
|
|
|
- Self {
|
|
|
|
- title,
|
|
|
|
- original_title: Some(original_title),
|
|
|
|
- year,
|
|
|
|
- download_count,
|
|
|
|
- parent_title: None,
|
|
|
|
- episode_number: None,
|
|
|
|
- season_number: None,
|
|
|
|
- files,
|
|
|
|
- language,
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- crate::ost::data::Feature::Episode(item) => {
|
|
|
|
- let Episode {
|
|
|
|
- year,
|
|
|
|
- title,
|
|
|
|
- parent_title,
|
|
|
|
- episode_number,
|
|
|
|
- season_number,
|
|
|
|
- ..
|
|
|
|
- } = item;
|
|
|
|
- Self {
|
|
|
|
- title,
|
|
|
|
- original_title: None,
|
|
|
|
- year,
|
|
|
|
- download_count,
|
|
|
|
- parent_title,
|
|
|
|
- episode_number,
|
|
|
|
- season_number,
|
|
|
|
- files,
|
|
|
|
- language,
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- crate::ost::data::Feature::Movie(item) => {
|
|
|
|
- let Movie {
|
|
|
|
- title,
|
|
|
|
- original_title,
|
|
|
|
- year,
|
|
|
|
- parent_title,
|
|
|
|
- episode_number,
|
|
|
|
- season_number,
|
|
|
|
- ..
|
|
|
|
- } = item;
|
|
|
|
- Self {
|
|
|
|
- title,
|
|
|
|
- original_title: Some(original_title),
|
|
|
|
- year,
|
|
|
|
- download_count,
|
|
|
|
- parent_title: Some(parent_title),
|
|
|
|
- episode_number,
|
|
|
|
- season_number,
|
|
|
|
- files,
|
|
|
|
- language,
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-fn gen_el_id() -> String {
|
|
|
|
- rand::thread_rng()
|
|
|
|
- .sample_iter(distributions::Alphanumeric)
|
|
|
|
- .take(16)
|
|
|
|
- .map(char::from)
|
|
|
|
- .collect::<String>()
|
|
|
|
-}
|
|
|