1use crate::{Song, DB_DIR};
2use jwalk::WalkDir;
3use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
4use rusqlite::{params, Connection, Params, Row};
5use std::{
6 path::PathBuf,
7 sync::{Mutex, MutexGuard},
8 thread::{self, JoinHandle},
9 time::Duration,
10};
11
12pub fn get_all_songs() -> Vec<(usize, Song)> {
13 let conn = conn();
14 let mut stmt = conn.prepare("SELECT *, rowid FROM song").unwrap();
15
16 stmt.query_map([], |row| {
17 let id = row.get(9).unwrap();
18 let song = song(row);
19 Ok((id, song))
20 })
21 .unwrap()
22 .flatten()
23 .collect()
24}
25pub fn get_all_artists() -> Vec<String> {
26 let conn = conn();
27 let mut stmt = conn
28 .prepare("SELECT DISTINCT artist FROM song ORDER BY artist COLLATE NOCASE")
29 .unwrap();
30
31 stmt.query_map([], |row| {
32 let artist: String = row.get(0).unwrap();
33 Ok(artist)
34 })
35 .unwrap()
36 .flatten()
37 .collect()
38}
39pub fn get_all_albums() -> Vec<(String, String)> {
40 let conn = conn();
41 let mut stmt = conn
42 .prepare("SELECT DISTINCT album, artist FROM song ORDER BY artist COLLATE NOCASE")
43 .unwrap();
44
45 stmt.query_map([], |row| {
46 let album: String = row.get(0).unwrap();
47 let artist: String = row.get(1).unwrap();
48 Ok((album, artist))
49 })
50 .unwrap()
51 .flatten()
52 .collect()
53}
54pub fn get_all_albums_by_artist(artist: &str) -> Vec<String> {
55 let conn = conn();
56 let mut stmt = conn
57 .prepare("SELECT DISTINCT album FROM song WHERE artist = ? ORDER BY album COLLATE NOCASE")
58 .unwrap();
59
60 stmt.query_map([artist], |row| row.get(0))
61 .unwrap()
62 .flatten()
63 .collect()
64}
65pub fn get_all_songs_from_album(album: &str, artist: &str) -> Vec<Song> {
66 collect_songs(
67 "SELECT * FROM song WHERE artist=(?1) AND album=(?2) ORDER BY disc, number",
68 params![artist, album],
69 )
70}
71pub fn get_songs_by_artist(artist: &str) -> Vec<Song> {
72 collect_songs(
73 "SELECT * FROM song WHERE artist = ? ORDER BY album, disc, number",
74 params![artist],
75 )
76}
77pub fn get_song(song: &(u64, String), album: &str, artist: &str) -> Vec<Song> {
78 collect_songs(
79 "SELECT * FROM song WHERE name=(?1) AND number=(?2) AND artist=(?3) AND album=(?4)",
80 params![song.1, song.0, artist, album],
81 )
82}
83pub fn get_songs_from_id(ids: &[usize]) -> Vec<Song> {
84 ids.iter()
85 .filter_map(|id| {
86 collect_songs("SELECT * FROM song WHERE rowid = ?", params![id])
87 .first()
88 .cloned()
89 })
90 .collect()
91}
92fn collect_songs<P>(query: &str, params: P) -> Vec<Song>
93where
94 P: Params,
95{
96 let conn = conn();
97 let mut stmt = conn.prepare(query).unwrap();
98
99 stmt.query_map(params, |row| Ok(song(row)))
100 .unwrap()
101 .flatten()
102 .collect()
103}
104fn song(row: &Row) -> Song {
105 let path: String = row.get(5).unwrap();
106 let dur: f64 = row.get(6).unwrap();
107 Song {
108 number: row.get(0).unwrap(),
109 disc: row.get(1).unwrap(),
110 name: row.get(2).unwrap(),
111 album: row.get(3).unwrap(),
112 artist: row.get(4).unwrap(),
113 duration: Duration::from_secs_f64(dur),
114 path: PathBuf::from(path),
115 track_gain: row.get(7).unwrap(),
116 }
117}
118
119pub static mut CONN: Option<Mutex<rusqlite::Connection>> = None;
120
121pub fn conn() -> MutexGuard<'static, Connection> {
122 unsafe { CONN.as_ref().unwrap().lock().unwrap() }
123}
124
125#[allow(unused)]
126pub fn reset() {
127 unsafe {
128 CONN = None;
129 }
130 std::fs::remove_file(DB_DIR.as_path());
131}
132
133pub fn open_database() -> Option<Mutex<rusqlite::Connection>> {
134 let exists = DB_DIR.exists();
135 if let Ok(conn) = Connection::open(DB_DIR.as_path()) {
136 if !exists {
137 conn.execute(
138 "CREATE TABLE song (
139 number INTEGER NOT NULL,
140 disc INTEGER NOT NULL,
141 name TEXT NOT NULL,
142 album TEXT NOT NULL,
143 artist TEXT NOT NULL,
144 path TEXT NOT NULL UNIQUE,
145 duration DOUBLE NOT NULL,
146 track_gain DOUBLE NOT NULL,
147 parent TEXT NOT NULL
148 )",
149 [],
150 )
151 .unwrap();
152 }
153 Some(Mutex::new(conn))
154 } else {
155 None
156 }
157}
158
159pub enum State {
160 Busy,
161 Idle,
162 NeedsUpdate,
163}
164
165#[derive(Default)]
166pub struct Database {
167 handle: Option<JoinHandle<()>>,
168}
169
170impl Database {
171 pub fn add_paths(&mut self, paths: &[String]) {
172 if let Some(handle) = &self.handle {
173 if !handle.is_finished() {
174 return;
175 }
176 }
177
178 let paths = paths.to_vec();
179
180 let handle = thread::spawn(move || {
181 let queries: Vec<String> = paths
182 .iter()
183 .map(|path| {
184 let paths: Vec<PathBuf> = WalkDir::new(path)
185 .into_iter()
186 .flatten()
187 .map(|dir| dir.path())
188 .filter(|path| match path.extension() {
189 Some(ex) => {
190 matches!(ex.to_str(), Some("flac" | "mp3" | "ogg" | "wav" | "m4a"))
191 }
192 None => false,
193 })
194 .collect();
195
196 let songs: Vec<Song> = paths
197 .par_iter()
198 .map(|dir| Song::from(dir))
199 .flatten()
200 .collect();
201
202 if songs.is_empty() {
203 String::new()
204 } else {
205 songs
206 .iter()
207 .map(|song| {
208 let artist = song.artist.replace('\'', r"''");
209 let album = song.album.replace('\'', r"''");
210 let name = song.name.replace('\'', r"''");
211 let song_path= song.path.to_string_lossy().replace('\'', r"''");
212 let parent = path.replace('\'', r"''");
213
214 format!("INSERT OR IGNORE INTO song (number, disc, name, album, artist, path, duration, track_gain, parent) VALUES ('{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}');",
215 song.number, song.disc, name, album, artist, song_path, song.duration.as_secs_f64(), song.track_gain, parent)
216 })
217 .collect::<Vec<String>>()
218 .join("\n")
219 }
220 })
221 .collect();
222
223 let stmt = format!("BEGIN;\nDELETE FROM song;\n{}COMMIT;\n", queries.join("\n"));
224 conn().execute_batch(&stmt).unwrap();
225 });
226
227 self.handle = Some(handle);
228 }
229 pub fn state(&mut self) -> State {
230 match self.handle {
231 Some(ref handle) => {
232 let finished = handle.is_finished();
233 if finished {
234 self.handle = None;
235 State::NeedsUpdate
236 } else {
237 State::Busy
238 }
239 }
240 None => State::Idle,
241 }
242 }
243}