Skip to main content

selene_core/library/
export.rs

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    // Cover art export
178    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}