gonk_core/
sqlite.rs

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}