biblius 10 месяцев назад
Родитель
Сommit
e153fc4f90
8 измененных файлов с 117 добавлено и 121 удалено
  1. 10 0
      Cargo.lock
  2. 1 1
      Cargo.toml
  3. 4 1
      assets/index.html
  4. 9 0
      assets/styles.css
  5. 31 60
      src/ffmpeg.rs
  6. 34 30
      src/main.rs
  7. 2 2
      src/ost/client.rs
  8. 26 27
      src/routes.rs

+ 10 - 0
Cargo.lock

@@ -1074,6 +1074,15 @@ dependencies = [
  "lazy_static",
 ]
 
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "slab"
 version = "0.4.9"
@@ -1207,6 +1216,7 @@ dependencies = [
  "mio",
  "num_cpus",
  "pin-project-lite",
+ "signal-hook-registry",
  "socket2",
  "tokio-macros",
  "windows-sys 0.48.0",

+ 1 - 1
Cargo.toml

@@ -16,7 +16,7 @@ reqwest = { version = "0.11.23", features = ["json"] }
 serde = { version = "1.0.194", features = ["derive"] }
 serde_json = "1.0.110"
 thiserror = "1.0.56"
-tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] }
+tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "process"] }
 tower-http = { version = "0.5.0", features = ["fs"] }
 tracing = "0.1.40"
 tracing-subscriber = "0.3.18"

+ 4 - 1
assets/index.html

@@ -4,7 +4,10 @@
 <link rel="stylesheet" href="/styles.css">
 
 <body>
-    <a id="header" href="/dir/root">Ntitled</a>
+    <header>
+        <a id="header" href="/dir/root">&#127968;</a>
+    </header>
+
     <main id="files">
         {{ divs }}
     </main>

+ 9 - 0
assets/styles.css

@@ -4,6 +4,15 @@ html * {
   background-color: black;
 }
 
