feat(slint): plex rows attach on upload, manager sort interleaving, offline cover chain
Upload to Qobuz now attaches Plex sidecar rows via the existing add_plex_track_to_playlist helper (positions after the local block), and the local entity is deleted only when the sidecar attach succeeds - a DB failure keeps the playlist with an upload-incomplete toast instead of silently dropping it. Playlist manager: local playlists interleave into the active sort with the same comparators as the Qobuz set (name/tracks real interleave; playcount sorts locals as zero, custom as after-the-positioned-set, recent after the API order - documented). u64-keyed mutators verified safe with local: ids. Offline covers: cached_tracks.artwork_path turned out to have zero writers in history, so CachedTrackInfo gained artwork_path + file_path and a frontend-agnostic resolve_cover_path chain (index path, CMAF cover.jpg, v1 sibling cover.jpg). The offline manager album rows and the local- playlist cached rows now resolve covers through it. cc
This commit is contained in:
6 files changed
+225
-79
No files matched your search
@@ -7,13 +7,13 @@ use crate::types::{
|
||||
CachedTrackInfo, OfflineCacheStats, OfflineCacheStatus, ReadyTrackForSync, TrackCacheInfo,
|
||||
};
|
||||
|
||||
/// Maps a `cached_tracks` row (with the canonical 15-column SELECT used by
|
||||
/// Maps a `cached_tracks` row (with the canonical 17-column SELECT used by
|
||||
/// `get_track`, `get_all_tracks`, and `get_album_tracks`) into a `CachedTrackInfo`.
|
||||
///
|
||||
/// SELECT must be:
|
||||
/// `track_id, title, artist, album, album_id, duration_secs, file_size_bytes,
|
||||
/// quality, bit_depth, sample_rate, status, progress_percent, error_message,
|
||||
/// created_at, last_accessed_at`
|
||||
/// created_at, last_accessed_at, artwork_path, file_path`
|
||||
fn row_to_cached_track_info(row: &rusqlite::Row) -> rusqlite::Result<CachedTrackInfo> {
|
||||
Ok(CachedTrackInfo {
|
||||
track_id: row.get::<_, i64>(0)? as u64,
|
||||
@@ -31,6 +31,8 @@ fn row_to_cached_track_info(row: &rusqlite::Row) -> rusqlite::Result<CachedTrack
|
||||
error_message: row.get(12)?,
|
||||
created_at: row.get(13)?,
|
||||
last_accessed_at: row.get(14)?,
|
||||
artwork_path: row.get::<_, Option<String>>(15)?,
|
||||
file_path: row.get(16)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -325,7 +327,7 @@ impl OfflineCacheDb {
|
||||
/// Get track info
|
||||
pub fn get_track(&self, track_id: u64) -> Result<Option<CachedTrackInfo>, String> {
|
||||
let result = self.conn.query_row(
|
||||
"SELECT track_id, title, artist, album, album_id, duration_secs, file_size_bytes, quality, bit_depth, sample_rate, status, progress_percent, error_message, created_at, last_accessed_at
|
||||
"SELECT track_id, title, artist, album, album_id, duration_secs, file_size_bytes, quality, bit_depth, sample_rate, status, progress_percent, error_message, created_at, last_accessed_at, artwork_path, file_path
|
||||
FROM cached_tracks WHERE track_id = ?1",
|
||||
params![track_id as i64],
|
||||
row_to_cached_track_info,
|
||||
@@ -341,7 +343,7 @@ impl OfflineCacheDb {
|
||||
/// Get all cached tracks
|
||||
pub fn get_all_tracks(&self) -> Result<Vec<CachedTrackInfo>, String> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT track_id, title, artist, album, album_id, duration_secs, file_size_bytes, quality, bit_depth, sample_rate, status, progress_percent, error_message, created_at, last_accessed_at
|
||||
"SELECT track_id, title, artist, album, album_id, duration_secs, file_size_bytes, quality, bit_depth, sample_rate, status, progress_percent, error_message, created_at, last_accessed_at, artwork_path, file_path
|
||||
FROM cached_tracks ORDER BY last_accessed_at DESC"
|
||||
).map_err(|e| format!("Failed to prepare query: {}", e))?;
|
||||
|
||||
@@ -364,7 +366,8 @@ impl OfflineCacheDb {
|
||||
.prepare(
|
||||
"SELECT track_id, title, artist, album, album_id, duration_secs,
|
||||
file_size_bytes, quality, bit_depth, sample_rate, status,
|
||||
progress_percent, error_message, created_at, last_accessed_at
|
||||
progress_percent, error_message, created_at, last_accessed_at,
|
||||
artwork_path, file_path
|
||||
FROM cached_tracks WHERE album_id = ?1",
|
||||
)
|
||||
.map_err(|e| format!("Prepare failed: {}", e))?;
|
||||
|
||||
@@ -112,6 +112,8 @@ mod tests {
|
||||
error_message: None,
|
||||
created_at: "".into(),
|
||||
last_accessed_at: "".into(),
|
||||
artwork_path: None,
|
||||
file_path: "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,46 @@ pub struct CachedTrackInfo {
|
||||
pub error_message: Option<String>,
|
||||
pub created_at: String,
|
||||
pub last_accessed_at: String,
|
||||
/// The index's `artwork_path` column. Historically never backfilled by
|
||||
/// the downloaders (they record the cover path in library.db only), so
|
||||
/// treat it as a hint — [`Self::resolve_cover_path`] falls back to the
|
||||
/// on-disk layouts when it is unset.
|
||||
pub artwork_path: Option<String>,
|
||||
/// The audio path: the organized FLAC for v1 rows, the CMAF segments
|
||||
/// path (inside `tracks-cmaf/<id>/`) for v2 rows.
|
||||
pub file_path: String,
|
||||
}
|
||||
|
||||
impl CachedTrackInfo {
|
||||
/// Resolve this row's on-disk cover thumbnail, if any:
|
||||
/// 1. the index's `artwork_path`, when set and still on disk;
|
||||
/// 2. the v2 CMAF bundle's `<cache_path>/tracks-cmaf/<id>/cover.jpg`;
|
||||
/// 3. the `cover.jpg` sibling of `file_path` — v1 rows save the cover
|
||||
/// next to the organized FLAC without backfilling `artwork_path`.
|
||||
pub fn resolve_cover_path(&self, cache_path: &str) -> Option<String> {
|
||||
use std::path::Path;
|
||||
if let Some(p) = self.artwork_path.as_deref().filter(|p| !p.is_empty()) {
|
||||
if Path::new(p).is_file() {
|
||||
return Some(p.to_string());
|
||||
}
|
||||
}
|
||||
let cmaf = Path::new(cache_path)
|
||||
.join("tracks-cmaf")
|
||||
.join(self.track_id.to_string())
|
||||
.join("cover.jpg");
|
||||
if cmaf.is_file() {
|
||||
return Some(cmaf.to_string_lossy().to_string());
|
||||
}
|
||||
let fp = Path::new(&self.file_path);
|
||||
let folder = if fp.is_dir() { Some(fp) } else { fp.parent() };
|
||||
if let Some(dir) = folder {
|
||||
let sibling = dir.join("cover.jpg");
|
||||
if sibling.is_file() {
|
||||
return Some(sibling.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal track info for syncing to library
|
||||
|
||||
Reference in new issue
Block a user