1use std::{
2 collections::HashMap,
3 fs,
4 io::{self},
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use barber::{ProgressBar, ProgressRenderer};
10use lofty::{config::WriteOptions, tag::TagExt};
11use lunar_lib::{
12 database::{DatabaseEntry, DbHandle, TransactionError},
13 error,
14 formatter::FormatTable,
15};
16use thiserror::Error;
17
18use crate::{
19 config::ExportConfig,
20 database::{LibraryDb, Resolveable},
21 library::{
22 album::{Album, ResolvedAlbum},
23 artist::add_from_artists,
24 track::{ResolvedTrack, Track, lyric_data::LyricData},
25 },
26};
27
28#[derive(Debug, Error)]
29pub enum ExportError {
30 #[error("IoError: {0}")]
31 Io(#[from] std::io::Error),
32
33 #[error("LoftyError: {0}")]
34 Lofty(#[from] lofty::error::LoftyError),
35
36 #[error("Transaction Error: {0}")]
37 Transaction(#[from] TransactionError),
38}
39
40pub fn export_library(
41 export_dir: impl AsRef<Path>,
42 export_config: ExportConfig,
43 progress_renderer: Arc<dyn ProgressRenderer>,
44) -> Result<(), ExportError> {
45 let db = DbHandle::<LibraryDb>::open().unwrap();
46 let tracks = Track::db_get_all(&db)?;
47
48 let mut conflicting_path_check: HashMap<PathBuf, usize> = HashMap::new();
49
50 let mut track_export_targets = Vec::with_capacity(tracks.len());
51 for track in tracks {
52 let track = Track::resolve(Arc::new(track), &db)?;
53 let export_path = {
54 let mut format_table = FormatTable::new();
55 format_table.extend_from_taggable(&track.metadata);
56
57 add_from_artists(
58 &mut format_table,
59 track.artists().iter().map(|a| &**a),
60 "track",
61 &export_config.artist_separator,
62 &export_config.alt_artist_separator,
63 );
64
65 if let Some((album, album_artists, track_num, disc_num)) = track.album_info() {
66 format_table.extend_from_taggable(&**album);
67 add_from_artists(
68 &mut format_table,
69 album_artists.iter().map(|a| &**a),
70 "album",
71 &export_config.artist_separator,
72 &export_config.alt_artist_separator,
73 );
74 if let Some(track_num) = track_num {
75 format_table.add_entry("track_num", track_num.to_string());
76 }
77 if let Some(disc_num) = disc_num {
78 format_table.add_entry("disc_num", disc_num.to_string());
79 }
80 }
81
82 let path = format!(
83 "{path}.{ext}",
84 path = format_table.render(export_config.file_name_format.as_arguments()),
85 ext = track.container().extension()
86 );
87 export_dir.as_ref().join(path)
88 };
89
90 if !export_config.overwrite && export_path.exists() {
91 continue;
92 }
93
94 *conflicting_path_check
95 .entry(export_path.clone())
96 .or_default() += 1;
97 track_export_targets.push((track, export_path));
98 }
99
100 let album_export_targets =
101 if let Some(album_data_path_format) = export_config.album_data_path.as_ref() {
102 let albums = Album::db_get_all(&db)?;
103 let mut album_export_targets = Vec::with_capacity(albums.len());
104 for album in albums {
105 let album = Album::resolve(Arc::new(album), &db)?;
106 let export_dir = {
107 let mut format_table = FormatTable::new();
108 format_table.extend_from_taggable(&*album);
109 add_from_artists(
110 &mut format_table,
111 album.artists.iter().map(|a| &**a),
112 "album",
113 &export_config.artist_separator,
114 &export_config.alt_artist_separator,
115 );
116
117 let path = format_table.render(album_data_path_format.as_arguments());
118 export_dir.as_ref().join(path)
119 };
120
121 *conflicting_path_check
122 .entry(export_dir.clone())
123 .or_default() += 1;
124 album_export_targets.push((album, export_dir));
125 }
126 album_export_targets
127 } else {
128 Vec::new()
129 };
130
131 for (check, count) in conflicting_path_check {
132 if count != 1 {
133 error!("Conflicting path during export: {}", check.display());
134 return Ok(());
135 }
136 }
137
138 let progress_bar = ProgressBar::new(
139 0,
140 track_export_targets.len() + album_export_targets.len(),
141 progress_renderer,
142 );
143
144 for (album, dir) in album_export_targets {
145 export_album(&album, &export_config, dir)?;
146 progress_bar.set_label(&format!("Exported data for album '{}'", album.name()));
147 progress_bar.increment();
148 }
149
150 for (track, path) in track_export_targets {
151 export_track(&track, &export_config, path)?;
152 progress_bar.set_label(&format!(
153 "Exported data for track '{}'",
154 track.metadata().safe_title()
155 ));
156 progress_bar.increment();
157 }
158
159 Ok(())
160}
161
162pub fn export_album(
163 album: &ResolvedAlbum,
164 export_config: &ExportConfig,
165 export_dir: impl AsRef<Path>,
166) -> Result<(), ExportError> {
167 let mut format_table = FormatTable::new();
168 format_table.extend_from_taggable(&**album);
169 add_from_artists(
170 &mut format_table,
171 album.artists.iter().map(|a| &**a),
172 "album",
173 &export_config.artist_separator,
174 &export_config.alt_artist_separator,
175 );
176
177 if let Some(image_art) = album.art.as_ref()
179 && export_config.album_data_path.is_some()
180 {
181 fs::create_dir_all(&export_dir)?;
182
183 let ext = infer::get_from_path(image_art.source())?
184 .map(|t| format!(".{}", t.extension()))
185 .unwrap_or_default();
186
187 let path = format!("cover{ext}");
188
189 let cover_art_path = export_dir.as_ref().join(path);
190 if export_config.overwrite || !cover_art_path.exists() {
191 fs::copy(image_art.source(), cover_art_path)?;
192 }
193 }
194
195 Ok(())
196}
197
198pub fn export_track(
199 track: &ResolvedTrack,
200 export_config: &ExportConfig,
201 export_path: impl Into<PathBuf>,
202) -> Result<(), ExportError> {
203 let export_path = export_path.into();
204
205 let buf = fs::read(track.container().path())?;
206 let mut cursor = io::Cursor::new(buf);
207
208 let mut tag = track.metadata_key_values(export_config);
209 if let Some(cover_art) = track.metadata.art.as_ref() {
210 tag.set_picture(0, cover_art.to_picture()?);
211 }
212
213 let options = WriteOptions::default().remove_others(true);
214 tag.save_to(&mut cursor, options).unwrap();
215 fs::create_dir_all(export_path.parent().unwrap())?;
216 fs::write(&export_path, cursor.into_inner())?;
217
218 if let Some(lyric_data) = track.metadata.lyric_data.as_ref() {
219 match lyric_data {
220 LyricData::Plain(plain_lyrics) if export_config.plain_lyrics_as_txt => {
221 let mut lrc_path = export_path.clone();
222 lrc_path.set_extension("txt");
223 fs::write(lrc_path, plain_lyrics.as_str())?;
224 }
225 LyricData::Synced(synced_lyrics) => {
226 if let Some((data, ext)) = export_config
227 .export_synced_lyrics_as
228 .map(|format| (synced_lyrics.to_lyrics(format), format.extension()))
229 {
230 let mut lrc_path = export_path.clone();
231 lrc_path.set_extension(ext);
232
233 fs::write(lrc_path, data)?;
234 }
235 }
236 _ => (),
237 }
238 }
239
240 Ok(())
241}