Переглянути джерело

add option to download subs for whole directory

biblius 8 місяців тому
батько
коміт
8c1511d147
6 змінених файлів з 130 додано та 18 видалено
  1. 13 0
      assets/styles.css
  2. BIN
      dump.rdb
  3. 2 0
      src/error.rs
  4. 24 3
      src/htmx.rs
  5. 2 11
      src/main.rs
  6. 89 4
      src/routes.rs

+ 13 - 0
assets/styles.css

@@ -44,6 +44,7 @@ main {
   display: flex;
   flex-wrap: wrap;
   align-items: start;
+  position: relative;
 }
 
 .file-entry .file-header {
@@ -137,3 +138,15 @@ main {
 .dir:hover {
   cursor: pointer;
 }
+
+.dir-container {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  margin: 0.2rem;
+}
+
+.download-dir-button {
+  height: 2rem;
+  z-index: 1000;
+}


+ 2 - 0
src/error.rs

@@ -3,6 +3,7 @@ use std::{num::ParseIntError, string::FromUtf8Error};
 use axum::{http::StatusCode, response::IntoResponse};
 use redis::RedisError;
 use thiserror::Error;
+use tracing::error;
 
 #[derive(Debug, Error)]
 pub enum NtitledError {
@@ -30,6 +31,7 @@ pub enum NtitledError {
 
 impl IntoResponse for NtitledError {
     fn into_response(self) -> axum::response::Response {
+        error!("Error: {self}");
         match self {
             NtitledError::IO(e) => {
                 (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()

+ 24 - 3
src/htmx.rs

@@ -19,13 +19,34 @@ pub struct DirHtmx {
     name: String,
 }
 
-impl From<Directory> for DirHtmx {
-    fn from(value: Directory) -> Self {
-        let Directory { name, path } = value;
+impl DirHtmx {
+    pub fn new(dir: Directory) -> Self {
+        let Directory { name, path } = dir;
         Self { path, name }
     }
 }
 
+#[derive(Debug, Element)]
+pub struct DownloadButtonHtmx {
+    #[element("button")]
+    #[attrs(class = "download-dir-button")]
+    #[hx_get("/download/dir?full_path={}", download_path)]
+    #[hx("swap" = "innerHtml")]
+    #[urlencode]
+    download: &'static str,
+
+    download_path: String,
+}
+
+impl DownloadButtonHtmx {
+    pub fn new(download_path: String) -> Self {
+        Self {
+            download: "Download",
+            download_path,
+        }
+    }
+}
+
 #[derive(Debug, Element)]
 #[element("div")]
 #[attrs(class = "file-entry file")]

+ 2 - 11
src/main.rs

@@ -316,14 +316,7 @@ async fn main() {
         cache,
     };
 
-    let router = axum::Router::new()
-        .route("/", get(|| async { Redirect::permanent("/dir/root") }))
-        .route("/dir/*key", get(routes::get_directory))
-        .route("/subtitles", get(routes::search_subtitles))
-        .route("/download", get(routes::download_subtitles))
-        .with_state(state);
-
-    let router_static = Router::new().fallback(get_service(ServeDir::new("assets")));
+    let router = routes::router(state);
 
     let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}"))
         .await
@@ -331,9 +324,7 @@ async fn main() {
 
     info!("Starting server on 0.0.0.0:{port}");
 
-    axum::serve(listener, router.merge(router_static))
-        .await
-        .unwrap();
+    axum::serve(listener, router).await.unwrap();
 }
 
 #[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]

+ 89 - 4
src/routes.rs

@@ -1,19 +1,36 @@
 use crate::{
     cache::Cache,
     error::NtitledError,
-    htmx::{DirHtmx, FileHtmx, SubtitleResponseHtmx},
+    htmx::{DirHtmx, DownloadButtonHtmx, FileHtmx, SubtitleResponseHtmx},
     ost::{data::SubtitleResponse, hash::compute_hash},
     FileType, State,
 };
 use axum::{
     http::{header, StatusCode},
-    response::IntoResponse,
+    response::{IntoResponse, Redirect},
+    routing::{get, get_service},
+    Router,
 };
 use htmxpress::HtmxElement;
 use minijinja::context;
 use serde::Deserialize;
 use std::{ffi::OsStr, fmt::Write, path::Path};
-use tracing::info;
+use tower_http::services::ServeDir;
+use tracing::{debug, info};
+
+pub fn router(state: State) -> Router {
+    let router = axum::Router::new()
+        .route("/", get(|| async { Redirect::permanent("/dir/root") }))
+        .route("/dir/*key", get(get_directory))
+        .route("/subtitles", get(search_subtitles))
+        .route("/download/dir", get(download_subtitles_dir))
+        .route("/download", get(download_subtitles))
+        .with_state(state);
+
+    let router_static = Router::new().fallback(get_service(ServeDir::new("assets")));
+
+    router.merge(router_static)
+}
 
 pub async fn get_directory(
     mut state: axum::extract::State<State>,
@@ -38,12 +55,21 @@ pub async fn get_directory(
         .fold(String::new(), |mut acc, (i, entry)| {
             match entry {
                 FileType::Directory(mut dir) => {
+                    let _ = write!(acc, r#"<div class="dir-container">"#);
+
                     let Some((_, path)) = dir.path.split_once(&state.base_path) else {
                         return acc;
                     };
+                    let download_path = dir.path.clone();
                     dir.path = path[1..].to_string();
-                    let dir: DirHtmx = dir.into();
+
+                    let dl_button = DownloadButtonHtmx::new(download_path);
+                    let _ = write!(acc, "{}", dl_button.to_htmx());
+
+                    let dir = DirHtmx::new(dir);
                     let _ = write!(acc, "{}", dir.to_htmx());
+
+                    let _ = write!(acc, "</div>");
                 }
                 FileType::File(mut file) => {
                     let Some((_, path)) = file.path.split_once(&state.base_path) else {
@@ -103,6 +129,7 @@ pub async fn search_subtitles(
 
     Ok(response)
 }
+
 #[derive(Debug, Deserialize)]
 pub struct DownloadQuery {
     file_id: String,
@@ -141,3 +168,61 @@ pub async fn download_subtitles(
 
     Ok(String::from("Successfully downloaded subtitles"))
 }
+
+#[derive(Debug, Deserialize)]
+pub struct DownloadDirQuery {
+    full_path: String,
+}
+
+pub async fn download_subtitles_dir(
+    mut state: axum::extract::State<State>,
+    query: axum::extract::Query<DownloadDirQuery>,
+) -> Result<String, NtitledError> {
+    let files = state.scan_dir_contents(&query.full_path)?;
+    for file in files {
+        let FileType::File(mut file) = file else {
+            continue;
+        };
+
+        if file.has_subs {
+            debug!("Subs exist, skipping {}", file.name);
+            continue;
+        }
+
+        let path = &file.path;
+
+        info!("Computing hash for {}", path);
+
+        let hash = compute_hash(path)?;
+
+        info!("Searching subtitles for {}", file.name);
+        let search = state.client.search(&file.name, Some(&hash)).await?;
+
+        let mut srt_files = search.data.into_iter().map(SubtitleResponse::from);
+
+        let Some(srt) = srt_files.find(|file| file.hashmatch) else {
+            info!("No hash match found, skipping {}", file.name);
+            continue;
+        };
+
+        if srt.files.is_empty() {
+            info!("No files found from search, skipping {}", file.name);
+            continue;
+        }
+
+        let ext = Path::new(&file.path).extension().and_then(OsStr::to_str);
+        let srt_path = ext
+            .map(|ext| file.path.replace(&format!(".{ext}"), ".srt"))
+            .unwrap_or(format!("{}.srt", file.path));
+
+        state
+            .client
+            .download_subtitles(srt.files.first().unwrap().file_id, &srt_path)
+            .await?;
+
+        file.has_subs = true;
+        state.cache.set_file_meta(&file).unwrap();
+    }
+
+    Ok(String::from("Done :)"))
+}