selene_core/library/
import.rs1use std::{path::Path, sync::Arc};
2
3use barber::{ProgressBar, ProgressRenderer};
4use rayon::{
5 ThreadPoolBuilder,
6 iter::{IntoParallelIterator, ParallelIterator},
7};
8
9use crate::{
10 config::common::common_config,
11 database::{DatabaseEntry, DatabaseError, writer::db_sync_transaction},
12 errors::{FfmpegError, ImportError, LibraryError},
13 ffmpeg::{command_mutators::FfmpegPresets, ffmpeg, loudnorm::LoudnormAnalysis, output_ffmpeg},
14 library::{album::Album, artist::Artist, track::Track},
15 media_container::MediaContainer,
16};
17
18#[derive(Debug, Clone)]
19pub struct ExtractResult {
20 pub track: Track,
21 pub album: Option<Album>,
22 pub artists: Vec<Artist>,
23}
24
25#[derive(Debug, Clone, Copy)]
27pub struct TrackImportOptions {
28 pub dry: bool,
29 pub apply_loudnorm: bool,
30}
31
32pub fn scan_for_reimport(loudnorm: bool) -> Result<Vec<Track>, DatabaseError> {
38 let mut tracks = Track::db_get_all()?;
39 let loudnorm_with = loudnorm.then_some(common_config().loudnorm_config);
40
41 tracks.retain(|track| {
42 let missing_lib = track.lib_container().is_none_or(|c| !c.path().is_file());
43 let needs_loudnorm = loudnorm_with.is_some_and(|cfg| track.loudnorm() != Some(&cfg));
44
45 missing_lib || needs_loudnorm
46 });
47
48 Ok(tracks)
49}
50
51pub fn reimport(
57 tracks: Vec<Track>,
58 progress_renderer: Arc<dyn ProgressRenderer>,
59 options: TrackImportOptions,
60) -> Result<(), ImportError> {
61 if tracks.is_empty() {
62 return Ok(());
63 }
64
65 let library_dir = common_config()
66 .library_dir()
67 .ok_or(LibraryError::NoLibrary)?
68 .to_path_buf();
69
70 for track in &tracks {
71 track.ensure_parent_dirs(&library_dir)?;
72 }
73
74 let progress_bar = ProgressBar::new(0, tracks.len(), progress_renderer);
75 progress_bar.set_label("Importing files...");
76
77 let max_threads = (rayon::current_num_threads() as f32 * 0.33).ceil() as usize;
78
79 let thread_pool = ThreadPoolBuilder::new()
80 .num_threads(max_threads)
81 .build()
82 .unwrap();
83
84 thread_pool.install(|| {
85 tracks
86 .into_par_iter()
87 .try_for_each(|track| -> Result<(), DatabaseError> {
88 let import_to = library_dir.join(&track.relative_library_path);
89 let track_path = track.relative_library_path.display().to_string();
90
91 if let Err(err) = import(track, import_to, options) {
92 progress_bar.set_label(&format!("Failed to import {track_path}: {err}",));
93 } else {
94 progress_bar.set_label(&format!("Imported '{track_path}'",));
95 }
96
97 progress_bar.increment();
98 Ok(())
99 })
100 })?;
101
102 progress_bar.flush();
103 Ok(())
104}
105
106pub fn import(
107 mut track: Track,
108 to: impl AsRef<Path>,
109 options: TrackImportOptions,
110) -> Result<(), ImportError> {
111 let audio_source = track.src_container().path();
112
113 let (container, codec) = track
114 .src_container()
115 .transcode_to()
116 .ok_or(FfmpegError::Other(
117 "Cannot transcode the input container: Unsupported or unrecognized container/codec pair"
118 .to_owned(),
119 ))?;
120
121 let container = container.format_name();
122 let codec = codec.codec_name();
123
124 let mut command = ffmpeg();
125
126 command.input_file(audio_source);
127
128 if let Some(cover) = &track.metadata.cover_art {
129 if cover.source() == audio_source {
130 command.cover_art_from(0);
131 } else {
132 command.input_file(cover.source());
133 command.cover_art_from(1);
134 }
135 }
136
137 command.set_codec(codec);
138
139 if options.apply_loudnorm {
140 let measured = if let Some(loudnorm_analysis) = track.loudnorm_analysis {
141 loudnorm_analysis
142 } else {
143 LoudnormAnalysis::from_file(audio_source)?
144 };
145
146 track.loudnorm_analysis = Some(measured);
147
148 command.apply_loudnorm_filter(measured);
149 }
150
151 command.map_audio_from(0);
152 command.drop_subtitles();
153 command.set_container(container);
154
155 command.drop_metadata();
156
157 let metadata = track.metadata_key_values()?;
158 command.add_metadata_group(metadata.iter());
159
160 let output_file = to.as_ref();
161
162 command.output_file(output_file);
163
164 output_ffmpeg(command)?;
165
166 let lib_container = MediaContainer::from_file(output_file.to_path_buf())?;
167 track.lib_container = Some(lib_container);
168
169 if options.apply_loudnorm {
170 let loudnorm_config = common_config().loudnorm_config;
171 track.set_loudnorm(loudnorm_config);
172 }
173
174 db_sync_transaction(move |cas_tx| cas_tx.tx_patch(track.clone()), false)?;
175
176 Ok(())
177}
178
179pub fn valdiate_tag_value(title: &str) -> Result<(), String> {
181 if title.chars().any(char::is_control) {
182 let fixed: String = title.chars().filter(|c| !c.is_control()).collect();
183 Err(fixed)
184 } else {
185 Ok(())
186 }
187}