use super::{Result, Track};
use rusqlite::{Connection, OptionalExtension, Row};
const COLS: &str = "id, path, title, artist, album, album_artist, track_no, disc_no,
year, genre, duration_ms, sample_rate, channels, bit_depth, mtime, size";
fn row_to_track(r: &Row) -> rusqlite::Result<Track> {
Ok(Track {
id: r.get(0)?,
path: r.get(1)?,
title: r.get(2)?,
artist: r.get(3)?,
album: r.get(4)?,
album_artist: r.get(5)?,
track_no: r.get(6)?,
disc_no: r.get(7)?,
year: r.get(8)?,
genre: r.get(9)?,
duration_ms: r.get(10)?,
sample_rate: r.get(11)?,
channels: r.get(12)?,
bit_depth: r.get(13)?,
mtime: r.get(14)?,
size: r.get(15)?,
})
}
const ORDER: &str = "ORDER BY album_artist IS NULL, album_artist, artist, album,
disc_no, track_no, title, path";
pub fn all(conn: &Connection) -> Result<Vec<Track>> {
let sql = format!("SELECT {COLS} FROM tracks {ORDER}");
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map([], row_to_track)?;
rows.collect()
}
pub fn count(conn: &Connection) -> Result<i64> {
conn.query_row("SELECT COUNT(*) FROM tracks", [], |r| r.get(0))
}
const FIELDS: &[(&str, &str)] = &[
("title", "title"),
("artist", "artist"),
("album", "album"),
("albumartist", "album_artist"),
("album_artist", "album_artist"),
];
fn terms(input: &str) -> Vec<(Option<&'static str>, String)> {
let mut out = Vec::new();
let mut chars = input.chars().peekable();
loop {
while chars.next_if(|c| c.is_whitespace()).is_some() {}
if chars.peek().is_none() {
return out;
}
let (mut text, mut quoted, mut seen_quote, mut colon) = (String::new(), false, false, None);
while let Some(c) = chars.next_if(|c| quoted || !c.is_whitespace()) {
match c {
'"' => (quoted, seen_quote) = (!quoted, true),
':' if !seen_quote && colon.is_none() => {
colon = Some(text.len());
text.push(c);
}
_ => text.push(c),
}
}
let field = colon.and_then(|at| {
let name = text[..at].to_ascii_lowercase();
FIELDS
.iter()
.find(|(typed, _)| *typed == name)
.map(|(_, column)| (at, *column))
});
match field {
Some((at, column)) => out.push((Some(column), text[at + 1..].to_string())),
None => out.push((None, text)),
}
}
}
fn fts_query(input: &str) -> Option<String> {
let terms: Vec<String> = terms(input)
.into_iter()
.filter(|(_, text)| !text.trim().is_empty())
.map(|(column, text)| {
let phrase = format!("\"{}\"*", text.replace('"', "\"\""));
match column {
Some(column) => format!("{column} : {phrase}"),
None => phrase,
}
})
.collect();
(!terms.is_empty()).then(|| terms.join(" "))
}
pub fn search(conn: &Connection, input: &str) -> Result<Vec<Track>> {
let Some(q) = fts_query(input) else {
return Ok(Vec::new());
};
let sql = format!(
"SELECT {COLS} FROM tracks
WHERE id IN (SELECT rowid FROM tracks_fts WHERE tracks_fts MATCH ?1)
{ORDER}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map([&q], row_to_track)?;
rows.collect()
}
pub fn by_path(conn: &Connection, path: &str) -> Result<Option<Track>> {
let sql = format!("SELECT {COLS} FROM tracks WHERE path = ?1");
conn.query_row(&sql, [path], row_to_track).optional()
}
pub fn under_path(conn: &Connection, prefix: &str) -> Result<Vec<Track>> {
let sql = format!("SELECT {COLS} FROM tracks WHERE substr(path, 1, length(?1)) = ?1 {ORDER}");
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map([prefix], row_to_track)?;
rows.collect()
}
#[derive(Debug, Clone, PartialEq)]
pub struct Playlist {
pub id: i64,
pub name: String,
pub len: i64,
}
pub fn playlists(conn: &Connection) -> Result<Vec<Playlist>> {
let mut stmt = conn.prepare(
"SELECT p.id, p.name, COUNT(i.track_id)
FROM playlists p LEFT JOIN playlist_items i ON i.playlist_id = p.id
GROUP BY p.id ORDER BY p.name",
)?;
let rows = stmt.query_map([], |r| {
Ok(Playlist {
id: r.get(0)?,
name: r.get(1)?,
len: r.get(2)?,
})
})?;
rows.collect()
}
pub fn find_playlist<'a>(lists: &'a [Playlist], name: &str) -> Option<&'a Playlist> {
if let Some(p) = lists.iter().find(|p| p.name == name) {
return Some(p);
}
let mut folded = lists.iter().filter(|p| p.name.eq_ignore_ascii_case(name));
match (folded.next(), folded.next()) {
(Some(p), None) => Some(p),
_ => None,
}
}
pub fn save_playlist(conn: &mut Connection, name: &str, track_ids: &[i64]) -> Result<i64> {
let tx = conn.transaction()?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
tx.execute(
"INSERT INTO playlists (name, created_at) VALUES (?1, ?2)
ON CONFLICT(name) DO UPDATE SET name = excluded.name",
rusqlite::params![name, now],
)?;
let id: i64 = tx.query_row("SELECT id FROM playlists WHERE name = ?1", [name], |r| {
r.get(0)
})?;
tx.execute("DELETE FROM playlist_items WHERE playlist_id = ?1", [id])?;
{
let mut ins = tx.prepare(
"INSERT INTO playlist_items (playlist_id, position, track_id) VALUES (?1, ?2, ?3)",
)?;
for (pos, tid) in track_ids.iter().enumerate() {
ins.execute(rusqlite::params![id, pos as i64, tid])?;
}
}
tx.commit()?;
Ok(id)
}
pub fn playlist_tracks(conn: &Connection, playlist_id: i64) -> Result<Vec<Track>> {
let sql = format!(
"SELECT {COLS} FROM tracks t
JOIN playlist_items i ON i.track_id = t.id
WHERE i.playlist_id = ?1 ORDER BY i.position"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map([playlist_id], row_to_track)?;
rows.collect()
}
pub fn rename_playlist(conn: &Connection, playlist_id: i64, name: &str) -> Result<()> {
conn.execute(
"UPDATE playlists SET name = ?1 WHERE id = ?2",
rusqlite::params![name, playlist_id],
)?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Mark {
pub frame: u64,
pub rate: u32,
}
impl Mark {
pub fn at_time(at: std::time::Duration, rate: u32) -> Self {
Mark {
frame: (at.as_secs_f64() * rate as f64).round() as u64,
rate,
}
}
pub fn time(&self) -> std::time::Duration {
std::time::Duration::from_secs_f64(self.frame as f64 / self.rate.max(1) as f64)
}
}
pub fn marks(conn: &Connection, path: &str) -> Result<Vec<Mark>> {
let mut stmt = conn.prepare("SELECT frame, rate FROM marks WHERE path = ?1 ORDER BY frame")?;
let rows = stmt.query_map([path], |r| {
Ok(Mark {
frame: r.get::<_, i64>(0)? as u64,
rate: r.get(1)?,
})
})?;
rows.collect()
}
pub fn add_mark(conn: &Connection, path: &str, mark: Mark) -> Result<()> {
conn.execute(
"INSERT OR IGNORE INTO marks (path, frame, rate) VALUES (?1, ?2, ?3)",
rusqlite::params![path, mark.frame as i64, mark.rate],
)?;
Ok(())
}
pub fn remove_last_mark(conn: &Connection, path: &str) -> Result<Option<Mark>> {
let last = conn
.query_row(
"SELECT rowid, frame, rate FROM marks WHERE path = ?1 ORDER BY rowid DESC LIMIT 1",
[path],
|r| {
let mark = Mark {
frame: r.get::<_, i64>(1)? as u64,
rate: r.get(2)?,
};
Ok((r.get::<_, i64>(0)?, mark))
},
)
.optional()?;
let Some((rowid, mark)) = last else {
return Ok(None);
};
conn.execute("DELETE FROM marks WHERE rowid = ?1", [rowid])?;
Ok(Some(mark))
}
pub fn clear_marks(conn: &Connection, path: &str) -> Result<usize> {
conn.execute("DELETE FROM marks WHERE path = ?1", [path])
}
pub fn delete_playlist(conn: &Connection, playlist_id: i64) -> Result<()> {
conn.execute("DELETE FROM playlists WHERE id = ?1", [playlist_id])?;
Ok(())
}