use anyhow::Result;
use rusqlite::Row;
use crate::analysis::normalize_title;
use crate::db::MasterDb;
use crate::query::Fields;
#[derive(Clone, Debug)]
pub struct TrackRow {
pub id: String,
pub title: String,
pub artist: String,
pub bpm: Option<i64>,
pub length: Option<i64>,
pub cue_count: i64,
pub analysed: i64,
pub file_type: Option<i64>,
pub norm_title: String,
pub locked: bool,
pub is_unlocked_cueless_audio: bool,
pub search_blob: String,
pub playlist_blob: String,
pub tags: String,
}
const AUDIO_FILE_TYPES: &[i64] = &[0, 1, 4, 5, 11];
#[derive(Default)]
pub(crate) struct RowInput {
pub id: String,
pub title: Option<String>,
pub artist: Option<String>,
pub bpm: Option<i64>,
pub length: Option<i64>,
pub analysed: Option<i64>,
pub file_type: Option<i64>,
pub cue_count: i64,
}
impl TrackRow {
pub(crate) fn from_db(input: RowInput) -> Self {
let RowInput {
id,
title,
artist,
bpm,
length,
analysed,
file_type,
cue_count,
} = input;
let title = title.unwrap_or_default();
let artist = artist.unwrap_or_default();
let norm_title = normalize_title(&title);
let analysed = analysed.unwrap_or(0);
let locked = analysed & 0x80 != 0;
let is_audio = file_type
.map(|ft| AUDIO_FILE_TYPES.contains(&ft))
.unwrap_or(false);
let is_unlocked_cueless_audio = !locked && cue_count == 0 && is_audio;
let search_blob = crate::query::text_blob(&title, &artist);
Self {
id,
title,
artist,
bpm,
length,
cue_count,
analysed,
file_type,
norm_title,
locked,
is_unlocked_cueless_audio,
search_blob,
playlist_blob: String::new(),
tags: String::new(),
}
}
pub fn fields(&self) -> Fields<'_> {
Fields {
text: &self.search_blob,
playlists: &self.playlist_blob,
tags: &self.tags,
bpm: self.bpm,
length: self.length,
}
}
#[cfg(test)]
pub(crate) fn stub(id: &str, title: &str) -> Self {
Self::from_db(RowInput {
id: id.to_string(),
title: Some(title.to_string()),
..Default::default()
})
}
#[cfg(test)]
pub(crate) fn with_playlists(mut self, paths: &[&str]) -> Self {
self.playlist_blob = paths.join("\n").to_lowercase();
self
}
#[cfg(test)]
pub(crate) fn with_tags(mut self, tags: &[&str]) -> Self {
self.tags = format!(" {} ", tags.join(" "));
self
}
}
pub fn load_rows(db: &MasterDb) -> Result<Vec<TrackRow>> {
let sql = "
SELECT c.ID, c.Title, c.BPM, c.Length, c.Analysed, c.FileType,
c.FolderPath, c.ServiceID, a.Name AS Artist,
(SELECT COUNT(*) FROM djmdCue
WHERE ContentID = c.ID
AND (rb_local_deleted = 0 OR rb_local_deleted IS NULL)) AS cue_count
FROM djmdContent c
LEFT JOIN djmdArtist a ON a.ID = c.ArtistID
WHERE c.rb_local_deleted = 0 OR c.rb_local_deleted IS NULL
ORDER BY c.Title COLLATE NOCASE";
let mut stmt = db.conn.prepare(sql)?;
let rows = stmt.query_map([], |r: &Row<'_>| {
let mut row = TrackRow::from_db(RowInput {
id: r.get("ID")?,
title: r.get("Title")?,
artist: r.get("Artist")?,
bpm: r.get("BPM")?,
length: r.get("Length")?,
analysed: r.get("Analysed")?,
file_type: r.get("FileType")?,
cue_count: r.get("cue_count")?,
});
let path = r.get::<_, Option<String>>("FolderPath")?;
let path = path.as_deref();
let origin = crate::format::origin(row.file_type, path, r.get("ServiceID")?);
row.tags = crate::format::track_tags(crate::format::TrackFacts {
origin,
file_type: row.file_type,
cue_count: row.cue_count,
locked: row.locked,
present: crate::presence::check(origin, path),
});
Ok(row)
})?;
let mut out = rows.collect::<rusqlite::Result<Vec<_>>>()?;
let mut playlists = crate::playlists::blobs_by_track(db).unwrap_or_default();
for row in &mut out {
row.playlist_blob = playlists.remove(&row.id).unwrap_or_default();
}
Ok(out)
}