[READ-ONLY] Mirror of https://github.com/mrgnw/dlna-rs. Single-binary DLNA media server in Rust. Auto-discovered by VLC on Apple TV.
0

Configure Feed

Select the types of activity you want to include in your feed.

dlna-rs / src / library.rs
3.9 kB 167 lines
1use anyhow::{Context, Result}; 2use std::collections::HashMap; 3use std::path::{Path, PathBuf}; 4 5#[derive(Debug, Clone, PartialEq, Eq)] 6pub enum Entry { 7 File(PathBuf), 8 Dir(PathBuf), 9} 10 11impl Entry { 12 pub fn path(&self) -> &Path { 13 match self { 14 Entry::File(p) | Entry::Dir(p) => p, 15 } 16 } 17 pub fn is_dir(&self) -> bool { 18 matches!(self, Entry::Dir(_)) 19 } 20} 21 22#[derive(Debug, Clone, PartialEq, Eq)] 23pub struct FileMeta { 24 pub rel: std::path::PathBuf, 25 pub size: u64, 26} 27 28#[derive(Debug, Clone, PartialEq, Eq)] 29pub struct Folder { 30 pub path: PathBuf, 31 pub recursive: bool, 32 pub file_count: usize, 33 pub files: Vec<FileMeta>, 34} 35 36#[derive(Debug, Default)] 37pub struct IdMap { 38 forward: HashMap<usize, Entry>, 39 reverse: HashMap<PathBuf, usize>, 40 next: usize, 41} 42 43impl IdMap { 44 pub fn intern(&mut self, entry: Entry) -> usize { 45 if let Some(&id) = self.reverse.get(entry.path()) { 46 return id; 47 } 48 self.next += 1; 49 let id = self.next; 50 self.reverse.insert(entry.path().to_path_buf(), id); 51 self.forward.insert(id, entry); 52 id 53 } 54 55 pub fn get(&self, id: usize) -> Option<&Entry> { 56 self.forward.get(&id) 57 } 58} 59 60#[derive(Debug, Default)] 61pub struct Library { 62 folders: Vec<Folder>, 63 ids: IdMap, 64} 65 66impl Library { 67 pub fn new() -> Self { 68 Self::default() 69 } 70 71 pub fn folders(&self) -> &[Folder] { 72 &self.folders 73 } 74 pub fn ids(&self) -> &IdMap { 75 &self.ids 76 } 77 78 pub fn intern(&mut self, entry: Entry) -> usize { 79 self.ids.intern(entry) 80 } 81 82 pub fn add_folder(&mut self, path: PathBuf, recursive: bool) -> Result<Folder> { 83 let canonical = path 84 .canonicalize() 85 .with_context(|| format!("canonicalize {}", path.display()))?; 86 if self.folders.iter().any(|f| f.path == canonical) { 87 anyhow::bail!("folder already added: {}", canonical.display()); 88 } 89 let files = Self::walk_files(&canonical, recursive); 90 let folder = Folder { 91 path: canonical, 92 recursive, 93 file_count: files.len(), 94 files, 95 }; 96 self.folders.push(folder.clone()); 97 Ok(folder) 98 } 99 100 pub fn remove_folder(&mut self, path: &Path) -> bool { 101 let target = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); 102 let before = self.folders.len(); 103 self.folders.retain(|f| f.path != target); 104 self.folders.len() != before 105 } 106 107 pub fn refresh_count(&mut self, path: &Path) -> Option<usize> { 108 let target = path.canonicalize().ok()?; 109 let f = self.folders.iter_mut().find(|f| f.path == target)?; 110 let files = Self::walk_files(&f.path, f.recursive); 111 f.file_count = files.len(); 112 f.files = files; 113 Some(f.file_count) 114 } 115 116 fn walk_files(root: &Path, recursive: bool) -> Vec<FileMeta> { 117 let max_depth = if recursive { usize::MAX } else { 1 }; 118 let mut out: Vec<FileMeta> = walkdir::WalkDir::new(root) 119 .max_depth(max_depth) 120 .follow_links(false) 121 .into_iter() 122 .filter_map(|e| e.ok()) 123 .filter(|e| e.file_type().is_file()) 124 .filter(|e| is_media_file(e.path())) 125 .filter_map(|e| { 126 let rel = e.path().strip_prefix(root).ok()?.to_path_buf(); 127 let size = e.metadata().ok()?.len(); 128 Some(FileMeta { rel, size }) 129 }) 130 .collect(); 131 out.sort_by(|a, b| a.rel.cmp(&b.rel)); 132 out 133 } 134 135 pub fn from_config(folders: &[crate::config::Folder]) -> Self { 136 let mut lib = Self::new(); 137 for f in folders { 138 let path = std::path::PathBuf::from(&f.path); 139 if let Err(e) = lib.add_folder(path.clone(), f.recursive) { 140 tracing::warn!(?path, "skipping folder: {e}"); 141 } 142 } 143 lib 144 } 145 146 pub fn to_config_folders(&self) -> Vec<crate::config::Folder> { 147 self.folders 148 .iter() 149 .map(|f| crate::config::Folder { 150 path: f.path.to_string_lossy().into_owned(), 151 recursive: f.recursive, 152 }) 153 .collect() 154 } 155} 156 157pub fn is_media_file(path: &Path) -> bool { 158 match mime_guess::from_path(path).first() { 159 Some(mime) => { 160 let essence = mime.essence_str(); 161 essence.starts_with("audio/") 162 || essence.starts_with("image/") 163 || essence.starts_with("video/") 164 } 165 None => false, 166 } 167}