[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.

browse: non-blocking try_write + lazy ContentDirectory traversal

+147 -45
+99 -45
src/browse.rs
··· 1 - use crate::library::{Entry, Library}; 1 + use crate::library::{is_media_file, Entry, FileMeta, Library}; 2 2 use librqbit_upnp_serve::services::content_directory::{ 3 3 browse::response::{Container, Item, ItemOrContainer}, 4 4 ContentDirectoryBrowseProvider, 5 5 }; 6 + use std::collections::BTreeSet; 7 + use std::path::{Component, Path, PathBuf}; 6 8 use std::sync::Arc; 7 9 use tokio::sync::RwLock; 8 10 ··· 14 16 parent_id: usize, 15 17 http_hostname: &str, 16 18 ) -> Vec<ItemOrContainer> { 17 - let mut lib = self.0.blocking_write(); 19 + let Ok(mut lib) = self.0.try_write() else { 20 + return Vec::new(); 21 + }; 18 22 if parent_id == 0 { 19 23 let mut out = Vec::new(); 20 24 let folder_paths: Vec<_> = lib.folders().iter().map(|f| f.path.clone()).collect(); ··· 41 45 Entry::Dir(p) => p, 42 46 Entry::File(_) => return Vec::new(), 43 47 }; 44 - let read = match std::fs::read_dir(&dir) { 45 - Ok(r) => r, 46 - Err(_) => return Vec::new(), 47 - }; 48 - let mut out = Vec::new(); 49 - for de in read.flatten() { 50 - let path = de.path(); 51 - let name = de.file_name().to_string_lossy().into_owned(); 52 - if name.starts_with('.') { 53 - continue; 54 - } 55 - let ft = match de.file_type() { 56 - Ok(f) => f, 57 - Err(_) => continue, 58 - }; 59 - if ft.is_dir() { 60 - let id = lib.intern(Entry::Dir(path.clone())); 61 - out.push(ItemOrContainer::Container(Container { 62 - id, 63 - parent_id: Some(parent_id), 64 - children_count: None, 65 - title: name, 66 - })); 67 - } else if ft.is_file() { 68 - let meta = match de.metadata() { 69 - Ok(m) => m, 70 - Err(_) => continue, 71 - }; 72 - let id = lib.intern(Entry::File(path.clone())); 73 - let mime = mime_guess::from_path(&path).first(); 74 - let url = format!("http://{http_hostname}/stream/{id}"); 75 - out.push(ItemOrContainer::Item(Item { 76 - id, 77 - parent_id, 78 - title: name, 79 - mime_type: mime, 80 - url, 81 - size: meta.len(), 82 - })); 83 - } 84 - } 85 - out 48 + children_for_dir(&mut lib, &dir, parent_id, http_hostname) 86 49 } 87 50 88 51 fn browse_metadata(&self, object_id: usize, http_hostname: &str) -> Vec<ItemOrContainer> { ··· 94 57 title: "dlna-rs".to_string(), 95 58 })]; 96 59 } 97 - let lib = self.0.blocking_read(); 60 + let Ok(lib) = self.0.try_read() else { 61 + return Vec::new(); 62 + }; 98 63 let entry = match lib.ids().get(object_id) { 99 64 Some(e) => e.clone(), 100 65 None => return Vec::new(), ··· 110 75 .unwrap_or_default(), 111 76 })], 112 77 Entry::File(p) => { 78 + if !is_media_file(&p) { 79 + return Vec::new(); 80 + } 113 81 let size = std::fs::metadata(&p).map(|m| m.len()).unwrap_or(0); 114 82 let mime = mime_guess::from_path(&p).first(); 115 83 let url = format!("http://{http_hostname}/stream/{object_id}"); ··· 128 96 } 129 97 } 130 98 } 99 + 100 + fn children_for_dir( 101 + lib: &mut Library, 102 + dir: &Path, 103 + parent_id: usize, 104 + http_hostname: &str, 105 + ) -> Vec<ItemOrContainer> { 106 + let Some((folder_path, files, base_rel)) = folder_files_for_dir(lib, dir) else { 107 + return Vec::new(); 108 + }; 109 + 110 + let mut out = Vec::new(); 111 + let mut child_dirs = BTreeSet::new(); 112 + 113 + for file in files { 114 + let Some(child) = child_under_dir(&base_rel, &file.rel) else { 115 + continue; 116 + }; 117 + match child { 118 + Child::File => { 119 + let path = folder_path.join(&file.rel); 120 + let id = lib.intern(Entry::File(path.clone())); 121 + let mime = mime_guess::from_path(&path).first(); 122 + let url = format!("http://{http_hostname}/stream/{id}"); 123 + let title = path 124 + .file_name() 125 + .map(|n| n.to_string_lossy().into_owned()) 126 + .unwrap_or_default(); 127 + out.push(ItemOrContainer::Item(Item { 128 + id, 129 + parent_id, 130 + title, 131 + mime_type: mime, 132 + url, 133 + size: file.size, 134 + })); 135 + } 136 + Child::Dir(name) => { 137 + child_dirs.insert(name); 138 + } 139 + } 140 + } 141 + 142 + for name in child_dirs { 143 + let path = dir.join(&name); 144 + let id = lib.intern(Entry::Dir(path)); 145 + out.push(ItemOrContainer::Container(Container { 146 + id, 147 + parent_id: Some(parent_id), 148 + children_count: None, 149 + title: name.to_string_lossy().into_owned(), 150 + })); 151 + } 152 + 153 + out 154 + } 155 + 156 + fn folder_files_for_dir( 157 + lib: &Library, 158 + dir: &Path, 159 + ) -> Option<(PathBuf, Vec<FileMeta>, PathBuf)> { 160 + lib.folders() 161 + .iter() 162 + .find_map(|folder| { 163 + let base_rel = dir.strip_prefix(&folder.path).ok()?.to_path_buf(); 164 + Some((folder.path.clone(), folder.files.clone(), base_rel)) 165 + }) 166 + } 167 + 168 + enum Child { 169 + File, 170 + Dir(PathBuf), 171 + } 172 + 173 + fn child_under_dir(base_rel: &Path, file_rel: &Path) -> Option<Child> { 174 + let rel = file_rel.strip_prefix(base_rel).ok()?; 175 + let mut components = rel.components(); 176 + let first = components.next()?; 177 + if components.next().is_none() { 178 + return Some(Child::File); 179 + } 180 + match first { 181 + Component::Normal(name) => Some(Child::Dir(PathBuf::from(name))), 182 + _ => None, 183 + } 184 + }
+3
tests/api.rs
··· 106 106 async fn list_files_for_configured_folder() { 107 107 let dir = tempdir().unwrap(); 108 108 std::fs::create_dir(dir.path().join("sub")).unwrap(); 109 + std::fs::write(dir.path().join(".DS_Store"), b"junk").unwrap(); 109 110 std::fs::write(dir.path().join("a.mp4"), b"hello").unwrap(); 111 + std::fs::write(dir.path().join("ca.strings"), b"junk").unwrap(); 110 112 std::fs::write(dir.path().join("sub/b.mp4"), b"world!").unwrap(); 113 + std::fs::write(dir.path().join("sub/metadata.tvdb"), b"junk").unwrap(); 111 114 112 115 let lib = Arc::new(RwLock::new(Library::new())); 113 116 let canonical = lib.write().await.add_folder(dir.path().to_path_buf(), true).unwrap().path;
+45
tests/browse.rs
··· 1 + use dlna_rs::browse::LibraryProvider; 2 + use dlna_rs::library::Library; 3 + use librqbit_upnp_serve::services::content_directory::{ 4 + browse::response::ItemOrContainer, 5 + ContentDirectoryBrowseProvider, 6 + }; 7 + use std::sync::Arc; 8 + use tempfile::tempdir; 9 + use tokio::sync::RwLock; 10 + 11 + #[tokio::test] 12 + async fn provider_browses_inside_tokio_runtime() { 13 + let dir = tempdir().unwrap(); 14 + std::fs::create_dir(dir.path().join("empty")).unwrap(); 15 + std::fs::create_dir(dir.path().join("docs")).unwrap(); 16 + std::fs::create_dir_all(dir.path().join("shows/season1")).unwrap(); 17 + std::fs::write(dir.path().join("movie.mp4"), b"x").unwrap(); 18 + std::fs::write(dir.path().join("docs/readme.txt"), b"x").unwrap(); 19 + std::fs::write(dir.path().join("notes.txt"), b"x").unwrap(); 20 + std::fs::write(dir.path().join("shows/season1/episode.mkv"), b"x").unwrap(); 21 + 22 + let mut library = Library::new(); 23 + library.add_folder(dir.path().to_path_buf(), true).unwrap(); 24 + let provider = LibraryProvider(Arc::new(RwLock::new(library))); 25 + 26 + let root = provider.browse_direct_children(0, "127.0.0.1:9119"); 27 + let folder_id = match root.as_slice() { 28 + [ItemOrContainer::Container(folder)] => folder.id, 29 + other => panic!("expected one root folder, got {other:?}"), 30 + }; 31 + 32 + let children = provider.browse_direct_children(folder_id, "127.0.0.1:9119"); 33 + assert_eq!(children.len(), 2); 34 + match &children[0] { 35 + ItemOrContainer::Item(item) => { 36 + assert_eq!(item.title, "movie.mp4"); 37 + assert_eq!(item.mime_type.as_ref().map(|m| m.essence_str()), Some("video/mp4")); 38 + } 39 + other => panic!("expected media item, got {other:?}"), 40 + } 41 + match &children[1] { 42 + ItemOrContainer::Container(container) => assert_eq!(container.title, "shows"), 43 + other => panic!("expected media-bearing subdirectory, got {other:?}"), 44 + } 45 + }