diff --git a/Cargo.toml b/Cargo.toml index c544173..3afd499 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,9 +43,7 @@ sha2 = "0.10.8" rand = "0.8.5" fernet = "0.2.1" minijinja = { version = "1.0.12", features = ["loader"] } -pyo3 = { version = "0.20.2", features = ["auto-initialize"] } url = "2.5.0" -itertools = "0.12.1" openssl = "0.10" regex = "1.5" walkdir = "2.3.2" diff --git a/src/python/fileio.py b/src/python/fileio.py deleted file mode 100644 index e8fd678..0000000 --- a/src/python/fileio.py +++ /dev/null @@ -1,88 +0,0 @@ -import json -import os -import pathlib -import re -from typing import Dict, List, Union - - -def natural_sort_key(filename: str) -> List[Union[int, str]]: - parts = re.split(r'(\d+)', filename) - return [int(part) if part.isdigit() else part.lower() for part in parts] - - -def get_dir_stream_content(parent: str, subdir: str, file_formats: List[str]) -> List[Dict[str, str]]: - files = [] - for file_ in os.listdir(parent): - if file_.startswith('_') or file_.startswith('.'): - continue - if pathlib.PurePath(file_).suffix in file_formats: - files.append({"name": file_, "path": os.path.join(subdir, file_)}) - return json.dumps({"files": sorted(files, key=lambda x: natural_sort_key(x['name']))}) - - -def get_all_stream_content(video_source: str, file_formats: List[str]) -> Dict[str, List[Dict[str, str]]]: - structure = {'files': [], 'directories': []} - for __path, __directory, __file in os.walk(video_source): - if __path.endswith('__'): - continue - for file_ in __file: - if file_.startswith('_') or file_.startswith('.'): - continue - if pathlib.PurePath(file_).suffix in file_formats: - if path := __path.replace(video_source, "").lstrip(os.path.sep): - entry = {"name": path, "path": os.path.join("stream", path)} - if entry in structure['directories']: - continue - structure['directories'].append(entry) - else: - structure['files'].append({"name": file_, "path": os.path.join("stream", file_)}) - return json.dumps(dict(files=sorted(structure['files'], key=lambda x: natural_sort_key(x['name'])), - directories=sorted(structure['directories'], key=lambda x: natural_sort_key(x['name'])))) - - -def get_iter(filepath: str, file_formats: List[str]) -> Union[List[str], List[None]]: - filepath = pathlib.PosixPath(filepath) - # Extract only the file formats that are supported - dir_content = sorted( - (file for file in os.listdir(filepath.parent) if pathlib.PosixPath(file).suffix in file_formats), - key=lambda x: natural_sort_key(x) - ) - idx = dir_content.index(filepath.name) - if idx > 0: - try: - previous_ = dir_content[idx - 1] - if previous_ == filepath.name: - previous_ = None - except IndexError: - previous_ = None - else: - previous_ = None - try: - next_ = dir_content[idx + 1] - except IndexError: - next_ = None - return json.dumps({"previous": previous_, "next": next_}) - - -def srt_to_vtt(filename: str) -> str: - if not filename.endswith('.srt'): - return json.dumps(False) - filename = pathlib.PosixPath(filename) - output_file = filename.with_suffix('.vtt') - with open(filename, 'r', encoding='utf-8') as rf: - srt_content = rf.read() - srt_content = srt_content.replace(',', '.') - srt_content = srt_content.replace(' --> ', '-->') - vtt_content = 'WEBVTT\n\n' - subtitle_blocks = srt_content.strip().split('\n\n') - for block in subtitle_blocks: - lines = block.split('\n') - timecode = lines[1] - text = '\n'.join(lines[2:]) - vtt_content += f"{timecode}\n{text}\n\n" - with open(output_file, 'w', encoding='utf-8') as wf: - wf.write(vtt_content) - wf.flush() - if output_file.exists(): - return json.dumps(True) - return json.dumps(False) diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 91ddc99..f5a1bc6 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,11 +1,9 @@ use std::sync::{Arc, Mutex}; -use std::time::Instant; use actix_web::{HttpRequest, HttpResponse, web}; use actix_web::cookie::Cookie; use actix_web::cookie::time::{Duration, OffsetDateTime}; use actix_web::http::StatusCode; -use itertools::Itertools; use minijinja; use serde::Serialize; @@ -129,27 +127,7 @@ pub async fn home(config: web::Data>, squire::logger::log_connection(&request); log::debug!("{}", auth_response.detail); - let start_rust = Instant::now(); let listing_page = squire::content::get_all_stream_content(&config); - let rust_time_taken = start_rust.elapsed(); - - let start_python = Instant::now(); - let default_values = squire::settings::default_file_formats(); - // https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.collect_tuple - let _file_format = config.file_formats.iter().collect_tuple(); - let file_format = if _file_format.is_none() { - log::debug!("CRITICAL::Failed to extract tuple from {:?}", config.file_formats); - default_values.iter().collect_tuple() - } else { - _file_format - }; - let args = (config.video_source.to_string_lossy().to_string(), file_format.unwrap()); - let _listing_page = squire::fileio::get_all_stream_content(args); - let python_time_taken = start_python.elapsed(); - - println!("home_page [py]: {} seconds", python_time_taken.as_secs_f64()); - println!("home_page [rs]: {} seconds", rust_time_taken.as_secs_f64()); - let template = environment.lock().unwrap(); let listing = template.get_template("listing").unwrap(); diff --git a/src/routes/video.rs b/src/routes/video.rs index aa834fd..2efb9d6 100644 --- a/src/routes/video.rs +++ b/src/routes/video.rs @@ -1,10 +1,8 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; -use std::time::Instant; use actix_web::{HttpRequest, HttpResponse, web}; use actix_web::http::StatusCode; -use itertools::Itertools; use minijinja::{context, Environment}; use serde::Deserialize; use url::form_urlencoded; @@ -34,6 +32,9 @@ struct Subtitles { /// /// * `path` - The input path string to be URL encoded. /// +/// # References: +/// - [RustJobs](https://rustjobs.dev/blog/how-to-url-encode-strings-in-rust/) +/// /// # Returns /// /// Returns a URL encoded string. @@ -133,34 +134,7 @@ pub async fn stream(config: web::Data>, let template = environment.lock().unwrap(); if __target.is_file() { let landing = template.get_template("landing").unwrap(); - let start_rust = Instant::now(); let rust_iter = squire::content::get_iter(&__target, &config.file_formats); - let rust_time_taken = start_rust.elapsed(); - - let start_python = Instant::now(); - let default_values = squire::settings::default_file_formats(); - // https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.collect_tuple - let _file_format = config.file_formats.iter().collect_tuple(); - let file_format = if _file_format.is_none() { - log::debug!("CRITICAL::Failed to extract tuple from {:?}", config.file_formats); - default_values.iter().collect_tuple() - } else { - _file_format - }; - // full path required to read directory - let args = (&__target_str, file_format.unwrap()); - let iter = squire::fileio::get_iter(args); - let python_time_taken = start_python.elapsed(); - - if rust_iter.previous == iter.previous && rust_iter.next == iter.next { - println!("iter [py]: {} seconds", python_time_taken.as_secs_f64()); - println!("iter [rs]: {} seconds", rust_time_taken.as_secs_f64()); - } else { - println!("iter [rs]::{:?}", &rust_iter); - println!("iter [py]::{:?}", &iter); - } - - // https://rustjobs.dev/blog/how-to-url-encode-strings-in-rust/ let render_path = format!("/video?file={}", url_encode(&filepath)); // Rust doesn't allow re-assignment, so might as well create a mutable variable // Load the default response body and re-construct with subtitles if present @@ -183,9 +157,9 @@ pub async fn stream(config: web::Data>, track => sfx_file )).unwrap(); } else if subtitle.srt.exists() { - log::info!("Converting '{:?}' to '{:?}' for subtitles", - subtitle.srt.file_name(), - subtitle.vtt.file_name()); + log::info!("Converting {:?} to {:?} for subtitles", + subtitle.srt.file_name().unwrap(), + subtitle.vtt.file_name().unwrap()); match squire::subtitles::srt_to_vtt(&subtitle.srt) { Ok(_) => { log::debug!("Successfully converted srt to vtt file"); @@ -206,27 +180,7 @@ pub async fn stream(config: web::Data>, .content_type("text/html; charset=utf-8").body(response_body); } else if __target.is_dir() { let child_dir = __target.iter().last().unwrap().to_string_lossy().to_string(); - let start_rust = Instant::now(); let listing_page = squire::content::get_dir_stream_content(&__target_str, &child_dir, &config.file_formats); - let rust_time_taken = start_rust.elapsed(); - - let start_python = Instant::now(); - let default_values = squire::settings::default_file_formats(); - // https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.collect_tuple - let _file_format = config.file_formats.iter().collect_tuple(); - let file_format = if _file_format.is_none() { - log::debug!("CRITICAL::Failed to extract tuple from {:?}", config.file_formats); - default_values.iter().collect_tuple() - } else { - _file_format - }; - let args = (__target_str, child_dir, file_format.unwrap()); - let _listing_page = squire::fileio::get_dir_stream_content(args); - let python_time_taken = start_python.elapsed(); - - println!("listing_page [py]: {} seconds", python_time_taken.as_secs_f64()); - println!("listing_page [rs]: {} seconds", rust_time_taken.as_secs_f64()); - let listing = template.get_template("listing").unwrap(); return HttpResponse::build(StatusCode::OK) .content_type("text/html; charset=utf-8") diff --git a/src/squire/content.rs b/src/squire/content.rs index e2d1941..626af22 100644 --- a/src/squire/content.rs +++ b/src/squire/content.rs @@ -124,7 +124,7 @@ pub fn get_all_stream_content(config: &Config) -> ContentPayload { /// # Returns /// /// A `ContentPayload` struct representing the content of the specified directory. -pub fn get_dir_stream_content(parent: &str, subdir: &str, file_formats: &Vec) -> ContentPayload { +pub fn get_dir_stream_content(parent: &str, subdir: &str, file_formats: &[String]) -> ContentPayload { let mut files = Vec::new(); for entry in fs::read_dir(parent).unwrap().flatten() { let file_name = entry.file_name().into_string().unwrap(); diff --git a/src/squire/fileio.rs b/src/squire/fileio.rs deleted file mode 100644 index 5063bc7..0000000 --- a/src/squire/fileio.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::collections::HashMap; - -use pyo3::{Py, PyAny, PyResult, Python}; -use pyo3::prelude::PyModule; -use serde::{Deserialize, Serialize}; - -/// Represents the payload structure for content, including files and directories. -/// -/// This struct is used for serialization and deserialization, providing default values -/// when necessary. -#[derive(Debug, Serialize, Deserialize, Default)] -pub struct ContentPayload { - /// List of files with their names and paths. - #[serde(default = "default_structure")] - pub files: Vec>, - /// List of directories with their names and paths. - #[serde(default = "default_structure")] - pub directories: Vec>, -} - -/// Returns the default structure for content, represented as an empty vector of HashMaps. -pub fn default_structure() -> Vec> { - Vec::new() -} - -/// Converts a JSON-formatted string into a `ContentPayload` struct. -/// -/// # Arguments -/// -/// * `content` - A JSON-formatted string containing content information. -/// -/// # Returns -/// -/// A `ContentPayload` struct representing the deserialized content. -pub fn convert_to_json(content: String) -> ContentPayload { - let output: serde_json::Result = serde_json::from_str(&content); - match output { - Ok(raw_config) => raw_config, - Err(err) => { - log::error!("Error deserializing JSON: {}", err); - log::error!("Raw content from Python: {:?}", content); - ContentPayload::default() - } - } -} - -/// Retrieves content information for all streams. -/// -/// # Arguments -/// -/// * `args` - A tuple containing a stream identifier, and references to two strings. -/// -/// # Returns -/// -/// A `ContentPayload` struct representing the content of all streams. -pub fn get_all_stream_content(args: (String, (&String, &String))) -> ContentPayload { - let py_app = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/python/fileio.py")); - let from_python = Python::with_gil(|py| -> PyResult> { - let app: Py = PyModule::from_code(py, py_app, "", "")? - .getattr("get_all_stream_content")? - .into(); - app.call1(py, args) - }); - convert_to_json(from_python.unwrap().to_string()) -} - -/// Retrieves content information for a specific directory within a stream. -/// -/// # Arguments -/// -/// * `args` - A tuple containing a stream identifier, a directory path, and references to two strings. -/// -/// # Returns -/// -/// A `ContentPayload` struct representing the content of the specified directory. -pub fn get_dir_stream_content(args: (String, String, (&String, &String))) -> ContentPayload { - let py_app = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/python/fileio.py")); - let from_python = Python::with_gil(|py| -> PyResult> { - let app: Py = PyModule::from_code(py, py_app, "", "")? - .getattr("get_dir_stream_content")? - .into(); - app.call1(py, args) - }); - convert_to_json(from_python.unwrap().to_string()) -} - -/// Represents an iterator structure with optional previous and next elements. -#[derive(Debug, Serialize, Deserialize, Default)] -pub struct Iter { - /// Optional previous element in the iteration. - pub previous: Option, - /// Optional next element in the iteration. - pub next: Option, -} - -/// Retrieves iterator information from Python based on the provided arguments. -/// -/// # Arguments -/// -/// * `args` - A tuple containing a stream identifier and references to two strings. -/// -/// # Returns -/// -/// An `Iter` struct representing the iterator information. -pub fn get_iter(args: (&String, (&String, &String))) -> Iter { - let py_app = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/python/fileio.py")); - let from_python = Python::with_gil(|py| -> PyResult> { - let app: Py = PyModule::from_code(py, py_app, "", "")? - .getattr("get_iter")? - .into(); - app.call1(py, args) - }); - match from_python { - Ok(result) => { - let content = result.to_string(); - let output: serde_json::Result = serde_json::from_str(&content); - match output { - Ok(parsed_vector) => parsed_vector, - Err(err) => { - log::error!("Error parsing JSON response from Python: {}", err); - log::error!("Raw content from Python: {}", content); - Iter::default() - } - } - } - Err(err) => { - log::error!("Error calling Python function: {}", err); - Iter::default() - } - } -} diff --git a/src/squire/mod.rs b/src/squire/mod.rs index 17bbce6..b069e8c 100644 --- a/src/squire/mod.rs +++ b/src/squire/mod.rs @@ -2,7 +2,6 @@ pub mod parser; pub mod settings; pub mod startup; pub mod secure; -pub mod fileio; pub mod logger; pub mod ascii_art; pub mod middleware; diff --git a/src/squire/settings.rs b/src/squire/settings.rs index a54f234..eaec078 100644 --- a/src/squire/settings.rs +++ b/src/squire/settings.rs @@ -76,8 +76,7 @@ fn default_session_duration() -> i32 { /// /// Set as public, since this function is re-used in `startup.rs` pub fn default_file_formats() -> Vec { - // todo: remove the dot (.) - vec![".mp4".to_string(), ".mov".to_string()] + vec!["mp4".to_string(), "mov".to_string()] } /// Returns the default number of worker threads (half of logical cores).