termusiclib/new_database/
mod.rs1#![allow(clippy::unnecessary_debug_formatting)] use std::{fmt::Debug, path::Path, sync::Arc};
4
5use anyhow::{Context, Result};
6use parking_lot::{Mutex, MutexGuard};
7use rusqlite::{Connection, OptionalExtension};
8use tokio::{runtime::Handle, sync::Semaphore};
9use track_insert::TrackInsertable;
10use walkdir::DirEntry;
11
12use crate::{
13 config::{ServerOverlay, v2::server::ScanDepth},
14 new_database::{
15 album_ops::delete_all_unreferenced_albums, artist_ops::delete_all_unreferenced_artists,
16 },
17 track::{MetadataOptions, parse_metadata_from_file},
18 utils::{filetype_supported, get_app_new_database_path},
19};
20
21pub type Integer = i64;
25
26mod album_insert;
27pub mod album_ops;
28mod artist_insert;
29pub mod artist_ops;
30mod migrate;
31mod track_insert;
32pub mod track_ops;
33
34#[allow(clippy::doc_markdown)]
35#[derive(Clone)]
39pub struct Database {
40 conn: Arc<Mutex<Connection>>,
41 semaphore: Arc<Semaphore>,
43}
44
45impl Debug for Database {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("DataBase")
48 .field("conn", &"<unavailable>")
49 .finish()
50 }
51}
52
53impl Database {
54 pub fn new(path: &Path) -> Result<Self> {
61 let conn = Connection::open(path).context("open/create database")?;
62
63 Self::new_from_connection(conn)
64 }
65
66 pub fn new_default_path() -> Result<Self> {
72 Self::new(&get_app_new_database_path()?)
73 }
74
75 pub fn get_connection(&self) -> MutexGuard<'_, Connection> {
77 self.conn.lock()
78 }
79
80 fn new_from_connection(conn: Connection) -> Result<Self> {
82 migrate::migrate(&conn).context("Database migration")?;
83
84 let conn = Arc::new(Mutex::new(conn));
85 let semaphore = Arc::new(Semaphore::new(1));
87 Ok(Self { conn, semaphore })
88 }
89
90 pub fn scan_path(
96 &self,
97 path: &Path,
98 config: &ServerOverlay,
99 replace_metadata: bool,
100 ) -> Result<()> {
101 let path = path
102 .canonicalize()
103 .with_context(|| path.display().to_string())?;
104
105 let walker = {
106 let mut walker = walkdir::WalkDir::new(&path).follow_links(true);
107
108 if let ScanDepth::Limited(limit) = config.get_metadata_scan_depth() {
109 walker = walker.max_depth(usize::try_from(limit).unwrap_or(usize::MAX));
110 }
111
112 walker
113 .into_iter()
114 .filter_map(Result::ok)
115 .filter(|v| v.file_type().is_file())
117 .filter(|v| filetype_supported(v.path()))
118 };
119
120 let separators = config.settings.metadata.artist_separators.clone();
121
122 self.spawn_worker(move |db| {
123 let separators: Vec<&str> = separators.iter().map(String::as_str).collect();
124 Self::process_iter(walker, &db, &path, replace_metadata, &separators);
125 });
126
127 Ok(())
128 }
129
130 fn spawn_worker<F>(&self, fun: F)
134 where
135 F: FnOnce(Self) + Send + 'static,
136 {
137 let handle = Handle::current();
138 let handle_1 = handle.clone();
139
140 let db = self.clone();
141
142 handle.spawn(async move {
144 let Ok(permit) = db.semaphore.clone().acquire_owned().await else {
145 error!("Failed to acquite permit for scanner!");
146 return;
147 };
148
149 handle_1.spawn_blocking(move || {
150 let _permit = permit;
152 fun(db);
153 });
154 });
155 }
156
157 fn process_iter(
161 walker: impl Iterator<Item = DirEntry>,
162 db: &Self,
163 path: &Path,
164 replace_metadata: bool,
165 separators: &[&str],
166 ) {
167 info!("Scanning {path:#?}");
169
170 let mut created_updated: usize = 0;
171
172 for record in walker {
176 let path = record.path();
177
178 if !replace_metadata {
180 match track_ops::track_exists(&db.conn.lock(), path) {
181 Ok(true) => continue,
182 Err(err) => {
183 warn!("Error checking if {path:#?} exists: {err:#?}");
184 continue;
185 }
186 Ok(false) => (),
187 }
188 }
189
190 let track_metadata = match parse_metadata_from_file(
191 path,
192 MetadataOptions {
193 album: true,
194 album_artist: true,
195 album_artists: true,
196 artist: true,
197 artists: true,
198 artist_separators: separators,
199 title: true,
200 duration: true,
201 genre: true,
202 ..Default::default()
203 },
204 ) {
205 Ok(v) => v,
206 Err(err) => {
207 warn!("Error scanning path {path:#?}: {err:#?}");
208 continue;
209 }
210 };
211
212 let db_track = match TrackInsertable::try_from_track(path, &track_metadata) {
213 Ok(v) => v,
214 Err(err) => {
215 warn!("Error converting to database track {path:#?}: {err:#?}");
216 continue;
217 }
218 };
219
220 let _id = match db_track.try_insert_or_update(&db.conn.lock()) {
221 Ok(v) => v,
222 Err(err) => {
223 warn!("Error inserting or updating {path:#?}: {err:#?}");
224 continue;
225 }
226 };
227
228 created_updated += 1;
229 }
230
231 info!("Finished Scanning {path:#?} with {created_updated} created or updated");
232 }
233
234 pub fn run_cleanup(&self) {
239 self.spawn_worker(move |db| {
240 if let Err(err) = Self::process_cleanup(&db) {
241 warn!("Error processing database cleanup: {err:#?}");
242 }
243 });
244 }
245
246 fn process_cleanup(db: &Self) -> Result<()> {
248 let conn = db.get_connection();
249
250 info!("Starting Database cleanup");
251
252 let affected_albums = delete_all_unreferenced_albums(&conn)?;
255
256 info!("Deleted {affected_albums} Albums");
257
258 let affected_artists = delete_all_unreferenced_artists(&conn)?;
259
260 info!("Deleted {affected_artists} Artists");
261
262 exec_optimize(&conn)?;
264
265 info!("Finished Database cleanup");
266
267 Ok(())
268 }
269}
270
271fn exec_optimize(conn: &Connection) -> Result<()> {
273 let mut stmt = conn.prepare("PRAGMA optimize;")?;
274
275 let _ = stmt.execute([]).optional()?.unwrap_or_default();
277
278 Ok(())
279}
280
281#[cfg(test)]
282mod test_utils {
283 use std::path::{Path, PathBuf};
284
285 use rusqlite::Connection;
286
287 use super::Database;
288
289 pub fn gen_database_raw() -> Connection {
291 Connection::open_in_memory().expect("open db failed")
292 }
293
294 pub fn gen_database() -> Database {
296 Database::new_from_connection(gen_database_raw()).expect("db creation failed")
297 }
298
299 pub fn test_path(path: &Path) -> PathBuf {
301 if cfg!(windows) {
302 let mut pathbuf = PathBuf::from("C:\\");
303 pathbuf.push(path);
304
305 pathbuf
306 } else {
307 path.to_path_buf()
308 }
309 }
310
311 #[test]
312 #[cfg(unix)]
313 fn test_path_absolute_unix() {
314 let path = test_path(Path::new("/somewhere/else"));
315 assert!(path.is_absolute());
316
317 assert_eq!(path, Path::new("/somewhere/else"));
318 }
319
320 #[test]
321 #[cfg(windows)]
322 fn test_path_absolute_windows() {
323 let path = test_path(Path::new("/somewhere/else"));
324 assert!(path.is_absolute());
325
326 assert_eq!(path, Path::new("C:\\somewhere\\else"));
327 }
328}