#![forbid(unsafe_code)]
use super::tags::{TagMap, TagValue};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TagPreference {
PreferA,
PreferB,
Merge,
}
pub fn copy_all_tags(src: &TagMap, dst: &mut TagMap) {
for (key, value) in src.iter() {
dst.set(key, value.clone());
}
}
#[must_use]
pub fn merge_tags(a: &TagMap, b: &TagMap, prefer: TagPreference) -> TagMap {
let mut result = TagMap::new();
match prefer {
TagPreference::PreferA => {
for (key, value) in b.iter() {
result.set(key, value.clone());
}
for (key, value) in a.iter() {
result.set(key, value.clone());
}
}
TagPreference::PreferB => {
for (key, value) in a.iter() {
result.set(key, value.clone());
}
for (key, value) in b.iter() {
result.set(key, value.clone());
}
}
TagPreference::Merge => {
for (key, value) in a.iter() {
let is_non_empty = match value {
TagValue::Text(s) => !s.is_empty(),
TagValue::Binary(b) => !b.is_empty(),
};
if is_non_empty {
result.set(key, value.clone());
}
}
for (key, value) in b.iter() {
if result.get(key).is_none() {
result.set(key, value.clone());
}
}
}
}
result
}
pub fn strip_tags(meta: &mut TagMap, tags_to_remove: &[&str]) {
for &key in tags_to_remove {
meta.remove(key);
}
}
pub fn copy_selected_tags(src: &TagMap, dst: &mut TagMap, keys: &[&str]) {
for &key in keys {
if let Some(value) = src.get(key) {
dst.set(key, value.clone());
}
}
}
pub fn rename_tag(meta: &mut TagMap, from: &str, to: &str) -> bool {
if let Some(value) = meta.get(from).cloned() {
meta.remove(from);
meta.set(to, value);
true
} else {
false
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TagFilter {
AllowList(Vec<String>),
BlockList(Vec<String>),
PassThrough,
}
impl TagFilter {
#[must_use]
pub fn allows(&self, key: &str) -> bool {
let key_upper = key.to_ascii_uppercase();
match self {
Self::AllowList(list) => list
.iter()
.any(|k| k.to_ascii_uppercase() == key_upper),
Self::BlockList(list) => !list
.iter()
.any(|k| k.to_ascii_uppercase() == key_upper),
Self::PassThrough => true,
}
}
}
impl Default for TagFilter {
fn default() -> Self {
Self::PassThrough
}
}
pub struct TagCopySession<'a> {
source: &'a TagMap,
filter: TagFilter,
destinations: Vec<&'a mut TagMap>,
}
impl<'a> TagCopySession<'a> {
#[must_use]
pub fn new(source: &'a TagMap) -> Self {
Self {
source,
filter: TagFilter::PassThrough,
destinations: Vec::new(),
}
}
#[must_use]
pub fn with_filter(mut self, filter: TagFilter) -> Self {
self.filter = filter;
self
}
#[must_use]
pub fn add_destination(mut self, dst: &'a mut TagMap) -> Self {
self.destinations.push(dst);
self
}
pub fn execute(self) -> usize {
let mut count = 0usize;
let entries: Vec<(&str, &TagValue)> = self
.source
.iter()
.filter(|(k, _)| self.filter.allows(k))
.collect();
for dst in self.destinations {
for (key, value) in &entries {
dst.set(*key, (*value).clone());
count += 1;
}
}
count
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_map(pairs: &[(&str, &str)]) -> TagMap {
let mut m = TagMap::new();
for &(k, v) in pairs {
m.set(k, v);
}
m
}
#[test]
fn test_copy_all_tags_basic() {
let src = make_map(&[("TITLE", "Title"), ("ARTIST", "Artist")]);
let mut dst = TagMap::new();
copy_all_tags(&src, &mut dst);
assert_eq!(dst.get_text("TITLE"), Some("Title"));
assert_eq!(dst.get_text("ARTIST"), Some("Artist"));
}
#[test]
fn test_copy_all_tags_overwrites_existing() {
let src = make_map(&[("TITLE", "New Title")]);
let mut dst = make_map(&[("TITLE", "Old Title"), ("ALBUM", "Existing")]);
copy_all_tags(&src, &mut dst);
assert_eq!(dst.get_text("TITLE"), Some("New Title"));
assert_eq!(dst.get_text("ALBUM"), Some("Existing"));
}
#[test]
fn test_copy_all_tags_from_empty() {
let src = TagMap::new();
let mut dst = make_map(&[("TITLE", "Keep")]);
copy_all_tags(&src, &mut dst);
assert_eq!(dst.get_text("TITLE"), Some("Keep"));
}
#[test]
fn test_copy_all_tags_into_empty() {
let src = make_map(&[("TITLE", "T"), ("ARTIST", "A"), ("ALBUM", "B")]);
let mut dst = TagMap::new();
copy_all_tags(&src, &mut dst);
assert_eq!(dst.get_text("TITLE"), Some("T"));
assert_eq!(dst.get_text("ARTIST"), Some("A"));
assert_eq!(dst.get_text("ALBUM"), Some("B"));
}
#[test]
fn test_merge_prefer_a_on_conflict() {
let a = make_map(&[("TITLE", "A-Title"), ("ARTIST", "A-Artist")]);
let b = make_map(&[("TITLE", "B-Title"), ("ALBUM", "B-Album")]);
let merged = merge_tags(&a, &b, TagPreference::PreferA);
assert_eq!(merged.get_text("TITLE"), Some("A-Title")); assert_eq!(merged.get_text("ARTIST"), Some("A-Artist")); assert_eq!(merged.get_text("ALBUM"), Some("B-Album")); }
#[test]
fn test_merge_prefer_b_on_conflict() {
let a = make_map(&[("TITLE", "A-Title"), ("ARTIST", "A-Artist")]);
let b = make_map(&[("TITLE", "B-Title"), ("ALBUM", "B-Album")]);
let merged = merge_tags(&a, &b, TagPreference::PreferB);
assert_eq!(merged.get_text("TITLE"), Some("B-Title")); assert_eq!(merged.get_text("ARTIST"), Some("A-Artist")); assert_eq!(merged.get_text("ALBUM"), Some("B-Album")); }
#[test]
fn test_merge_merge_policy() {
let a = make_map(&[("TITLE", "A-Title"), ("ALBUM", "")]);
let b = make_map(&[("TITLE", "B-Title"), ("ALBUM", "B-Album"), ("ARTIST", "B-Artist")]);
let merged = merge_tags(&a, &b, TagPreference::Merge);
assert_eq!(merged.get_text("TITLE"), Some("A-Title"));
assert_eq!(merged.get_text("ALBUM"), Some("B-Album"));
assert_eq!(merged.get_text("ARTIST"), Some("B-Artist"));
}
#[test]
fn test_merge_both_empty() {
let a = TagMap::new();
let b = TagMap::new();
let merged = merge_tags(&a, &b, TagPreference::Merge);
assert!(merged.is_empty());
}
#[test]
fn test_merge_only_a_has_values() {
let a = make_map(&[("TITLE", "A"), ("ARTIST", "B")]);
let b = TagMap::new();
let merged = merge_tags(&a, &b, TagPreference::PreferB);
assert_eq!(merged.get_text("TITLE"), Some("A"));
assert_eq!(merged.get_text("ARTIST"), Some("B"));
}
#[test]
fn test_strip_tags_basic() {
let mut meta = make_map(&[("TITLE", "Keep"), ("COMMENT", "Remove"), ("ENCODER", "Remove")]);
strip_tags(&mut meta, &["COMMENT", "ENCODER"]);
assert_eq!(meta.get_text("TITLE"), Some("Keep"));
assert!(meta.get_text("COMMENT").is_none());
assert!(meta.get_text("ENCODER").is_none());
}
#[test]
fn test_strip_tags_case_insensitive() {
let mut meta = make_map(&[("COMMENT", "x"), ("TITLE", "y")]);
strip_tags(&mut meta, &["comment"]);
assert!(meta.get_text("COMMENT").is_none());
assert_eq!(meta.get_text("TITLE"), Some("y"));
}
#[test]
fn test_strip_tags_absent_key_is_noop() {
let mut meta = make_map(&[("TITLE", "T")]);
strip_tags(&mut meta, &["NONEXISTENT", "ALSO_GONE"]);
assert_eq!(meta.get_text("TITLE"), Some("T"));
}
#[test]
fn test_strip_tags_empty_list() {
let mut meta = make_map(&[("TITLE", "T"), ("ARTIST", "A")]);
strip_tags(&mut meta, &[]);
assert_eq!(meta.get_text("TITLE"), Some("T"));
assert_eq!(meta.get_text("ARTIST"), Some("A"));
}
#[test]
fn test_strip_all_tags() {
let mut meta = make_map(&[("A", "1"), ("B", "2"), ("C", "3")]);
strip_tags(&mut meta, &["A", "B", "C"]);
assert!(meta.is_empty());
}
#[test]
fn test_copy_selected_tags_subset() {
let src = make_map(&[("TITLE", "T"), ("ARTIST", "A"), ("COMMENT", "C")]);
let mut dst = TagMap::new();
copy_selected_tags(&src, &mut dst, &["TITLE", "ARTIST"]);
assert_eq!(dst.get_text("TITLE"), Some("T"));
assert_eq!(dst.get_text("ARTIST"), Some("A"));
assert!(dst.get_text("COMMENT").is_none());
}
#[test]
fn test_copy_selected_tags_absent_key_skipped() {
let src = make_map(&[("TITLE", "T")]);
let mut dst = TagMap::new();
copy_selected_tags(&src, &mut dst, &["TITLE", "NONEXISTENT"]);
assert_eq!(dst.get_text("TITLE"), Some("T"));
}
#[test]
fn test_copy_selected_tags_overwrites() {
let src = make_map(&[("TITLE", "New")]);
let mut dst = make_map(&[("TITLE", "Old")]);
copy_selected_tags(&src, &mut dst, &["TITLE"]);
assert_eq!(dst.get_text("TITLE"), Some("New"));
}
#[test]
fn test_rename_tag_basic() {
let mut meta = make_map(&[("OLDKEY", "value"), ("OTHER", "x")]);
let did_rename = rename_tag(&mut meta, "OLDKEY", "NEWKEY");
assert!(did_rename);
assert_eq!(meta.get_text("NEWKEY"), Some("value"));
assert!(meta.get_text("OLDKEY").is_none());
assert_eq!(meta.get_text("OTHER"), Some("x")); }
#[test]
fn test_rename_tag_absent_is_noop() {
let mut meta = make_map(&[("TITLE", "T")]);
let did_rename = rename_tag(&mut meta, "ABSENT", "NEW");
assert!(!did_rename);
assert_eq!(meta.get_text("TITLE"), Some("T"));
assert!(meta.get_text("NEW").is_none());
}
#[test]
fn test_tag_filter_pass_through() {
let f = TagFilter::PassThrough;
assert!(f.allows("TITLE"));
assert!(f.allows("ANYTHING"));
}
#[test]
fn test_tag_filter_allow_list() {
let f = TagFilter::AllowList(vec!["TITLE".into(), "ARTIST".into()]);
assert!(f.allows("TITLE"));
assert!(f.allows("title")); assert!(!f.allows("COMMENT"));
}
#[test]
fn test_tag_filter_block_list() {
let f = TagFilter::BlockList(vec!["ENCODER".into()]);
assert!(f.allows("TITLE"));
assert!(!f.allows("encoder")); }
#[test]
fn test_tag_copy_session_single_destination() {
let src = make_map(&[("TITLE", "T"), ("ARTIST", "A"), ("ENCODER", "enc")]);
let mut dst = TagMap::new();
let count = TagCopySession::new(&src)
.with_filter(TagFilter::BlockList(vec!["ENCODER".into()]))
.add_destination(&mut dst)
.execute();
assert_eq!(count, 2);
assert_eq!(dst.get_text("TITLE"), Some("T"));
assert_eq!(dst.get_text("ARTIST"), Some("A"));
assert!(dst.get_text("ENCODER").is_none());
}
#[test]
fn test_tag_copy_session_multiple_destinations() {
let src = make_map(&[("TITLE", "T"), ("ARTIST", "A")]);
let mut dst1 = TagMap::new();
let mut dst2 = TagMap::new();
let count = TagCopySession::new(&src)
.add_destination(&mut dst1)
.add_destination(&mut dst2)
.execute();
assert_eq!(count, 4);
assert_eq!(dst1.get_text("TITLE"), Some("T"));
assert_eq!(dst2.get_text("ARTIST"), Some("A"));
}
#[test]
fn test_tag_copy_session_no_destinations() {
let src = make_map(&[("TITLE", "T")]);
let count = TagCopySession::new(&src).execute();
assert_eq!(count, 0);
}
#[test]
fn test_tag_copy_session_allow_list_filter() {
let src = make_map(&[("TITLE", "T"), ("ARTIST", "A"), ("COMMENT", "C")]);
let mut dst = TagMap::new();
let count = TagCopySession::new(&src)
.with_filter(TagFilter::AllowList(vec!["TITLE".into()]))
.add_destination(&mut dst)
.execute();
assert_eq!(count, 1);
assert_eq!(dst.get_text("TITLE"), Some("T"));
assert!(dst.get_text("ARTIST").is_none());
}
#[test]
fn test_tag_copy_session_empty_source() {
let src = TagMap::new();
let mut dst = TagMap::new();
dst.set("TITLE", "Existing");
let count = TagCopySession::new(&src)
.add_destination(&mut dst)
.execute();
assert_eq!(count, 0);
assert_eq!(dst.get_text("TITLE"), Some("Existing"));
}
}