feat(library): add playlist_plex_tracks table and CRUD for Plex-in-playlist
First half of the Plex-tracks-in-playlists fix. Adds a dedicated
playlist_plex_tracks table alongside the existing playlist_local_tracks
so Plex content (TEXT rating key, owned by a remote server) doesn't
collide with the i64 filesystem id used by local_tracks. No foreign
key to plex_cache_tracks — the cache can be purged independently and
the user's intent should survive.
Database methods added:
* add_plex_track_to_playlist(playlist_id, rating_key, position)
* remove_plex_track_from_playlist(playlist_id, rating_key)
* get_playlist_plex_tracks_with_position(playlist_id) ->
Vec<(rating_key, position)>
* get_playlist_plex_track_count(playlist_id) -> u32
get_all_playlist_local_track_counts now merges plex counts into the
same HashMap so the sidebar's hasLocalContent signal stays a single
number and covers Plex-populated playlists without a second round
trip from the frontend.
cc
This commit is contained in:
1 file changed
+152
-2
@@ -155,6 +155,24 @@ impl LibraryDatabase {
|
||||
CREATE INDEX IF NOT EXISTS idx_playlist_local_tracks_playlist
|
||||
ON playlist_local_tracks(qobuz_playlist_id);
|
||||
|
||||
-- Plex tracks added to playlists. Kept in its own table because
|
||||
-- Plex tracks live on a remote server and have a TEXT rating key,
|
||||
-- not the i64 filesystem id used by local_tracks. No foreign key
|
||||
-- to plex_cache_tracks: that cache can be purged without losing
|
||||
-- the user's intent (the rows gray out in the UI until Plex is
|
||||
-- reachable again).
|
||||
CREATE TABLE IF NOT EXISTS playlist_plex_tracks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
qobuz_playlist_id INTEGER NOT NULL,
|
||||
plex_rating_key TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
added_at INTEGER NOT NULL,
|
||||
UNIQUE(qobuz_playlist_id, plex_rating_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_playlist_plex_tracks_playlist
|
||||
ON playlist_plex_tracks(qobuz_playlist_id);
|
||||
|
||||
-- Custom track order per playlist (user-defined arrangement)
|
||||
CREATE TABLE IF NOT EXISTS playlist_track_custom_order (
|
||||
id INTEGER PRIMARY KEY,
|
||||
@@ -2782,6 +2800,107 @@ impl LibraryDatabase {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// === Playlist Plex Tracks ===
|
||||
|
||||
/// Add a Plex track to a playlist, identified by its Plex rating key.
|
||||
/// The rating key is stored verbatim so the pairing survives Plex
|
||||
/// cache rebuilds.
|
||||
pub fn add_plex_track_to_playlist(
|
||||
&self,
|
||||
qobuz_playlist_id: u64,
|
||||
plex_rating_key: &str,
|
||||
position: i32,
|
||||
) -> Result<(), LibraryError> {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0);
|
||||
|
||||
self.conn
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO playlist_plex_tracks
|
||||
(qobuz_playlist_id, plex_rating_key, position, added_at)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
params![qobuz_playlist_id as i64, plex_rating_key, position, now],
|
||||
)
|
||||
.map_err(|e| {
|
||||
LibraryError::Database(format!("Failed to add Plex track to playlist: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a Plex track from a playlist.
|
||||
pub fn remove_plex_track_from_playlist(
|
||||
&self,
|
||||
qobuz_playlist_id: u64,
|
||||
plex_rating_key: &str,
|
||||
) -> Result<(), LibraryError> {
|
||||
self.conn
|
||||
.execute(
|
||||
"DELETE FROM playlist_plex_tracks
|
||||
WHERE qobuz_playlist_id = ?1 AND plex_rating_key = ?2",
|
||||
params![qobuz_playlist_id as i64, plex_rating_key],
|
||||
)
|
||||
.map_err(|e| {
|
||||
LibraryError::Database(format!("Failed to remove Plex track from playlist: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all Plex tracks in a playlist with their stored position.
|
||||
/// Returns (rating_key, position) pairs. The caller is responsible
|
||||
/// for hydrating metadata from the Plex cache.
|
||||
pub fn get_playlist_plex_tracks_with_position(
|
||||
&self,
|
||||
qobuz_playlist_id: u64,
|
||||
) -> Result<Vec<(String, i32)>, LibraryError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare(
|
||||
"SELECT plex_rating_key, position
|
||||
FROM playlist_plex_tracks
|
||||
WHERE qobuz_playlist_id = ?1
|
||||
ORDER BY position ASC",
|
||||
)
|
||||
.map_err(|e| LibraryError::Database(format!("Failed to prepare query: {}", e)))?;
|
||||
|
||||
let rows = stmt
|
||||
.query_map(params![qobuz_playlist_id as i64], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, i32>(1)?))
|
||||
})
|
||||
.map_err(|e| {
|
||||
LibraryError::Database(format!(
|
||||
"Failed to query playlist plex tracks: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
rows.collect::<Result<Vec<_>, _>>().map_err(|e| {
|
||||
LibraryError::Database(format!("Failed to collect playlist plex tracks: {}", e))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get count of Plex tracks in a playlist
|
||||
pub fn get_playlist_plex_track_count(
|
||||
&self,
|
||||
qobuz_playlist_id: u64,
|
||||
) -> Result<u32, LibraryError> {
|
||||
let count: u32 = self
|
||||
.conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM playlist_plex_tracks WHERE qobuz_playlist_id = ?1",
|
||||
params![qobuz_playlist_id as i64],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| {
|
||||
LibraryError::Database(format!("Failed to count playlist plex tracks: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Get all local tracks in a playlist
|
||||
pub fn get_playlist_local_tracks(
|
||||
&self,
|
||||
@@ -2934,10 +3053,19 @@ impl LibraryDatabase {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Get local track counts for all playlists
|
||||
/// Get local track counts for all playlists.
|
||||
///
|
||||
/// "Local" here is the user-facing sense — anything that isn't a Qobuz
|
||||
/// server track. That includes file-system local tracks (user / qobuz
|
||||
/// purchases / offline-cached downloads, all in local_tracks) plus
|
||||
/// Plex tracks (in a parallel playlist_plex_tracks table). The two
|
||||
/// sums are merged per playlist so the sidebar's hasLocalContent
|
||||
/// indicator picks up Plex content too.
|
||||
pub fn get_all_playlist_local_track_counts(
|
||||
&self,
|
||||
) -> Result<std::collections::HashMap<u64, u32>, LibraryError> {
|
||||
let mut result: std::collections::HashMap<u64, u32> = std::collections::HashMap::new();
|
||||
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare(
|
||||
@@ -2955,13 +3083,35 @@ impl LibraryDatabase {
|
||||
})
|
||||
.map_err(|e| LibraryError::Database(format!("Failed to query: {}", e)))?;
|
||||
|
||||
let mut result = std::collections::HashMap::new();
|
||||
for row in rows {
|
||||
let (playlist_id, count) =
|
||||
row.map_err(|e| LibraryError::Database(format!("Failed to read row: {}", e)))?;
|
||||
result.insert(playlist_id, count);
|
||||
}
|
||||
|
||||
let mut plex_stmt = self
|
||||
.conn
|
||||
.prepare(
|
||||
"SELECT qobuz_playlist_id, COUNT(*) as count
|
||||
FROM playlist_plex_tracks
|
||||
GROUP BY qobuz_playlist_id",
|
||||
)
|
||||
.map_err(|e| LibraryError::Database(format!("Failed to prepare query: {}", e)))?;
|
||||
|
||||
let plex_rows = plex_stmt
|
||||
.query_map([], |row| {
|
||||
let playlist_id: i64 = row.get(0)?;
|
||||
let count: u32 = row.get(1)?;
|
||||
Ok((playlist_id as u64, count))
|
||||
})
|
||||
.map_err(|e| LibraryError::Database(format!("Failed to query: {}", e)))?;
|
||||
|
||||
for row in plex_rows {
|
||||
let (playlist_id, count) =
|
||||
row.map_err(|e| LibraryError::Database(format!("Failed to read row: {}", e)))?;
|
||||
*result.entry(playlist_id).or_insert(0) += count;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user