| 1 | //! Reading and writing tags of local files for the tag editor. |
| 2 | |
| 3 | use std::{collections::BTreeMap, path::PathBuf}; |
| 4 | |
| 5 | use lofty::{config::WriteOptions, prelude::*, tag::Tag}; |
| 6 | |
| 7 | /// The tags the editor can change. |
| 8 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] |
| 9 | pub enum TagField { |
| 10 | Title, |
| 11 | Artist, |
| 12 | AlbumArtist, |
| 13 | Album, |
| 14 | Track, |
| 15 | Disc, |
| 16 | Date, |
| 17 | Genre, |
| 18 | Comment, |
| 19 | } |
| 20 | |
| 21 | impl TagField { |
| 22 | pub const ALL: [Self; 9] = [ |
| 23 | Self::Title, |
| 24 | Self::Artist, |
| 25 | Self::AlbumArtist, |
| 26 | Self::Album, |
| 27 | Self::Track, |
| 28 | Self::Disc, |
| 29 | Self::Date, |
| 30 | Self::Genre, |
| 31 | Self::Comment, |
| 32 | ]; |
| 33 | |
| 34 | pub fn label(self) -> &'static str { |
| 35 | match self { |
| 36 | Self::Title => "Title", |
| 37 | Self::Artist => "Artist", |
| 38 | Self::AlbumArtist => "Album Artist", |
| 39 | Self::Album => "Album", |
| 40 | Self::Track => "Track", |
| 41 | Self::Disc => "Disc", |
| 42 | Self::Date => "Date", |
| 43 | Self::Genre => "Genre", |
| 44 | Self::Comment => "Comment", |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | /// The tag name MPD reports for this field. |
| 49 | pub fn mpd_tag(self) -> &'static str { |
| 50 | match self { |
| 51 | Self::Title => "Title", |
| 52 | Self::Artist => "Artist", |
| 53 | Self::AlbumArtist => "AlbumArtist", |
| 54 | Self::Album => "Album", |
| 55 | Self::Track => "Track", |
| 56 | Self::Disc => "Disc", |
| 57 | Self::Date => "Date", |
| 58 | Self::Genre => "Genre", |
| 59 | Self::Comment => "Comment", |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | fn item_key(self) -> ItemKey { |
| 64 | match self { |
| 65 | Self::Title => ItemKey::TrackTitle, |
| 66 | Self::Artist => ItemKey::TrackArtist, |
| 67 | Self::AlbumArtist => ItemKey::AlbumArtist, |
| 68 | Self::Album => ItemKey::AlbumTitle, |
| 69 | Self::Track => ItemKey::TrackNumber, |
| 70 | Self::Disc => ItemKey::DiscNumber, |
| 71 | Self::Date => ItemKey::RecordingDate, |
| 72 | Self::Genre => ItemKey::Genre, |
| 73 | Self::Comment => ItemKey::Comment, |
| 74 | } |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | /// Pending changes for one file. An empty value removes the tag. |
| 79 | #[derive(Clone, Debug, PartialEq, Eq)] |
| 80 | pub struct TagWrite { |
| 81 | pub path: PathBuf, |
| 82 | /// Library-relative path, for reporting and rescanning. |
| 83 | pub uri: String, |
| 84 | pub changes: BTreeMap<TagField, String>, |
| 85 | } |
| 86 | |
| 87 | /// Writes `changes` into the file, creating its primary tag if needed. |
| 88 | pub fn write(write: &TagWrite) -> Result<(), String> { |
| 89 | let describe = |error: &dyn std::fmt::Display| format!("{}: {error}", write.uri); |
| 90 | let mut file = lofty::read_from_path(&write.path).map_err(|e| describe(&e))?; |
| 91 | if file.primary_tag().is_none() { |
| 92 | file.insert_tag(Tag::new(file.primary_tag_type())); |
| 93 | } |
| 94 | let tag = file |
| 95 | .primary_tag_mut() |
| 96 | .ok_or_else(|| format!("{}: cannot create a tag", write.uri))?; |
| 97 | for (field, value) in &write.changes { |
| 98 | let value = value.trim(); |
| 99 | if value.is_empty() { |
| 100 | tag.remove_key(field.item_key()); |
| 101 | } else if !tag.insert_text(field.item_key(), value.to_owned()) { |
| 102 | return Err(format!( |
| 103 | "{}: the {:?} tag cannot hold {}", |
| 104 | write.uri, |
| 105 | tag.tag_type(), |
| 106 | field.label() |
| 107 | )); |
| 108 | } |
| 109 | } |
| 110 | tag.save_to_path(&write.path, WriteOptions::default()) |
| 111 | .map_err(|e| describe(&e)) |
| 112 | } |