1use crate::ContainerFormat;
7
8use super::tags::{StandardTag, TagMap, TagValue};
9
10#[cfg(not(target_arch = "wasm32"))]
11use oximedia_core::OxiResult;
12#[cfg(not(target_arch = "wasm32"))]
13use oximedia_io::FileSource;
14#[cfg(not(target_arch = "wasm32"))]
15use std::path::{Path, PathBuf};
16
17#[cfg(not(target_arch = "wasm32"))]
18use super::reader::{detect_format, FlacMetadataReader, MatroskaMetadataReader, MetadataReader};
19#[cfg(not(target_arch = "wasm32"))]
20use super::util::MediaSourceExt;
21#[cfg(not(target_arch = "wasm32"))]
22use super::writer::{
23 FlacMetadataWriter, MatroskaMetadataWriter, MetadataWriter, OggMetadataWriter,
24};
25#[cfg(not(target_arch = "wasm32"))]
26use crate::demux::Demuxer;
27#[cfg(not(target_arch = "wasm32"))]
28use crate::demux::MatroskaDemuxer;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum MetadataFormat {
33 Flac,
35 Ogg,
37 Matroska,
39 WebM,
41}
42
43impl From<ContainerFormat> for MetadataFormat {
44 fn from(format: ContainerFormat) -> Self {
45 match format {
46 ContainerFormat::Flac => Self::Flac,
47 ContainerFormat::Ogg => Self::Ogg,
48 ContainerFormat::WebM => Self::WebM,
49 _ => Self::Matroska, }
51 }
52}
53
54#[cfg(not(target_arch = "wasm32"))]
55pub struct MetadataEditor {
80 path: PathBuf,
82 format: MetadataFormat,
84 tags: TagMap,
86 modified: bool,
88}
89
90#[cfg(not(target_arch = "wasm32"))]
91impl MetadataEditor {
92 pub async fn open(path: impl AsRef<Path>) -> OxiResult<Self> {
101 let path = path.as_ref().to_path_buf();
102
103 let mut magic = [0u8; 8];
105 let mut source_clone = FileSource::open(&path).await?;
106 source_clone.read_exact(&mut magic).await?;
107
108 let container_format = detect_format(&magic)?;
109 let format = MetadataFormat::from(container_format);
110
111 let tags = match format {
113 MetadataFormat::Flac => {
114 let source = FileSource::open(&path).await?;
115 FlacMetadataReader::read(source).await?
116 }
117 MetadataFormat::Ogg => {
118 TagMap::new()
121 }
122 MetadataFormat::Matroska | MetadataFormat::WebM => {
123 let source = FileSource::open(&path).await?;
124 let mut demuxer = MatroskaDemuxer::new(source);
125 demuxer.probe().await?;
126
127 let tags = demuxer.tags();
128 MatroskaMetadataReader::convert_tags(tags)
129 }
130 };
131
132 Ok(Self {
133 path,
134 format,
135 tags,
136 modified: false,
137 })
138 }
139
140 #[must_use]
142 pub const fn format(&self) -> MetadataFormat {
143 self.format
144 }
145
146 #[must_use]
148 pub const fn is_modified(&self) -> bool {
149 self.modified
150 }
151
152 #[must_use]
154 pub fn get(&self, key: &str) -> Option<&TagValue> {
155 self.tags.get(key)
156 }
157
158 #[must_use]
160 pub fn get_text(&self, key: &str) -> Option<&str> {
161 self.tags.get_text(key)
162 }
163
164 #[must_use]
166 pub fn get_all(&self, key: &str) -> &[TagValue] {
167 self.tags.get_all(key)
168 }
169
170 #[must_use]
172 pub fn get_standard(&self, tag: StandardTag) -> Option<&TagValue> {
173 self.tags.get_standard(tag)
174 }
175
176 pub fn set(&mut self, key: impl AsRef<str>, value: impl Into<TagValue>) {
178 self.tags.set(key, value);
179 self.modified = true;
180 }
181
182 pub fn add(&mut self, key: impl AsRef<str>, value: impl Into<TagValue>) {
184 self.tags.add(key, value);
185 self.modified = true;
186 }
187
188 pub fn set_standard(&mut self, tag: StandardTag, value: impl Into<TagValue>) {
190 self.tags.set_standard(tag, value);
191 self.modified = true;
192 }
193
194 pub fn remove(&mut self, key: &str) -> bool {
198 let removed = self.tags.remove(key);
199 if removed {
200 self.modified = true;
201 }
202 removed
203 }
204
205 pub fn clear(&mut self) {
207 if !self.tags.is_empty() {
208 self.tags.clear();
209 self.modified = true;
210 }
211 }
212
213 pub fn keys(&self) -> impl Iterator<Item = &str> {
215 self.tags.keys()
216 }
217
218 pub fn iter(&self) -> impl Iterator<Item = (&str, &TagValue)> {
220 self.tags.iter()
221 }
222
223 #[must_use]
225 pub const fn tags(&self) -> &TagMap {
226 &self.tags
227 }
228
229 pub fn tags_mut(&mut self) -> &mut TagMap {
231 self.modified = true;
232 &mut self.tags
233 }
234
235 pub async fn save(&mut self) -> OxiResult<()> {
244 if !self.modified {
245 return Ok(());
246 }
247
248 let mut source = FileSource::open(&self.path).await?;
249
250 match self.format {
251 MetadataFormat::Flac => {
252 FlacMetadataWriter::write(&mut source, &self.tags).await?;
253 }
254 MetadataFormat::Ogg => {
255 OggMetadataWriter::write(&mut source, &self.tags).await?;
256 }
257 MetadataFormat::Matroska | MetadataFormat::WebM => {
258 MatroskaMetadataWriter::write(&mut source, &self.tags).await?;
259 }
260 }
261
262 self.modified = false;
263 Ok(())
264 }
265
266 pub async fn reload(&mut self) -> OxiResult<()> {
272 let new_editor = Self::open(&self.path).await?;
273 self.tags = new_editor.tags;
274 self.modified = false;
275 Ok(())
276 }
277
278 pub fn copy_all_from(&mut self, source: &TagMap) {
284 self.tags.merge(source);
285 self.modified = true;
286 }
287
288 pub fn copy_tags_from(&mut self, source: &TagMap, tag_keys: &[&str]) {
293 for &key in tag_keys {
294 let values = source.get_all(key);
295 if !values.is_empty() {
296 self.tags.remove(key);
298 for val in values {
299 self.tags.add(key, val.clone());
300 }
301 self.modified = true;
302 }
303 }
304 }
305
306 pub fn copy_standard_tags_from(&mut self, source: &TagMap, tags: &[StandardTag]) {
310 for &tag in tags {
311 if let Some(value) = source.get_standard(tag) {
312 self.tags.set_standard(tag, value.clone());
313 self.modified = true;
314 }
315 }
316 }
317
318 pub fn apply_batch(&mut self, operations: &[BatchTagOperation]) {
323 let mut any_change = false;
324 for op in operations {
325 match op {
326 BatchTagOperation::Set { key, value } => {
327 self.tags.set(key.as_str(), value.clone());
328 any_change = true;
329 }
330 BatchTagOperation::Add { key, value } => {
331 self.tags.add(key.as_str(), value.clone());
332 any_change = true;
333 }
334 BatchTagOperation::Remove { key } => {
335 if self.tags.remove(key) {
336 any_change = true;
337 }
338 }
339 BatchTagOperation::Rename { from, to } => {
340 let values: Vec<TagValue> = self.tags.get_all(from).to_vec();
341 if !values.is_empty() {
342 self.tags.remove(from);
343 for val in values {
344 self.tags.add(to.as_str(), val);
345 }
346 any_change = true;
347 }
348 }
349 BatchTagOperation::SetStandard { tag, value } => {
350 self.tags.set_standard(*tag, value.clone());
351 any_change = true;
352 }
353 BatchTagOperation::RemoveAll => {
354 if !self.tags.is_empty() {
355 self.tags.clear();
356 any_change = true;
357 }
358 }
359 BatchTagOperation::ReplaceValue {
360 key,
361 old_value,
362 new_value,
363 } => {
364 let values: Vec<TagValue> = self.tags.get_all(key).to_vec();
365 let old_text = old_value.as_str();
366 let has_match = values.iter().any(|v| v.as_text() == Some(old_text));
367 if has_match {
368 self.tags.remove(key);
369 for val in values {
370 if val.as_text() == Some(old_text) {
371 self.tags.add(key.as_str(), new_value.clone());
372 } else {
373 self.tags.add(key.as_str(), val);
374 }
375 }
376 any_change = true;
377 }
378 }
379 BatchTagOperation::PrefixValues { key, prefix } => {
380 let values: Vec<TagValue> = self.tags.get_all(key).to_vec();
381 if !values.is_empty() {
382 self.tags.remove(key);
383 for val in values {
384 if let Some(text) = val.as_text() {
385 let new_text = format!("{prefix}{text}");
386 self.tags.add(key.as_str(), TagValue::Text(new_text));
387 } else {
388 self.tags.add(key.as_str(), val);
389 }
390 }
391 any_change = true;
392 }
393 }
394 }
395 }
396 if any_change {
397 self.modified = true;
398 }
399 }
400
401 #[must_use]
405 pub fn diff(&self, other: &TagMap) -> Vec<TagDiff> {
406 let mut diffs = Vec::new();
407
408 for (key, value) in self.tags.iter() {
410 if other.get(key).is_none() {
411 diffs.push(TagDiff::Added {
412 key: key.to_string(),
413 value: value.clone(),
414 });
415 }
416 }
417
418 for (key, _value) in other.iter() {
420 if self.tags.get(key).is_none() {
421 diffs.push(TagDiff::Removed {
422 key: key.to_string(),
423 });
424 }
425 }
426
427 for (key, self_value) in self.tags.iter() {
429 if let Some(other_value) = other.get(key) {
430 if self_value != other_value {
431 diffs.push(TagDiff::Modified {
432 key: key.to_string(),
433 old_value: other_value.clone(),
434 new_value: self_value.clone(),
435 });
436 }
437 }
438 }
439
440 diffs
441 }
442}
443
444#[derive(Debug, Clone)]
448pub enum BatchTagOperation {
449 Set {
451 key: String,
453 value: TagValue,
455 },
456 Add {
458 key: String,
460 value: TagValue,
462 },
463 Remove {
465 key: String,
467 },
468 Rename {
470 from: String,
472 to: String,
474 },
475 SetStandard {
477 tag: StandardTag,
479 value: TagValue,
481 },
482 RemoveAll,
484 ReplaceValue {
486 key: String,
488 old_value: String,
490 new_value: TagValue,
492 },
493 PrefixValues {
495 key: String,
497 prefix: String,
499 },
500}
501
502impl BatchTagOperation {
503 #[must_use]
505 pub fn set(key: impl Into<String>, value: impl Into<TagValue>) -> Self {
506 Self::Set {
507 key: key.into(),
508 value: value.into(),
509 }
510 }
511
512 #[must_use]
514 pub fn add(key: impl Into<String>, value: impl Into<TagValue>) -> Self {
515 Self::Add {
516 key: key.into(),
517 value: value.into(),
518 }
519 }
520
521 #[must_use]
523 pub fn remove(key: impl Into<String>) -> Self {
524 Self::Remove { key: key.into() }
525 }
526
527 #[must_use]
529 pub fn rename(from: impl Into<String>, to: impl Into<String>) -> Self {
530 Self::Rename {
531 from: from.into(),
532 to: to.into(),
533 }
534 }
535
536 #[must_use]
538 pub fn set_standard(tag: StandardTag, value: impl Into<TagValue>) -> Self {
539 Self::SetStandard {
540 tag,
541 value: value.into(),
542 }
543 }
544
545 #[must_use]
547 pub const fn remove_all() -> Self {
548 Self::RemoveAll
549 }
550
551 #[must_use]
553 pub fn replace_value(
554 key: impl Into<String>,
555 old_value: impl Into<String>,
556 new_value: impl Into<TagValue>,
557 ) -> Self {
558 Self::ReplaceValue {
559 key: key.into(),
560 old_value: old_value.into(),
561 new_value: new_value.into(),
562 }
563 }
564
565 #[must_use]
567 pub fn prefix_values(key: impl Into<String>, prefix: impl Into<String>) -> Self {
568 Self::PrefixValues {
569 key: key.into(),
570 prefix: prefix.into(),
571 }
572 }
573}
574
575#[derive(Debug, Clone, PartialEq)]
577pub enum TagDiff {
578 Added {
580 key: String,
582 value: TagValue,
584 },
585 Removed {
587 key: String,
589 },
590 Modified {
592 key: String,
594 old_value: TagValue,
596 new_value: TagValue,
598 },
599}
600
601impl TagDiff {
602 #[must_use]
604 pub fn key(&self) -> &str {
605 match self {
606 Self::Added { key, .. } | Self::Removed { key, .. } | Self::Modified { key, .. } => key,
607 }
608 }
609
610 #[must_use]
612 pub const fn is_added(&self) -> bool {
613 matches!(self, Self::Added { .. })
614 }
615
616 #[must_use]
618 pub const fn is_removed(&self) -> bool {
619 matches!(self, Self::Removed { .. })
620 }
621
622 #[must_use]
624 pub const fn is_modified(&self) -> bool {
625 matches!(self, Self::Modified { .. })
626 }
627}
628
629#[derive(Debug, Clone)]
635pub enum MetadataOp {
636 Set {
638 key: String,
640 value: TagValue,
642 },
643 Remove {
645 key: String,
647 },
648 Rename {
651 from: String,
653 to: String,
655 },
656 SetIfAbsent {
658 key: String,
660 value: TagValue,
662 },
663}
664
665#[derive(Debug, Default)]
692pub struct BatchMetadataEditor {
693 operations: Vec<MetadataOp>,
694}
695
696impl BatchMetadataEditor {
697 #[must_use]
699 pub fn new() -> Self {
700 Self::default()
701 }
702
703 #[must_use]
705 pub fn set(mut self, key: impl Into<String>, value: impl Into<TagValue>) -> Self {
706 self.operations.push(MetadataOp::Set {
707 key: key.into(),
708 value: value.into(),
709 });
710 self
711 }
712
713 #[must_use]
715 pub fn remove(mut self, key: impl Into<String>) -> Self {
716 self.operations.push(MetadataOp::Remove { key: key.into() });
717 self
718 }
719
720 #[must_use]
722 pub fn rename(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
723 self.operations.push(MetadataOp::Rename {
724 from: from.into(),
725 to: to.into(),
726 });
727 self
728 }
729
730 #[must_use]
732 pub fn set_if_absent(mut self, key: impl Into<String>, value: impl Into<TagValue>) -> Self {
733 self.operations.push(MetadataOp::SetIfAbsent {
734 key: key.into(),
735 value: value.into(),
736 });
737 self
738 }
739
740 pub fn apply(
751 &self,
752 metadata: &mut std::collections::HashMap<String, TagValue>,
753 ) -> oximedia_core::OxiResult<usize> {
754 let mut applied: usize = 0;
755 for op in &self.operations {
756 match op {
757 MetadataOp::Set { key, value } => {
758 let changed = metadata
759 .get(key.as_str())
760 .map_or(true, |existing| existing != value);
761 metadata.insert(key.clone(), value.clone());
762 if changed {
763 applied += 1;
764 }
765 }
766 MetadataOp::Remove { key } => {
767 if metadata.remove(key.as_str()).is_some() {
768 applied += 1;
769 }
770 }
771 MetadataOp::Rename { from, to } => {
772 if let Some(value) = metadata.remove(from.as_str()) {
773 metadata.insert(to.clone(), value);
774 applied += 1;
775 }
776 }
778 MetadataOp::SetIfAbsent { key, value } => {
779 if !metadata.contains_key(key.as_str()) {
780 metadata.insert(key.clone(), value.clone());
781 applied += 1;
782 }
783 }
784 }
785 }
786 Ok(applied)
787 }
788
789 #[cfg(not(target_arch = "wasm32"))]
805 pub fn apply_to_file(&self, path: &std::path::Path) -> oximedia_core::OxiResult<usize> {
806 use oximedia_core::OxiError;
807
808 let rt = tokio::runtime::Builder::new_current_thread()
809 .enable_all()
810 .build()
811 .map_err(|e| {
812 OxiError::Io(std::io::Error::new(
813 std::io::ErrorKind::Other,
814 e.to_string(),
815 ))
816 })?;
817
818 rt.block_on(async {
819 let mut editor = MetadataEditor::open(path).await?;
820
821 let mut map: std::collections::HashMap<String, TagValue> = editor
823 .iter()
824 .map(|(k, v)| (k.to_string(), v.clone()))
825 .collect();
826
827 let count = self.apply(&mut map)?;
828
829 editor.clear();
831 for (k, v) in &map {
832 editor.set(k, v.clone());
833 }
834
835 editor.save().await?;
836 Ok(count)
837 })
838 }
839
840 #[must_use]
842 pub fn len(&self) -> usize {
843 self.operations.len()
844 }
845
846 #[must_use]
848 pub fn is_empty(&self) -> bool {
849 self.operations.is_empty()
850 }
851}
852
853#[cfg(not(target_arch = "wasm32"))]
854pub async fn read_metadata(path: impl AsRef<Path>) -> OxiResult<TagMap> {
862 let editor = MetadataEditor::open(path).await?;
863 Ok(editor.tags)
864}
865
866#[cfg(not(target_arch = "wasm32"))]
867pub async fn write_metadata(path: impl AsRef<Path>, tags: &TagMap) -> OxiResult<()> {
876 let mut editor = MetadataEditor::open(path).await?;
877 editor.tags = tags.clone();
878 editor.modified = true;
879 editor.save().await
880}
881
882#[cfg(all(test, not(target_arch = "wasm32")))]
883mod tests {
884 use super::*;
885
886 #[test]
887 fn test_metadata_format_from_container_format() {
888 assert_eq!(
889 MetadataFormat::from(ContainerFormat::Flac),
890 MetadataFormat::Flac
891 );
892 assert_eq!(
893 MetadataFormat::from(ContainerFormat::Ogg),
894 MetadataFormat::Ogg
895 );
896 assert_eq!(
897 MetadataFormat::from(ContainerFormat::Matroska),
898 MetadataFormat::Matroska
899 );
900 assert_eq!(
901 MetadataFormat::from(ContainerFormat::WebM),
902 MetadataFormat::WebM
903 );
904 }
905
906 #[test]
907 fn test_metadata_editor_modification_tracking() {
908 let editor = MetadataEditor {
909 path: PathBuf::from("test.flac"),
910 format: MetadataFormat::Flac,
911 tags: TagMap::new(),
912 modified: false,
913 };
914
915 assert!(!editor.is_modified());
916 }
917
918 #[test]
919 fn test_metadata_editor_set() {
920 let mut editor = MetadataEditor {
921 path: PathBuf::from("test.flac"),
922 format: MetadataFormat::Flac,
923 tags: TagMap::new(),
924 modified: false,
925 };
926
927 editor.set("TITLE", "Test");
928 assert!(editor.is_modified());
929 assert_eq!(editor.get_text("TITLE"), Some("Test"));
930 }
931
932 #[test]
933 fn test_metadata_editor_add() {
934 let mut editor = MetadataEditor {
935 path: PathBuf::from("test.flac"),
936 format: MetadataFormat::Flac,
937 tags: TagMap::new(),
938 modified: false,
939 };
940
941 editor.add("ARTIST", "Artist 1");
942 editor.add("ARTIST", "Artist 2");
943
944 assert!(editor.is_modified());
945 let artists = editor.get_all("ARTIST");
946 assert_eq!(artists.len(), 2);
947 }
948
949 #[test]
950 fn test_metadata_editor_remove() {
951 let mut editor = MetadataEditor {
952 path: PathBuf::from("test.flac"),
953 format: MetadataFormat::Flac,
954 tags: TagMap::new(),
955 modified: false,
956 };
957
958 editor.set("TITLE", "Test");
959 editor.modified = false; assert!(editor.remove("TITLE"));
962 assert!(editor.is_modified());
963 assert!(!editor.remove("TITLE"));
964 }
965
966 #[test]
967 fn test_metadata_editor_clear() {
968 let mut editor = MetadataEditor {
969 path: PathBuf::from("test.flac"),
970 format: MetadataFormat::Flac,
971 tags: TagMap::new(),
972 modified: false,
973 };
974
975 editor.set("TITLE", "Test");
976 editor.set("ARTIST", "Test");
977 editor.modified = false;
978
979 editor.clear();
980 assert!(editor.is_modified());
981 assert!(editor.tags.is_empty());
982 }
983
984 #[test]
985 fn test_metadata_editor_standard_tags() {
986 let mut editor = MetadataEditor {
987 path: PathBuf::from("test.flac"),
988 format: MetadataFormat::Flac,
989 tags: TagMap::new(),
990 modified: false,
991 };
992
993 editor.set_standard(StandardTag::Title, "Test Title");
994 assert_eq!(
995 editor
996 .get_standard(StandardTag::Title)
997 .and_then(|v| v.as_text()),
998 Some("Test Title")
999 );
1000 }
1001
1002 #[test]
1003 fn test_metadata_editor_iter() {
1004 let mut editor = MetadataEditor {
1005 path: PathBuf::from("test.flac"),
1006 format: MetadataFormat::Flac,
1007 tags: TagMap::new(),
1008 modified: false,
1009 };
1010
1011 editor.set("TITLE", "Title");
1012 editor.set("ARTIST", "Artist");
1013
1014 let entries: Vec<_> = editor.iter().collect();
1015 assert_eq!(entries.len(), 2);
1016 }
1017
1018 #[test]
1019 fn test_metadata_editor_keys() {
1020 let mut editor = MetadataEditor {
1021 path: PathBuf::from("test.flac"),
1022 format: MetadataFormat::Flac,
1023 tags: TagMap::new(),
1024 modified: false,
1025 };
1026
1027 editor.set("TITLE", "Title");
1028 editor.set("ARTIST", "Artist");
1029
1030 let keys: Vec<_> = editor.keys().collect();
1031 assert_eq!(keys.len(), 2);
1032 assert!(keys.contains(&"TITLE"));
1033 assert!(keys.contains(&"ARTIST"));
1034 }
1035
1036 #[test]
1039 fn test_copy_all_from() {
1040 let mut editor = MetadataEditor {
1041 path: PathBuf::from("test.flac"),
1042 format: MetadataFormat::Flac,
1043 tags: TagMap::new(),
1044 modified: false,
1045 };
1046 editor.set("TITLE", "Original");
1047
1048 let mut source = TagMap::new();
1049 source.set("TITLE", "Copied");
1050 source.set("ARTIST", "New Artist");
1051 source.set("ALBUM", "New Album");
1052
1053 editor.copy_all_from(&source);
1054
1055 assert!(editor.is_modified());
1056 assert_eq!(editor.get_text("TITLE"), Some("Copied")); assert_eq!(editor.get_text("ARTIST"), Some("New Artist"));
1058 assert_eq!(editor.get_text("ALBUM"), Some("New Album"));
1059 }
1060
1061 #[test]
1062 fn test_copy_tags_from_selective() {
1063 let mut editor = MetadataEditor {
1064 path: PathBuf::from("test.flac"),
1065 format: MetadataFormat::Flac,
1066 tags: TagMap::new(),
1067 modified: false,
1068 };
1069
1070 let mut source = TagMap::new();
1071 source.set("TITLE", "Source Title");
1072 source.set("ARTIST", "Source Artist");
1073 source.set("ALBUM", "Source Album");
1074
1075 editor.copy_tags_from(&source, &["TITLE", "ALBUM"]);
1076
1077 assert!(editor.is_modified());
1078 assert_eq!(editor.get_text("TITLE"), Some("Source Title"));
1079 assert_eq!(editor.get_text("ALBUM"), Some("Source Album"));
1080 assert!(editor.get_text("ARTIST").is_none()); }
1082
1083 #[test]
1084 fn test_copy_tags_from_nonexistent() {
1085 let mut editor = MetadataEditor {
1086 path: PathBuf::from("test.flac"),
1087 format: MetadataFormat::Flac,
1088 tags: TagMap::new(),
1089 modified: false,
1090 };
1091
1092 let source = TagMap::new();
1093 editor.copy_tags_from(&source, &["TITLE"]);
1094
1095 assert!(!editor.is_modified()); }
1097
1098 #[test]
1099 fn test_copy_standard_tags_from() {
1100 let mut editor = MetadataEditor {
1101 path: PathBuf::from("test.flac"),
1102 format: MetadataFormat::Flac,
1103 tags: TagMap::new(),
1104 modified: false,
1105 };
1106
1107 let mut source = TagMap::new();
1108 source.set_standard(StandardTag::Title, "Std Title");
1109 source.set_standard(StandardTag::Artist, "Std Artist");
1110 source.set_standard(StandardTag::Album, "Std Album");
1111
1112 editor.copy_standard_tags_from(&source, &[StandardTag::Title, StandardTag::Album]);
1113
1114 assert!(editor.is_modified());
1115 assert_eq!(
1116 editor
1117 .get_standard(StandardTag::Title)
1118 .and_then(|v| v.as_text()),
1119 Some("Std Title")
1120 );
1121 assert_eq!(
1122 editor
1123 .get_standard(StandardTag::Album)
1124 .and_then(|v| v.as_text()),
1125 Some("Std Album")
1126 );
1127 assert!(editor.get_standard(StandardTag::Artist).is_none());
1128 }
1129
1130 #[test]
1131 fn test_apply_batch_set_and_add() {
1132 let mut editor = MetadataEditor {
1133 path: PathBuf::from("test.flac"),
1134 format: MetadataFormat::Flac,
1135 tags: TagMap::new(),
1136 modified: false,
1137 };
1138
1139 let ops = vec![
1140 BatchTagOperation::set("TITLE", "Batch Title"),
1141 BatchTagOperation::set("ARTIST", "Main Artist"),
1142 BatchTagOperation::add("ARTIST", "Featured Artist"),
1143 ];
1144
1145 editor.apply_batch(&ops);
1146
1147 assert!(editor.is_modified());
1148 assert_eq!(editor.get_text("TITLE"), Some("Batch Title"));
1149 assert_eq!(editor.get_all("ARTIST").len(), 2);
1150 }
1151
1152 #[test]
1153 fn test_apply_batch_remove() {
1154 let mut editor = MetadataEditor {
1155 path: PathBuf::from("test.flac"),
1156 format: MetadataFormat::Flac,
1157 tags: TagMap::new(),
1158 modified: false,
1159 };
1160
1161 editor.set("TITLE", "Test");
1162 editor.set("ARTIST", "Test");
1163 editor.modified = false;
1164
1165 let ops = vec![BatchTagOperation::remove("TITLE")];
1166 editor.apply_batch(&ops);
1167
1168 assert!(editor.is_modified());
1169 assert!(editor.get_text("TITLE").is_none());
1170 assert_eq!(editor.get_text("ARTIST"), Some("Test")); }
1172
1173 #[test]
1174 fn test_apply_batch_rename() {
1175 let mut editor = MetadataEditor {
1176 path: PathBuf::from("test.flac"),
1177 format: MetadataFormat::Flac,
1178 tags: TagMap::new(),
1179 modified: false,
1180 };
1181
1182 editor.set("COMMENT", "My comment");
1183 editor.modified = false;
1184
1185 let ops = vec![BatchTagOperation::rename("COMMENT", "DESCRIPTION")];
1186 editor.apply_batch(&ops);
1187
1188 assert!(editor.is_modified());
1189 assert!(editor.get_text("COMMENT").is_none());
1190 assert_eq!(editor.get_text("DESCRIPTION"), Some("My comment"));
1191 }
1192
1193 #[test]
1194 fn test_apply_batch_set_standard() {
1195 let mut editor = MetadataEditor {
1196 path: PathBuf::from("test.flac"),
1197 format: MetadataFormat::Flac,
1198 tags: TagMap::new(),
1199 modified: false,
1200 };
1201
1202 let ops = vec![
1203 BatchTagOperation::set_standard(StandardTag::Title, "Std Batch"),
1204 BatchTagOperation::set_standard(StandardTag::Genre, "Rock"),
1205 ];
1206
1207 editor.apply_batch(&ops);
1208
1209 assert!(editor.is_modified());
1210 assert_eq!(
1211 editor
1212 .get_standard(StandardTag::Title)
1213 .and_then(|v| v.as_text()),
1214 Some("Std Batch")
1215 );
1216 assert_eq!(
1217 editor
1218 .get_standard(StandardTag::Genre)
1219 .and_then(|v| v.as_text()),
1220 Some("Rock")
1221 );
1222 }
1223
1224 #[test]
1225 fn test_apply_batch_remove_all() {
1226 let mut editor = MetadataEditor {
1227 path: PathBuf::from("test.flac"),
1228 format: MetadataFormat::Flac,
1229 tags: TagMap::new(),
1230 modified: false,
1231 };
1232
1233 editor.set("TITLE", "Title");
1234 editor.set("ARTIST", "Artist");
1235 editor.modified = false;
1236
1237 let ops = vec![BatchTagOperation::remove_all()];
1238 editor.apply_batch(&ops);
1239
1240 assert!(editor.is_modified());
1241 assert!(editor.tags().is_empty());
1242 }
1243
1244 #[test]
1245 fn test_apply_batch_replace_value() {
1246 let mut editor = MetadataEditor {
1247 path: PathBuf::from("test.flac"),
1248 format: MetadataFormat::Flac,
1249 tags: TagMap::new(),
1250 modified: false,
1251 };
1252
1253 editor.add("ARTIST", "Old Artist");
1254 editor.add("ARTIST", "Keep This");
1255 editor.modified = false;
1256
1257 let ops = vec![BatchTagOperation::replace_value(
1258 "ARTIST",
1259 "Old Artist",
1260 "New Artist",
1261 )];
1262 editor.apply_batch(&ops);
1263
1264 assert!(editor.is_modified());
1265 let artists = editor.get_all("ARTIST");
1266 assert_eq!(artists.len(), 2);
1267 let texts: Vec<_> = artists.iter().filter_map(|v| v.as_text()).collect();
1269 assert!(texts.contains(&"New Artist"));
1270 assert!(texts.contains(&"Keep This"));
1271 }
1272
1273 #[test]
1274 fn test_apply_batch_prefix_values() {
1275 let mut editor = MetadataEditor {
1276 path: PathBuf::from("test.flac"),
1277 format: MetadataFormat::Flac,
1278 tags: TagMap::new(),
1279 modified: false,
1280 };
1281
1282 editor.add("GENRE", "Rock");
1283 editor.add("GENRE", "Metal");
1284 editor.modified = false;
1285
1286 let ops = vec![BatchTagOperation::prefix_values("GENRE", "Heavy ")];
1287 editor.apply_batch(&ops);
1288
1289 assert!(editor.is_modified());
1290 let genres = editor.get_all("GENRE");
1291 let texts: Vec<_> = genres.iter().filter_map(|v| v.as_text()).collect();
1292 assert!(texts.contains(&"Heavy Rock"));
1293 assert!(texts.contains(&"Heavy Metal"));
1294 }
1295
1296 #[test]
1297 fn test_apply_batch_no_change() {
1298 let mut editor = MetadataEditor {
1299 path: PathBuf::from("test.flac"),
1300 format: MetadataFormat::Flac,
1301 tags: TagMap::new(),
1302 modified: false,
1303 };
1304
1305 let ops = vec![BatchTagOperation::remove("NONEXISTENT")];
1307 editor.apply_batch(&ops);
1308
1309 assert!(!editor.is_modified());
1310 }
1311
1312 #[test]
1313 fn test_apply_batch_complex_workflow() {
1314 let mut editor = MetadataEditor {
1315 path: PathBuf::from("test.flac"),
1316 format: MetadataFormat::Flac,
1317 tags: TagMap::new(),
1318 modified: false,
1319 };
1320
1321 let ops = vec![
1323 BatchTagOperation::set("TITLE", "My Song"),
1324 BatchTagOperation::set("ARTIST", "Band Name"),
1325 BatchTagOperation::set("ALBUM", "Album Title"),
1326 BatchTagOperation::set("DATE", "2024"),
1327 BatchTagOperation::set_standard(StandardTag::Genre, "Alternative"),
1328 BatchTagOperation::set("TRACKNUMBER", "5"),
1329 BatchTagOperation::set("TOTALTRACKS", "12"),
1330 ];
1331
1332 editor.apply_batch(&ops);
1333
1334 assert!(editor.is_modified());
1335 assert_eq!(editor.get_text("TITLE"), Some("My Song"));
1336 assert_eq!(editor.get_text("ARTIST"), Some("Band Name"));
1337 assert_eq!(editor.get_text("ALBUM"), Some("Album Title"));
1338 assert_eq!(editor.get_text("DATE"), Some("2024"));
1339 assert_eq!(editor.get_text("TRACKNUMBER"), Some("5"));
1340 }
1341
1342 #[test]
1345 fn test_diff_added() {
1346 let mut editor = MetadataEditor {
1347 path: PathBuf::from("test.flac"),
1348 format: MetadataFormat::Flac,
1349 tags: TagMap::new(),
1350 modified: false,
1351 };
1352 editor.set("TITLE", "New");
1353
1354 let other = TagMap::new();
1355 let diffs = editor.diff(&other);
1356
1357 assert!(!diffs.is_empty());
1358 assert!(diffs.iter().any(|d| d.is_added() && d.key() == "TITLE"));
1359 }
1360
1361 #[test]
1362 fn test_diff_removed() {
1363 let editor = MetadataEditor {
1364 path: PathBuf::from("test.flac"),
1365 format: MetadataFormat::Flac,
1366 tags: TagMap::new(),
1367 modified: false,
1368 };
1369
1370 let mut other = TagMap::new();
1371 other.set("TITLE", "Old");
1372
1373 let diffs = editor.diff(&other);
1374 assert!(diffs.iter().any(|d| d.is_removed() && d.key() == "TITLE"));
1375 }
1376
1377 #[test]
1378 fn test_diff_modified() {
1379 let mut editor = MetadataEditor {
1380 path: PathBuf::from("test.flac"),
1381 format: MetadataFormat::Flac,
1382 tags: TagMap::new(),
1383 modified: false,
1384 };
1385 editor.set("TITLE", "New");
1386
1387 let mut other = TagMap::new();
1388 other.set("TITLE", "Old");
1389
1390 let diffs = editor.diff(&other);
1391 assert!(diffs.iter().any(|d| d.is_modified() && d.key() == "TITLE"));
1392 }
1393
1394 #[test]
1395 fn test_diff_no_changes() {
1396 let mut editor = MetadataEditor {
1397 path: PathBuf::from("test.flac"),
1398 format: MetadataFormat::Flac,
1399 tags: TagMap::new(),
1400 modified: false,
1401 };
1402 editor.set("TITLE", "Same");
1403
1404 let mut other = TagMap::new();
1405 other.set("TITLE", "Same");
1406
1407 let diffs = editor.diff(&other);
1408 assert!(diffs.is_empty());
1409 }
1410
1411 #[test]
1412 fn test_tag_diff_methods() {
1413 let added = TagDiff::Added {
1414 key: "TITLE".to_string(),
1415 value: TagValue::Text("Test".to_string()),
1416 };
1417 assert!(added.is_added());
1418 assert!(!added.is_removed());
1419 assert!(!added.is_modified());
1420 assert_eq!(added.key(), "TITLE");
1421
1422 let removed = TagDiff::Removed {
1423 key: "ARTIST".to_string(),
1424 };
1425 assert!(removed.is_removed());
1426
1427 let modified = TagDiff::Modified {
1428 key: "ALBUM".to_string(),
1429 old_value: TagValue::Text("Old".to_string()),
1430 new_value: TagValue::Text("New".to_string()),
1431 };
1432 assert!(modified.is_modified());
1433 }
1434
1435 #[test]
1438 fn test_batch_op_constructors() {
1439 let _set = BatchTagOperation::set("TITLE", "Test");
1440 let _add = BatchTagOperation::add("ARTIST", "Test");
1441 let _remove = BatchTagOperation::remove("COMMENT");
1442 let _rename = BatchTagOperation::rename("OLD", "NEW");
1443 let _std = BatchTagOperation::set_standard(StandardTag::Title, "Test");
1444 let _clear = BatchTagOperation::remove_all();
1445 let _replace = BatchTagOperation::replace_value("ARTIST", "Old", "New");
1446 let _prefix = BatchTagOperation::prefix_values("GENRE", "Classic ");
1447 }
1448
1449 #[test]
1452 fn test_batch_metadata_editor_set() {
1453 let mut map: std::collections::HashMap<String, TagValue> = std::collections::HashMap::new();
1454 let count = BatchMetadataEditor::new()
1455 .set("TITLE", TagValue::Text("Hello".to_string()))
1456 .set("ARTIST", TagValue::Text("World".to_string()))
1457 .apply(&mut map)
1458 .expect("apply failed");
1459 assert_eq!(count, 2);
1460 assert_eq!(map.get("TITLE").and_then(|v| v.as_text()), Some("Hello"));
1461 assert_eq!(map.get("ARTIST").and_then(|v| v.as_text()), Some("World"));
1462 }
1463
1464 #[test]
1465 fn test_batch_metadata_editor_remove() {
1466 let mut map: std::collections::HashMap<String, TagValue> = std::collections::HashMap::new();
1467 map.insert("COMMENT".to_string(), TagValue::Text("old".to_string()));
1468 let count = BatchMetadataEditor::new()
1469 .remove("COMMENT")
1470 .apply(&mut map)
1471 .expect("apply failed");
1472 assert_eq!(count, 1);
1473 assert!(!map.contains_key("COMMENT"));
1474 }
1475
1476 #[test]
1477 fn test_batch_metadata_editor_remove_absent() {
1478 let mut map: std::collections::HashMap<String, TagValue> = std::collections::HashMap::new();
1479 let count = BatchMetadataEditor::new()
1480 .remove("NONEXISTENT")
1481 .apply(&mut map)
1482 .expect("apply failed");
1483 assert_eq!(count, 0);
1484 }
1485
1486 #[test]
1487 fn test_batch_metadata_editor_rename() {
1488 let mut map: std::collections::HashMap<String, TagValue> = std::collections::HashMap::new();
1489 map.insert("OLD_KEY".to_string(), TagValue::Text("value".to_string()));
1490 let count = BatchMetadataEditor::new()
1491 .rename("OLD_KEY", "NEW_KEY")
1492 .apply(&mut map)
1493 .expect("apply failed");
1494 assert_eq!(count, 1);
1495 assert!(!map.contains_key("OLD_KEY"));
1496 assert_eq!(map.get("NEW_KEY").and_then(|v| v.as_text()), Some("value"));
1497 }
1498
1499 #[test]
1500 fn test_batch_metadata_editor_rename_absent() {
1501 let mut map: std::collections::HashMap<String, TagValue> = std::collections::HashMap::new();
1502 let count = BatchMetadataEditor::new()
1503 .rename("MISSING", "TARGET")
1504 .apply(&mut map)
1505 .expect("apply failed");
1506 assert_eq!(count, 0);
1507 assert!(!map.contains_key("TARGET"));
1508 }
1509
1510 #[test]
1511 fn test_batch_metadata_editor_set_if_absent_missing() {
1512 let mut map: std::collections::HashMap<String, TagValue> = std::collections::HashMap::new();
1513 let count = BatchMetadataEditor::new()
1514 .set_if_absent("TITLE", TagValue::Text("Default".to_string()))
1515 .apply(&mut map)
1516 .expect("apply failed");
1517 assert_eq!(count, 1);
1518 assert_eq!(map.get("TITLE").and_then(|v| v.as_text()), Some("Default"));
1519 }
1520
1521 #[test]
1522 fn test_batch_metadata_editor_set_if_absent_present() {
1523 let mut map: std::collections::HashMap<String, TagValue> = std::collections::HashMap::new();
1524 map.insert("TITLE".to_string(), TagValue::Text("Existing".to_string()));
1525 let count = BatchMetadataEditor::new()
1526 .set_if_absent("TITLE", TagValue::Text("Default".to_string()))
1527 .apply(&mut map)
1528 .expect("apply failed");
1529 assert_eq!(count, 0);
1530 assert_eq!(map.get("TITLE").and_then(|v| v.as_text()), Some("Existing"));
1531 }
1532
1533 #[test]
1534 fn test_batch_metadata_editor_set_same_value_no_count() {
1535 let mut map: std::collections::HashMap<String, TagValue> = std::collections::HashMap::new();
1536 map.insert("TITLE".to_string(), TagValue::Text("Same".to_string()));
1537 let count = BatchMetadataEditor::new()
1538 .set("TITLE", TagValue::Text("Same".to_string()))
1539 .apply(&mut map)
1540 .expect("apply failed");
1541 assert_eq!(count, 0);
1542 }
1543
1544 #[test]
1545 fn test_batch_metadata_editor_combined() {
1546 let mut map: std::collections::HashMap<String, TagValue> = std::collections::HashMap::new();
1547 map.insert("TITLE".to_string(), TagValue::Text("Old".to_string()));
1548 map.insert("DELETE_ME".to_string(), TagValue::Text("bye".to_string()));
1549
1550 let count = BatchMetadataEditor::new()
1551 .set("TITLE", TagValue::Text("New".to_string()))
1552 .remove("DELETE_ME")
1553 .set_if_absent("ARTIST", TagValue::Text("Unknown".to_string()))
1554 .apply(&mut map)
1555 .expect("apply failed");
1556
1557 assert_eq!(count, 3);
1558 assert_eq!(map.get("TITLE").and_then(|v| v.as_text()), Some("New"));
1559 assert!(!map.contains_key("DELETE_ME"));
1560 assert_eq!(map.get("ARTIST").and_then(|v| v.as_text()), Some("Unknown"));
1561 }
1562
1563 #[test]
1564 fn test_batch_metadata_editor_empty() {
1565 let mut map: std::collections::HashMap<String, TagValue> = std::collections::HashMap::new();
1566 let editor = BatchMetadataEditor::new();
1567 assert!(editor.is_empty());
1568 assert_eq!(editor.len(), 0);
1569 let count = editor.apply(&mut map).expect("apply failed");
1570 assert_eq!(count, 0);
1571 }
1572
1573 #[test]
1574 fn test_batch_metadata_editor_len() {
1575 let editor = BatchMetadataEditor::new()
1576 .set("A", TagValue::Text("1".to_string()))
1577 .remove("B")
1578 .rename("C", "D")
1579 .set_if_absent("E", TagValue::Text("5".to_string()));
1580 assert_eq!(editor.len(), 4);
1581 assert!(!editor.is_empty());
1582 }
1583}