1use std::env;
2use std::fmt;
3use std::fs;
4use std::io::{self};
5use std::path::PathBuf;
6
7pub struct AudioFile {
8 path: PathBuf,
9 file: taglib::File,
10}
11
12pub enum FileError {
13 NotAFile,
14 TaglibError(taglib::FileError),
15}
16
17impl From<taglib::FileError> for FileError {
18 fn from(error: taglib::FileError) -> Self {
19 FileError::TaglibError(error)
20 }
21}
22
23impl AudioFile {
24 pub fn new(path: PathBuf) -> Result<Self, FileError> {
25 let is_file = path
26 .metadata()
27 .map(|metadata| metadata.is_file())
28 .unwrap_or(false);
29
30 if !is_file {
31 return Err(FileError::NotAFile);
32 }
33
34 let file = taglib::File::new(&path)?;
35
36 Ok(AudioFile { path, file })
37 }
38
39 pub fn collection_to_editable_content(audio_files: &[AudioFile]) -> String {
40 let lines_with_tags: String = audio_files
41 .iter()
42 .map(AudioFile::to_editable_content_line)
43 .collect();
44
45 format!(
46 "{}\n{}",
47 "Title\tArtist\t(Remove all lines to abort the update)", lines_with_tags
48 )
49 }
50
51 pub fn from_current_dir() -> Result<Vec<AudioFile>, io::Error> {
52 let dir = env::current_dir()?;
53 let entries: Result<Vec<_>, _> = fs::read_dir(dir)?.collect();
54
55 Ok(entries?
56 .iter()
57 .filter_map(|entry| AudioFile::new(entry.path()).ok())
58 .collect())
59 }
60
61 pub fn update_tags_from_edited_content(
62 audio_files: &mut [AudioFile],
63 content: String,
64 ) -> Result<(), UpdateError> {
65 if content.trim().is_empty() {
66 return Err(UpdateError::UpdateAborted);
67 }
68
69 let number_of_lines = content.lines().count() - 1; if audio_files.len() != number_of_lines {
72 return Err(UpdateError::LineNumberMismatch {
73 number_of_files: audio_files.len(),
74 number_of_lines,
75 });
76 }
77
78 for (audio_file, line) in audio_files.iter_mut().zip(
79 content.lines().skip(1),
81 ) {
82 audio_file.update_tag_from_line(line)?
83 }
84
85 Ok(())
86 }
87
88 pub fn to_editable_content_line(&self) -> String {
89 let tag = self
90 .file
91 .tag()
92 .expect("Failed to get tag inside to_editable_content_line");
93
94 format!(
95 "{}\t{}\n",
96 tag.title().unwrap_or_default(),
97 tag.artist().unwrap_or_default()
98 )
99 }
100
101 pub fn update_tag_from_line(&mut self, line: &str) -> Result<(), UpdateError> {
102 let mut tag = match self.file.tag() {
103 Ok(tag) => tag,
104 Err(e) => return Err(UpdateError::CannotReadTag(e)),
105 };
106
107 if let [title, artist] = line.split('\t').collect::<Vec<_>>()[..] {
108 tag.set_title(title);
109 tag.set_artist(artist);
110
111 if self.file.save() {
112 Ok(())
113 } else {
114 Err(UpdateError::SaveFailed(self.path.clone()))
115 }
116 } else {
117 Err(UpdateError::InvalidLine(line.to_string()))
118 }
119 }
120}
121
122#[derive(Debug)]
123pub enum UpdateError {
124 UpdateAborted,
125 LineNumberMismatch {
126 number_of_files: usize,
127 number_of_lines: usize,
128 },
129 InvalidLine(String),
130 CannotReadTag(taglib::FileError),
131 SaveFailed(PathBuf),
132}
133
134impl fmt::Display for UpdateError {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 use UpdateError::*;
137
138 match self {
139 UpdateAborted => write!(f, "Update aborted"),
140 LineNumberMismatch {
141 number_of_files,
142 number_of_lines,
143 } => write!(
144 f,
145 "Found {}, so expected {}, but found only {}",
146 pluralize(*number_of_files, "file", "files"),
147 pluralize(*number_of_files, "line", "lines"),
148 pluralize(*number_of_lines, "line", "lines"),
149 ),
150 InvalidLine(invalid_line) => write!(
151 f,
152 "Expected the line to have format\n\n\t`title<TAB>artist`\n\nbut found this instead:\n\n\t{}",
153 invalid_line.replace('\t', "<TAB>")
154 ),
155 SaveFailed(path) => write!(f, "Failed to save updated tags to file {:?}", path.clone().into_os_string()),
156 CannotReadTag(error) => write!(f, "Failed to read tags from the file {:?}", error)
157 }
158 }
159}
160
161fn pluralize(count: usize, singular: &str, plural: &str) -> String {
162 format!("{} {}", count, if count == 1 { singular } else { plural })
163}