Skip to main content

koan_core/db/queries/
mod.rs

1mod albums;
2mod artists;
3pub mod auth;
4pub mod batch;
5mod favourites;
6pub mod history;
7pub mod lyrics;
8pub mod playback_state;
9pub mod playlists;
10pub mod radio;
11mod scan_cache;
12mod search;
13mod stats;
14pub mod tracks;
15pub mod vectors;
16
17use std::path::PathBuf;
18
19// Re-export all public items so `use queries::*` still works.
20pub use albums::*;
21pub use artists::*;
22pub use batch::*;
23pub use favourites::*;
24pub use history::*;
25pub use lyrics::*;
26pub use playback_state::*;
27pub use playlists::*;
28pub use radio::*;
29pub use scan_cache::*;
30pub use search::*;
31pub use stats::*;
32pub use tracks::*;
33pub use vectors::*;
34
35/// The half-open range of paths under a folder, for `path >= .0 AND path < .1`.
36///
37/// A prefix match on an indexed column, rather than `LIKE 'folder/%'` — which
38/// SQLite answers by reading every row, because a pattern is opaque to an
39/// index until it has been evaluated. It also takes the pattern out of the
40/// path: `LIKE` reads `_` as "any character" and folds ASCII case, so
41/// `/Volumes/My_Music` matched `/Volumes/My Music` and `/volumes/my_music`
42/// alike.
43///
44/// The trailing separator is what keeps `/Volumes/Music` out of
45/// `/Volumes/Music Backup`; the upper bound is the highest code point, so
46/// every path under the folder sorts below it.
47pub fn folder_prefix_range(folder: &std::path::Path) -> (String, String) {
48    let prefix = format!(
49        "{}{}",
50        folder
51            .to_string_lossy()
52            .trim_end_matches(std::path::MAIN_SEPARATOR),
53        std::path::MAIN_SEPARATOR
54    );
55    let upper = format!("{prefix}\u{10FFFF}");
56    (prefix, upper)
57}
58
59// --- Row types ---
60
61#[derive(Debug, Clone)]
62pub struct ArtistRow {
63    pub id: i64,
64    pub name: String,
65    pub sort_name: Option<String>,
66    pub remote_id: Option<String>,
67    /// Albums credited to this artist, and tracks across them. Aggregated in
68    /// the same query as the row itself — a count per artist would be one
69    /// query per row in a list thousands long.
70    pub album_count: i64,
71    pub track_count: i64,
72}
73
74#[derive(Debug, Clone)]
75pub struct AlbumRow {
76    pub id: i64,
77    pub title: String,
78    pub artist_id: i64,
79    pub artist_name: String,
80    pub date: Option<String>,
81    pub total_discs: Option<i32>,
82    pub total_tracks: Option<i32>,
83    pub codec: Option<String>,
84    pub label: Option<String>,
85    pub remote_id: Option<String>,
86    /// When the album entered the library — the server's `created` for remote
87    /// albums, otherwise the time it was first indexed.
88    pub added_at: Option<String>,
89}
90
91#[derive(Debug, Clone)]
92pub struct TrackRow {
93    pub id: i64,
94    pub album_id: Option<i64>,
95    pub artist_id: Option<i64>,
96    pub artist_name: String,
97    pub album_artist_name: String,
98    pub album_title: String,
99    pub disc: Option<i32>,
100    pub track_number: Option<i32>,
101    pub title: String,
102    pub duration_ms: Option<i64>,
103    pub path: Option<String>,
104    pub codec: Option<String>,
105    pub sample_rate: Option<i32>,
106    pub bit_depth: Option<i32>,
107    pub channels: Option<i32>,
108    pub bitrate: Option<i32>,
109    pub genre: Option<String>,
110    pub source: String,
111    pub remote_id: Option<String>,
112    pub cached_path: Option<String>,
113}
114
115/// Where to get audio data for playback. Local always wins.
116#[derive(Debug, Clone)]
117pub enum PlaybackSource {
118    Local(PathBuf),
119    Cached(PathBuf),
120    Remote(String),
121}
122
123#[derive(Debug, Clone, Default)]
124pub struct LibraryStats {
125    pub total_tracks: i64,
126    pub local_tracks: i64,
127    pub remote_tracks: i64,
128    pub cached_tracks: i64,
129    pub total_albums: i64,
130    pub total_artists: i64,
131}
132
133/// Metadata for inserting/updating a track.
134#[derive(Debug, Clone)]
135pub struct TrackMeta {
136    pub title: String,
137    pub artist: String,
138    pub album_artist: Option<String>,
139    pub album: String,
140    pub date: Option<String>,
141    pub disc: Option<i32>,
142    pub track_number: Option<i32>,
143    pub genre: Option<String>,
144    pub label: Option<String>,
145    pub duration_ms: Option<i64>,
146    pub codec: Option<String>,
147    pub sample_rate: Option<i32>,
148    pub bit_depth: Option<i32>,
149    pub channels: Option<i32>,
150    pub bitrate: Option<i32>,
151    pub size_bytes: Option<i64>,
152    pub mtime: Option<i64>,
153    pub path: Option<String>,
154    pub source: String,
155    pub remote_id: Option<String>,
156    pub remote_url: Option<String>,
157    /// The server's ids for the album and its artist.
158    ///
159    /// Carried alongside the track's own, because the server keys stars,
160    /// shares and cover art off them — a library synced without these has
161    /// albums and artists it can name but cannot refer to.
162    pub album_remote_id: Option<String>,
163    pub artist_remote_id: Option<String>,
164    /// MusicBrainz recording id. From the server today; a local scan could
165    /// read it from `MUSICBRAINZ_TRACKID` too.
166    pub mbid: Option<String>,
167    /// When the album this track belongs to entered the library. Remote sync
168    /// supplies the server's `created`; anything else leaves it and the album
169    /// is stamped with the time it was first seen.
170    pub album_added_at: Option<String>,
171}
172
173/// Test helper: build a sample TrackMeta for use in tests across sub-modules.
174#[cfg(test)]
175pub fn sample_meta(title: &str, artist: &str, album: &str) -> TrackMeta {
176    TrackMeta {
177        title: title.into(),
178        artist: artist.into(),
179        album_artist: Some(artist.into()),
180        album: album.into(),
181        date: Some("2024".into()),
182        disc: Some(1),
183        track_number: Some(1),
184        genre: Some("Electronic".into()),
185        label: None,
186        duration_ms: Some(240_000),
187        codec: Some("FLAC".into()),
188        sample_rate: Some(44100),
189        bit_depth: Some(16),
190        channels: Some(2),
191        bitrate: Some(1000),
192        size_bytes: Some(30_000_000),
193        mtime: Some(1700000000),
194        path: Some(format!("/music/{}/{}.flac", album, title)),
195        source: "local".into(),
196        remote_id: None,
197        album_remote_id: None,
198        artist_remote_id: None,
199        mbid: None,
200        remote_url: None,
201        album_added_at: None,
202    }
203}