Skip to main content

mtag_cli/
metadata.rs

1use std::path::{Path, PathBuf};
2
3use lofty::{read_from_path, Accessor, ItemKey, TaggedFileExt};
4
5use crate::error::{MtagError, MtagResult};
6
7/// Metadata fields used by the organization planner.
8///
9/// Every field except [`TrackMetadata::source_path`] is optional because tags are often
10/// incomplete in real music libraries. Planning code falls back to `Other` for missing
11/// artist or album values.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct TrackMetadata {
14    /// Original audio file path.
15    pub source_path: PathBuf,
16    /// Track artist, usually the performer of one song.
17    pub artist: Option<String>,
18    /// Album title.
19    pub album: Option<String>,
20    /// Album artist, preferred for folder grouping when available.
21    pub album_artist: Option<String>,
22    /// Disc number as read from tags.
23    pub disc: Option<String>,
24    /// Track number as read from tags.
25    pub track: Option<String>,
26    /// Track title.
27    pub title: Option<String>,
28}
29
30/// Reads the tag metadata needed by the planner from one audio file.
31///
32/// # Errors
33///
34/// Returns [`MtagError::ReadMetadata`] if Lofty cannot parse the file and
35/// [`MtagError::MissingTags`] when the file contains no readable tag block.
36pub fn read_track_metadata(path: &Path) -> MtagResult<TrackMetadata> {
37    let tagged_file = read_from_path(path).map_err(|source| MtagError::ReadMetadata {
38        path: path.to_path_buf(),
39        source,
40    })?;
41    let tag = tagged_file
42        .primary_tag()
43        .or_else(|| tagged_file.first_tag())
44        .ok_or_else(|| MtagError::MissingTags {
45            path: path.to_path_buf(),
46        })?;
47
48    Ok(TrackMetadata {
49        source_path: path.to_path_buf(),
50        artist: clean_string(tag.artist().as_deref()),
51        album: clean_string(tag.album().as_deref()),
52        album_artist: clean_string(tag.get_string(&ItemKey::AlbumArtist)),
53        disc: tag.disk().map(|disk| disk.to_string()),
54        track: tag.track().map(|track| track.to_string()),
55        title: clean_string(tag.title().as_deref()),
56    })
57}
58
59fn clean_string(value: Option<&str>) -> Option<String> {
60    value
61        .map(str::trim)
62        .filter(|value| !value.is_empty())
63        .map(ToOwned::to_owned)
64}