file_io.rs (4979B)
1 use anyhow::Result; 2 use lofty::config::WriteOptions; 3 use lofty::prelude::*; 4 use lofty::probe::Probe; 5 use lofty::tag::{ItemKey, Tag}; 6 use std::path::Path; 7 8 use crate::audio_processing::{calculate_bpm, calculate_key}; 9 use crate::types::TrackInfo; 10 use bliss_audio::decoder::Decoder as DecoderTrait; 11 use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder; 12 13 #[derive(Debug, Default)] 14 pub struct FileMetadata { 15 pub artist: Option<String>, 16 pub track: Option<String>, 17 } 18 19 fn build_track_info(path: &Path, metadata: FileMetadata, bpm: f32, key: String) -> TrackInfo { 20 TrackInfo { 21 path: path.to_path_buf(), 22 artist: metadata.artist, 23 track: metadata.track, 24 bpm, 25 key, 26 } 27 } 28 29 /// Process a single file, detecting BPM and key. 30 /// 31 /// # Errors 32 /// 33 /// Returns an error string if the file cannot be decoded or key detection fails. 34 pub fn process_file(path: &Path, write_tags: bool, force: bool) -> Result<TrackInfo, String> { 35 let (metadata, existing_tags) = read_tags(path); 36 37 if !force && let Some((bpm, key)) = existing_tags { 38 println!( 39 "File: {}, BPM: {:.1}, Key: {} (existing tags)", 40 path.display(), 41 bpm, 42 key 43 ); 44 return Ok(build_track_info(path, metadata, bpm, key)); 45 } 46 47 let song = Decoder::decode(path).map_err(|e| format!("Error decoding: {e}"))?; 48 let samples = &song.sample_array; 49 50 let bpm = calculate_bpm(samples); 51 let key = calculate_key(samples).map_err(|e| format!("Error calculating key: {e}"))?; 52 53 println!("File: {}, BPM: {bpm:.1}, Key: {key}", path.display()); 54 55 if write_tags && let Err(e) = write_metadata(path, bpm, &key) { 56 eprintln!("Error writing metadata for {}: {e}", path.display()); 57 } 58 59 Ok(build_track_info(path, metadata, bpm, key)) 60 } 61 62 fn read_tags(path: &Path) -> (FileMetadata, Option<(f32, String)>) { 63 match Probe::open(path).and_then(Probe::read) { 64 Ok(tagged_file) => { 65 let tag = tagged_file 66 .primary_tag() 67 .or_else(|| tagged_file.first_tag()); 68 let Some(t) = tag else { 69 return (FileMetadata::default(), None); 70 }; 71 72 let metadata = FileMetadata { 73 artist: t 74 .get_string(ItemKey::TrackArtist) 75 .or_else(|| t.get_string(ItemKey::AlbumArtist)) 76 .map(String::from), 77 track: t.get_string(ItemKey::TrackTitle).map(String::from), 78 }; 79 80 let existing_tags = t 81 .get_string(ItemKey::Bpm) 82 .zip(t.get_string(ItemKey::InitialKey)) 83 .and_then(|(bpm_str, key)| { 84 bpm_str 85 .parse::<f32>() 86 .ok() 87 .map(|bpm| (bpm, key.to_string())) 88 }); 89 90 (metadata, existing_tags) 91 } 92 Err(_) => (FileMetadata::default(), None), 93 } 94 } 95 96 /// Write BPM and key metadata to a file. 97 /// 98 /// # Errors 99 /// 100 /// Returns an error if the file cannot be opened, read, or written. 101 /// 102 /// # Panics 103 /// 104 /// Panics if there are no tags and a new tag cannot be created. 105 pub fn write_metadata(path: &Path, bpm: f32, key: &str) -> Result<()> { 106 let mut tagged_file = Probe::open(path)?.read()?; 107 let tag = match tagged_file.primary_tag_mut() { 108 Some(primary_tag) => primary_tag, 109 None => { 110 if let Some(first_tag) = tagged_file.first_tag_mut() { 111 first_tag 112 } else { 113 let tag_type = tagged_file.primary_tag_type(); 114 tagged_file.insert_tag(Tag::new(tag_type)); 115 tagged_file 116 .primary_tag_mut() 117 .expect("Failed to create new tag") 118 } 119 } 120 }; 121 122 tag.insert_text(ItemKey::Bpm, bpm.round().to_string()); 123 tag.insert_text(ItemKey::InitialKey, key.to_string()); 124 if let Some(tkey) = ItemKey::from_key(tag.tag_type(), "TKEY") { 125 tag.insert_text(tkey, key.to_string()); 126 } 127 128 tag.save_to_path(path, WriteOptions::default())?; 129 Ok(()) 130 } 131 132 #[cfg(test)] 133 mod tests { 134 use super::*; 135 136 #[test] 137 fn build_track_info_carries_metadata_and_analysis() { 138 let metadata = FileMetadata { 139 artist: Some("Aphex Twin".to_string()), 140 track: Some("Xtal".to_string()), 141 }; 142 let info = build_track_info(Path::new("/music/xtal.flac"), metadata, 120.5, "A Minor".to_string()); 143 144 assert_eq!(info.path, Path::new("/music/xtal.flac")); 145 assert_eq!(info.artist.as_deref(), Some("Aphex Twin")); 146 assert_eq!(info.track.as_deref(), Some("Xtal")); 147 assert!((info.bpm - 120.5).abs() < f32::EPSILON); 148 assert_eq!(info.key, "A Minor"); 149 } 150 151 #[test] 152 fn file_metadata_defaults_to_none() { 153 let md = FileMetadata::default(); 154 assert!(md.artist.is_none()); 155 assert!(md.track.is_none()); 156 } 157 }