[READ-ONLY] Mirror of https://github.com/mrgnw/dlna-rs. Single-binary DLNA media server in Rust. Auto-discovered by VLC on Apple TV.
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}