use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Dir,
Video,
Audio,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
pub name: String,
pub path: PathBuf,
pub kind: Kind,
}
const VIDEO_EXT: &[&str] = &["mkv", "mp4", "avi", "mov", "webm", "m4v", "ts", "wmv"];
const AUDIO_EXT: &[&str] = &["mp3", "flac", "m4a", "ogg", "opus", "wav", "aac"];
pub fn classify(path: &Path) -> Option<Kind> {
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
if VIDEO_EXT.contains(&ext.as_str()) {
Some(Kind::Video)
} else if AUDIO_EXT.contains(&ext.as_str()) {
Some(Kind::Audio)
} else {
None
}
}
pub fn list(dir: &Path) -> io::Result<Vec<Entry>> {
let mut entries = Vec::new();
for e in std::fs::read_dir(dir)? {
let e = e?;
let path = e.path();
let name = e.file_name().to_string_lossy().into_owned();
if name.starts_with('.') {
continue;
}
let ft = e.file_type()?;
let is_dir = if ft.is_symlink() {
match std::fs::metadata(&path) {
Ok(m) => m.is_dir(),
Err(_) => continue,
}
} else {
ft.is_dir()
};
let kind = if is_dir {
Kind::Dir
} else {
match classify(&path) {
Some(k) => k,
None => continue,
}
};
entries.push(Entry { name, path, kind });
}
entries.sort_by(|a, b| {
let dir_first = (a.kind != Kind::Dir).cmp(&(b.kind != Kind::Dir));
dir_first.then_with(|| a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase()))
});
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp() -> PathBuf {
let p = std::env::temp_dir().join(format!("medias-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn classify_by_ext() {
assert_eq!(classify(Path::new("a.MKV")), Some(Kind::Video));
assert_eq!(classify(Path::new("b.flac")), Some(Kind::Audio));
assert_eq!(classify(Path::new("c.txt")), None);
assert_eq!(classify(Path::new("noext")), None);
}
#[test]
fn list_sorts_dirs_first_then_name_and_skips_junk() {
let d = tmp();
std::fs::create_dir(d.join("Zseason")).unwrap();
std::fs::write(d.join("beta.mp4"), b"").unwrap();
std::fs::write(d.join("Alpha.mkv"), b"").unwrap();
std::fs::write(d.join("notes.txt"), b"").unwrap(); std::fs::write(d.join(".hidden.mp4"), b"").unwrap(); std::os::unix::fs::symlink(d.join("Zseason"), d.join("Linked")).unwrap();
std::os::unix::fs::symlink(d.join("absent.mkv"), d.join("gone.mkv")).unwrap();
let got: Vec<(String, Kind)> =
list(&d).unwrap().into_iter().map(|e| (e.name, e.kind)).collect();
assert_eq!(
got,
vec![
("Linked".into(), Kind::Dir),
("Zseason".into(), Kind::Dir),
("Alpha.mkv".into(), Kind::Video),
("beta.mp4".into(), Kind::Video),
]
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn missing_dir_errors() {
assert!(list(Path::new("/no/such/medias/dir")).is_err());
}
}