rustream/squire/
content.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7use walkdir::WalkDir;
8
9use crate::constant;
10use crate::squire::authenticator;
11use crate::squire::settings;
12
13/// Represents the payload structure for content, including files and directories.
14///
15/// This struct is used for serialization and deserialization, providing default values
16/// when necessary.
17#[derive(Debug, Serialize, Deserialize, Default)]
18pub struct ContentPayload {
19    /// List of files with their names, paths and font icons.
20    #[serde(default = "default_structure")]
21    pub files: Vec<HashMap<String, String>>,
22    /// List of directories with their names, paths and font icons.
23    #[serde(default = "default_structure")]
24    pub directories: Vec<HashMap<String, String>>,
25    /// List of user specific directories with their names, paths and font icons.
26    #[serde(default = "default_structure")]
27    pub secured_directories: Vec<HashMap<String, String>>,
28}
29
30/// Returns the default structure for content, represented as an empty vector of HashMaps.
31pub fn default_structure() -> Vec<HashMap<String, String>> {
32    Vec::new()
33}
34
35/// Extracts a natural sort key from a filename.
36///
37/// This function takes a filename as input and splits it into a list of parts using a regular expression.
38/// It then converts numeric parts to integers while keeping non-numeric parts as lowercase strings.
39/// This enables a natural sorting order that considers both alphabetical and numerical components of filenames,
40/// making it suitable for sorting filenames in a human-friendly manner.
41///
42/// # Arguments
43///
44/// * `regex` - Pre-compiled regex object.
45/// * `filename` - A string representing the filename.
46///
47/// # Returns
48///
49/// A vector of `Result<i32, String>` where each element is either an integer representing a numeric part
50/// or a string representing a non-numeric part converted to lowercase.
51fn natural_sort_key(regex: &Regex, filename: &str) -> Vec<Result<i32, String>> {
52    // reusing regex is way faster than creating a new object everytime (~8s
53    regex.find_iter(filename)
54        .map(|part| {
55            // chaining methods is kinda faster (~79% faster in terms of ms)
56            part.as_str().parse::<i32>().map_err(|e| e.to_string())
57            // if let Ok(num) = part.as_str().parse::<i32>() {
58            //     Ok(num)
59            // } else {
60            //     Err(part.as_str().to_string())
61            // }
62        })
63        .collect()
64}
65
66
67/// Generate font awesome icon's value for a given file extension.
68///
69/// Creates custom icons for `image` files, defaults to `video` icon.
70///
71/// # Arguments
72///
73/// * `extn` - File extension.
74///
75/// # Returns
76///
77/// A string with the `fa` value based on the file extension.
78pub fn get_file_font(extn: &str) -> String {
79    let font = if constant::IMAGE_FORMATS.contains(&extn) {
80        "fa-regular fa-file-image"
81    } else {
82        "fa-regular fa-file-video"
83    };
84    font.to_string()
85}
86
87/// Generate font awesome icon's value for a given folder depth.
88///
89/// Creates custom icons for `folder-tree`, defaults to `folder` icon.
90///
91/// # Arguments
92///
93/// * `tree` - Depth of directories.
94///
95/// # Returns
96///
97/// A string with the `fa` value based on the folder depth.
98fn get_folder_font(structure: &Path,
99                   auth_response: &authenticator::AuthToken) -> HashMap<String, String> {
100    let directory = structure.to_string_lossy().to_string();
101    let mut entry_map = HashMap::new();
102    entry_map.insert("path".to_string(), format!("stream/{}", &directory));
103    let depth = &structure.iter().count();
104    for component in structure.iter() {
105        let secured = format!("{}_{}", &auth_response.username, constant::SECURE_INDEX);
106        if secured == component.to_string_lossy() {
107            entry_map.insert("name".to_string(), directory);
108            entry_map.insert("font".to_string(), "fa-solid fa-lock".to_string());
109            entry_map.insert("secured".to_string(), "true".to_string());
110            return entry_map;
111        } else if component.to_string_lossy().ends_with(constant::SECURE_INDEX) {
112            // If the path has secure index value (includes folder trees / subdirectories)
113            return HashMap::new();
114        }
115    }
116    entry_map.insert("name".to_string(), directory);
117    if *depth > 1 {
118        entry_map.insert("font".to_string(), "fa-solid fa-folder-tree".to_string());
119    } else {
120        entry_map.insert("font".to_string(), "fa fa-folder".to_string());
121    }
122    entry_map
123}
124
125/// Retrieves content information for all streams.
126///
127/// # Arguments
128///
129/// * `config` - Configuration data for the application.
130///
131/// # Returns
132///
133/// A `ContentPayload` struct representing the content of all streams.
134pub fn get_all_stream_content(config: &settings::Config, auth_response: &authenticator::AuthToken) -> ContentPayload {
135    let mut payload = ContentPayload::default();
136
137    for entry in WalkDir::new(&config.media_source).into_iter().filter_map(|e| e.ok()) {
138        if entry.path().ends_with("__") {
139            continue;
140        }
141
142        if let Some(file_name) = entry.file_name().to_str() {
143            if file_name.starts_with('_') || file_name.starts_with('.') {
144                continue;
145            }
146
147            if let Some(extension) = PathBuf::from(file_name).extension().and_then(|ext| ext.to_str()) {
148                if config.file_formats.iter().any(|format| extension == format) {
149                    let path = entry.path().strip_prefix(&config.media_source)
150                        .unwrap_or_else(|_| Path::new(""));
151                    let components: &Vec<_> = &path.components().collect();
152                    if components.len() == 1 {
153                        let mut entry_map = HashMap::new();
154                        entry_map.insert("path".to_string(), format!("stream/{}", &file_name));
155                        entry_map.insert("name".to_string(), file_name.to_string());
156                        entry_map.insert("font".to_string(), get_file_font(extension));
157                        payload.files.push(entry_map);
158                    } else {
159                        /*
160                        path.components(): returns an iterator over the components of the path
161                        .rev(): reverses the order of the iterator
162                        .skip(1): skips the first (originally last) component of the reversed path
163                         */
164                        let skimmed = path.components().rev().skip(1)
165                            .collect::<Vec<_>>().iter().rev()
166                            .collect::<PathBuf>();
167                        let entry_map = get_folder_font(&skimmed, auth_response);
168                        if entry_map.get("secured").unwrap_or(&"".to_string()) == "true" {
169                            if payload.secured_directories.contains(&entry_map) || entry_map.is_empty() { continue; }
170                            payload.secured_directories.push(entry_map);
171                        } else {
172                            if payload.directories.contains(&entry_map) || entry_map.is_empty() { continue; }
173                            payload.directories.push(entry_map);
174                        }
175                    }
176                }
177            }
178        }
179    }
180
181    let re = Regex::new(r"(\D+|\d+)").unwrap();
182    payload.files.sort_by(|a, b| natural_sort_key(&re, &a["name"]).cmp(&natural_sort_key(&re, &b["name"])));
183    payload.directories.sort_by(|a, b| natural_sort_key(&re, &a["name"]).cmp(&natural_sort_key(&re, &b["name"])));
184
185    payload
186}
187
188/// Retrieves content information for a specific directory within a stream.
189///
190/// # Arguments
191///
192/// * `parent` - Path to the parent directory.
193/// * `child` - Path to the child directory.
194/// * `file_formats` - File formats (set as env vars) that are allowed for streaming.
195///
196/// # Returns
197///
198/// A `ContentPayload` struct representing the content of the specified directory.
199pub fn get_dir_stream_content(parent: &str,
200                              child: &str,
201                              file_formats: &[String]) -> ContentPayload {
202    let mut files = Vec::new();
203    for entry in fs::read_dir(parent).unwrap().flatten() {
204        let file_name = entry.file_name().into_string().unwrap();
205        if file_name.starts_with('_') || file_name.starts_with('.') {
206            continue;
207        }
208        let file_path = Path::new(child).join(&file_name);
209        let file_extn = &file_path.extension().unwrap_or_default().to_string_lossy().to_string();
210        if file_formats.contains(file_extn) {
211            let map = HashMap::from([
212                ("name".to_string(), file_name),
213                ("path".to_string(), file_path.to_string_lossy().to_string()),
214                ("font".to_string(), get_file_font(file_extn))
215            ]);
216            files.push(map);
217        }
218    }
219    let re = Regex::new(r"(\D+|\d+)").unwrap();
220    files.sort_by_key(|a| natural_sort_key(&re, a.get("name").unwrap()));
221    ContentPayload { files, ..Default::default() }
222}
223
224/// Represents an iterator structure with optional previous and next elements.
225#[derive(Debug, Serialize, Deserialize, Default)]
226pub struct Iter {
227    /// Optional previous element in the iteration.
228    pub previous: Option<String>,
229    /// Optional next element in the iteration.
230    pub next: Option<String>,
231}
232
233/// Retrieves the previous and/or next file to the currently streaming file.
234///
235/// # Arguments
236///
237/// * `filepath` - File that is requested for streaming.
238/// * `file_formats` - Vector of file formats (as String) that are allowed.
239///
240/// # Returns
241///
242/// An `Iter` struct representing the iterator information.
243pub fn get_iter(filepath: &Path, file_formats: &[String]) -> Iter {
244    let parent = filepath.parent().unwrap();
245    let mut dir_content: Vec<String> = fs::read_dir(parent)
246        .ok().unwrap()
247        .flatten()
248        .filter_map(|entry| {
249            let file_name = entry.file_name().to_string_lossy().to_string();
250            let file_extn = Path::new(&file_name).extension().unwrap_or_default().to_string_lossy().to_string();
251            if file_formats.contains(&file_extn) {
252                Some(file_name)
253            } else {
254                None
255            }
256        })
257        .collect();
258    let re = Regex::new(r"(\D+|\d+)").unwrap();
259    dir_content.sort_by_key(|a| natural_sort_key(&re, a));
260
261    let idx = dir_content.iter().position(|file| file == filepath.file_name().unwrap().to_str().unwrap()).unwrap();
262
263    let previous_ = if idx > 0 {
264        let previous_ = &dir_content[idx - 1];
265        if previous_ == filepath.file_name().unwrap().to_str().unwrap() {
266            None
267        } else {
268            Some(previous_.clone())
269        }
270    } else {
271        None
272    };
273
274    let next_ = dir_content.get(idx + 1).cloned();
275
276    Iter { previous: previous_, next: next_ }
277}