+body header {
+  position: fixed;
+}
+
+header a {
+  font-size: 32px;
+  color: white;
+}
+
 a {
   font-family: Arial;
   text-decoration: none;

+ 31 - 60
src/ffmpeg.rs

@@ -1,84 +1,55 @@
 use std::{collections::HashMap, process::Stdio};
 
-use serde::{
-    de::{DeserializeOwned, Unexpected},
-    Deserialize, Deserializer, Serialize,
-};
+use serde::{de::Unexpected, Deserialize, Deserializer, Serialize};
+use tracing::warn;
 
 use crate::error::NtitledError;
 
 #[derive(Debug, Deserialize)]
-pub struct FFProbeOutput<T> {
-    pub streams: Option<Vec<T>>,
+pub struct FFProbeOutput {
+    pub streams: Option<Vec<Stream>>,
 }
 
-impl<T> FFProbeOutput<T>
-where
-    T: DeserializeOwned + FFProbeStream,
-{
+impl FFProbeOutput {
     pub fn probe(path: &str) -> Result<Self, NtitledError> {
-        let stream = T::id();
-
         let child = std::process::Command::new("ffprobe")
             .args([
                 "-loglevel",
                 "error",
-                "-select_streams",
-                stream,
                 "-show_entries",
                 "stream:stream_tags=language",
+                "-show_streams",
                 "-of",
                 "json",
                 path,
             ])
             .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
             .spawn()?
             .wait_with_output()?;
 
         let out = String::from_utf8(child.stdout)?;
+        let err = String::from_utf8(child.stderr)?;
+        let err = err.trim();
 
-        serde_json::from_str::<Self>(&out).map_err(NtitledError::from)
-    }
-}
-
-#[derive(Debug, Deserialize)]
-pub struct Codec {
-    #[serde(rename = "index")]
-    pub stream_idx: usize,
-
-    pub codec_type: String,
-}
+        if !err.is_empty() {
+            warn!("{err}")
+        }
 
-impl Codec {
-    pub fn probe(path: &str) -> Result<Option<Vec<Self>>, NtitledError> {
-        let child = std::process::Command::new("ffprobe")
-            .args([
-                "-loglevel",
-                "error",
-                "-show_entries",
-                "stream=index,codec_type",
-                "-of",
-                "json",
-                path,
-            ])
-            .stdout(Stdio::piped())
-            .spawn()?
-            .wait_with_output()?;
-
-        let out = String::from_utf8(child.stdout)?;
-
-        let out = serde_json::from_str::<FFProbeOutput<Self>>(&out)?;
-
-        Ok(out.streams)
+        serde_json::from_str::<Self>(&out).map_err(NtitledError::from)
     }
 }
 
-pub trait FFProbeStream {
-    fn id() -> &'static str;
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum Stream {
+    Video(VideoStream),
+    Audio(AudioStream),
+    Sub(SubtitleStream),
 }
 
 #[derive(Debug, Serialize, Deserialize)]
-pub struct VideoMeta {
+pub struct VideoStream {
     pub index: usize,
     #[serde(rename = "codec_name")]
     pub codec: String,
@@ -100,14 +71,20 @@ pub struct VideoMeta {
     pub tags: Option<HashMap<String, String>>,
 }
 
-impl FFProbeStream for VideoMeta {
-    fn id() -> &'static str {
-        "v"
-    }
+#[derive(Debug, Serialize, Deserialize)]
+pub struct AudioStream {
+    pub index: usize,
+
+    #[serde(rename = "codec_name")]
+    pub codec: String,
+    #[serde(rename = "codec_long_name")]
+    pub codec_long: String,
+
+    pub sample_rate: String,
 }
 
 #[derive(Debug, Serialize, Deserialize)]
-pub struct SubtitleMeta {
+pub struct SubtitleStream {
     pub index: usize,
     #[serde(rename = "codec_name")]
     pub codec: String,
@@ -123,12 +100,6 @@ pub struct SubtitleMeta {
     pub tags: HashMap<String, String>,
 }
 
-impl FFProbeStream for SubtitleMeta {
-    fn id() -> &'static str {
-        "s"
-    }
-}
-
 #[derive(Debug, Serialize, Deserialize)]
 pub struct Disposition {
     #[serde(deserialize_with = "bool_from_int")]

+ 34 - 30
src/main.rs

@@ -3,26 +3,26 @@ use axum::{
     routing::{get, get_service},
     Router,
 };
+use ffmpeg::VideoStream;
 use minijinja::Environment;
 use ost::client::OSClient;
 use serde::Serialize;
 use std::{
     fs::{self, DirEntry},
+    num::NonZeroUsize,
     path::Path,
     thread,
 };
 use tower_http::services::ServeDir;
 use tracing::{debug, info, Level};
 
-use crate::ffmpeg::{Codec, FFProbeOutput, SubtitleMeta, VideoMeta};
+use crate::ffmpeg::{FFProbeOutput, SubtitleStream};
 
 pub mod error;
 pub mod ffmpeg;
 pub mod ost;
 pub mod routes;
 
-const MAX_THREADS: usize = 4;
-
 #[derive(Debug, Clone)]
 pub struct State {
     client: OSClient,
@@ -40,6 +40,10 @@ lazy_static::lazy_static! {
         std::fs::read_to_string("assets/filecontainer.html").expect("missing template");
 }
 
+lazy_static::lazy_static! {
+    pub static ref MAX_THREADS: usize = std::thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()).into();
+}
+
 #[tokio::main]
 async fn main() {
     dotenv::dotenv().expect("could not load env");
@@ -48,9 +52,13 @@ async fn main() {
         .with_max_level(Level::DEBUG)
         .init();
 
+    info!("Starting ntitled");
+
+    info!("Max threads: {}", *MAX_THREADS);
+
     let client = OSClient::init().await;
 
-    info!("Successfully OST loaded client");
+    info!("Successfully loaded OST client");
 
     let base_path = std::env::var("BASE_PATH").expect("base path not configured");
     let port = std::env::var("PORT").unwrap_or(String::from("3001"));
@@ -108,9 +116,9 @@ pub fn scan_dir_contents(path: impl AsRef<Path>) -> std::io::Result<Vec<FileType
     let mut files = thread::scope(|scope| {
         let mut handles = vec![];
 
-        let mut acc = Vec::with_capacity(MAX_THREADS);
+        let mut acc = Vec::with_capacity(*MAX_THREADS);
 
-        for _ in 0..MAX_THREADS {
+        for _ in 0..*MAX_THREADS {
             acc.push(vec![]);
         }
 
@@ -118,18 +126,16 @@ pub fn scan_dir_contents(path: impl AsRef<Path>) -> std::io::Result<Vec<FileType
             .into_iter()
             .enumerate()
             .fold(acc, |mut acc, (i, el)| {
-                acc[i % MAX_THREADS].push(el);
+                acc[i % *MAX_THREADS].push(el);
                 acc
             });
 
-        let subs = &subs;
-
         for entries in jobs {
             let task = scope.spawn(|| {
                 let mut files = vec![];
 
                 for entry in entries {
-                    if let Some(file) = extract_entry(subs, &entry) {
+                    if let Some(file) = extract_entry(&subs, &entry) {
                         files.push(file)
                     }
                 }
@@ -179,38 +185,36 @@ fn extract_entry(subs: &[String], entry: &DirEntry) -> Option<FileType> {
 
         let path_str = path.to_str()?;
 
-        let codecs = Codec::probe(path_str).ok()?;
-
-        if codecs.is_none() || codecs.is_some_and(|codec| codec.is_empty()) {
-            return None;
-        }
-
-        let video_meta = match FFProbeOutput::<VideoMeta>::probe(path_str) {
-            Ok(video_meta) => video_meta,
+        let out = match FFProbeOutput::probe(path_str) {
+            Ok(streams) => streams,
             Err(e) => {
                 debug!("File {path_str} is not a video: {e}");
                 return None;
             }
         };
 
-        let Some(video_meta) = video_meta.streams else {
+        let Some(streams) = out.streams else {
             return None;
         };
 
-        let sub_meta = match FFProbeOutput::<SubtitleMeta>::probe(path_str) {
-            Ok(meta) => meta.streams,
-            Err(e) => {
-                debug!("File {path_str} has no subs: {e}");
-                None
-            }
-        };
-
         let ext = path.extension()?.to_str()?;
 
         let (name, _) = path.file_name()?.to_str()?.split_once(ext)?;
 
         let subs_file = subs.contains(&name.to_string());
-        let subs_embedded = sub_meta.as_ref().is_some_and(|s| !s.is_empty());
+
+        let mut video_meta = vec![];
+        let mut sub_meta = vec![];
+
+        for stream in streams {
+            match stream {
+                ffmpeg::Stream::Video(s) => video_meta.push(s),
+                ffmpeg::Stream::Sub(s) => sub_meta.push(s),
+                ffmpeg::Stream::Audio(s) => {}
+            }
+        }
+
+        let subs_embedded = !sub_meta.is_empty();
 
         debug!("File: {name}; Subs: {}", subs_embedded || subs_file);
 
@@ -236,9 +240,9 @@ pub struct File {
     name: String,
     path: String,
 
-    video_meta: Vec<VideoMeta>,
+    video_meta: Vec<VideoStream>,
 
-    sub_meta: Option<Vec<SubtitleMeta>>,
+    sub_meta: Vec<SubtitleStream>,
 
     /// `true` if a same file exists with a `.srt` extension found
     /// in the same directory

+ 2 - 2
src/ost/client.rs

@@ -24,12 +24,12 @@ impl OSClient {
         let token = std::env::var("JWT").ok();
 
         if let Some(token) = token {
-            println!("Intialising client with token");
+            info!("Initialising client with token");
             OSClient::new(api_key, token)
                 .await
                 .expect("error while loading client")
         } else {
-            println!("Intialising client with login");
+            info!("Initialising client with login");
             OSClient::new_with_login(api_key, username, password)
                 .await
                 .expect("error while loading client")

+ 26 - 27
src/routes.rs

@@ -1,6 +1,6 @@
 use crate::{
     error::NtitledError,
-    ffmpeg::{SubtitleMeta, VideoMeta},
+    ffmpeg::{SubtitleStream, VideoStream},
     ost::{
         data::{Episode, File, GuessIt, Movie, Subtitle, SubtitleAttrs, TvShow},
         hash::compute_hash,
@@ -268,7 +268,7 @@ fn write_dir_html(state: &State, mut acc: String, file: FileType) -> String {
             };
 
             let video_meta = video_meta.into_iter().fold(String::new(), |mut acc, el| {
-                let VideoMeta {
+                let VideoStream {
                     codec,
                     width,
                     height,
@@ -289,37 +289,36 @@ fn write_dir_html(state: &State, mut acc: String, file: FileType) -> String {
                 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#"
+            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
-                })
+                )
+                .unwrap();
+                acc
             });
+
             let template = state.env.get_template("file_container").unwrap();
             let template = template
                 .render(context! {