feat(session): round-trip streamable, parental_warning, source_item_id_hint (#315)

The PersistedQueueTrack struct already had streamable and parental_warning
fields but the load path hardcoded streamable=true and parental_warning
=false because the columns didn't exist in queue_tracks. source_item_id
_hint was missing entirely.

Adds the three columns via the same idempotent ALTER-TABLE pattern the
existing migrations use, persists them in save_session, and reads them in
load_session. PersistedQueueTrack gains source_item_id_hint as an
Option<String> with serde default so saves from older builds deserialize
cleanly.

User-visible effect: a restored queue now keeps explicit-content badges,
unstreamable visual state, and Mixtape/Collection source-item ids
matching the live queue instead of resetting to defaults at every
launch. Backwards-compatible: columns get safe defaults for older rows
(streamable=1, parental_warning=0, hint=NULL) so users upgrading from
1.2.12 don't see a regression.

cc
This commit is contained in:
vicrodh committed 2026-05-13 12:41:58 -06:00
1 parent 7284568c40
commit 267440f541
3 files changed
+55 -5

No files matched your search

+39 -5
View File
@@ -31,6 +31,12 @@ pub struct PersistedQueueTrack {
pub source: Option<String>,
#[serde(default)]
pub parental_warning: bool,
/// Opaque id of the Mixtape/Collection item that produced this track.
/// Persisted so a Mixtape-sourced queue restored from disk keeps the
/// item association the live queue would have (album_id is the safe
/// fallback for non-Mixtape paths; None reads fine downstream).
#[serde(default)]
pub source_item_id_hint: Option<String>,
}
/// Represents the full persisted session state
@@ -195,6 +201,30 @@ impl SessionStore {
);
}
// streamable / parental_warning / source_item_id_hint were hardcoded
// to defaults on load (streamable=true, parental_warning=false,
// source_item_id_hint=None) because the columns didn't exist.
// Persisting them round-trips the live QueueTrack faithfully so
// a restored queue matches what the user had open before close.
let has_streamable: bool = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('queue_tracks') WHERE name = 'streamable'",
[],
|row| row.get(0),
)
.unwrap_or(0)
> 0;
if !has_streamable {
let _ = conn.execute_batch(
"
ALTER TABLE queue_tracks ADD COLUMN streamable INTEGER NOT NULL DEFAULT 1;
ALTER TABLE queue_tracks ADD COLUMN parental_warning INTEGER NOT NULL DEFAULT 0;
ALTER TABLE queue_tracks ADD COLUMN source_item_id_hint TEXT;
",
);
}
// Add last_view, view_context_id, view_context_type columns to player_state
let has_last_view: bool = conn
.query_row(
@@ -239,8 +269,8 @@ impl SessionStore {
// Insert queue tracks
for (pos, track) in session.queue_tracks.iter().enumerate() {
if let Err(e) = self.conn.execute(
"INSERT INTO queue_tracks (position, track_id, title, artist, album, duration_secs, artwork_url, hires, bit_depth, sample_rate, is_local, album_id, artist_id, source)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
"INSERT INTO queue_tracks (position, track_id, title, artist, album, duration_secs, artwork_url, hires, bit_depth, sample_rate, is_local, album_id, artist_id, source, streamable, parental_warning, source_item_id_hint)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)",
params![
pos as i64,
track.id as i64,
@@ -256,6 +286,9 @@ impl SessionStore {
track.album_id,
track.artist_id.map(|v| v as i64),
track.source,
track.streamable as i64,
track.parental_warning as i64,
track.source_item_id_hint,
],
) {
let _ = self.conn.execute("ROLLBACK", []);
@@ -327,7 +360,7 @@ impl SessionStore {
// Load queue tracks
let mut stmt = self.conn
.prepare("SELECT track_id, title, artist, album, duration_secs, artwork_url, hires, bit_depth, sample_rate, is_local, album_id, artist_id, source FROM queue_tracks ORDER BY position")
.prepare("SELECT track_id, title, artist, album, duration_secs, artwork_url, hires, bit_depth, sample_rate, is_local, album_id, artist_id, source, streamable, parental_warning, source_item_id_hint FROM queue_tracks ORDER BY position")
.map_err(|e| format!("Failed to prepare queue query: {}", e))?;
let tracks: Vec<PersistedQueueTrack> = stmt
@@ -345,9 +378,10 @@ impl SessionStore {
is_local: row.get::<_, i64>(9).unwrap_or(0) != 0,
album_id: row.get(10)?,
artist_id: row.get::<_, Option<i64>>(11)?.map(|v| v as u64),
streamable: true, // Default to true for persisted tracks
source: row.get(12)?,
parental_warning: false, // Not persisted in DB
streamable: row.get::<_, i64>(13).unwrap_or(1) != 0,
parental_warning: row.get::<_, i64>(14).unwrap_or(0) != 0,
source_item_id_hint: row.get(15)?,
})
})
.map_err(|e| format!("Failed to query queue tracks: {}", e))?
+6
View File
@@ -26,6 +26,12 @@ export interface PersistedQueueTrack {
// v2_play_track (Qobuz) instead of v2_library_play_track. Backend also
// keeps this in session_store; this interface just needs to carry it.
source?: string | null;
// Round-tripped fields, previously hardcoded on load. Persisting them
// means a restored queue matches the live one for explicit-content
// badges, unstreamable visual state, and Mixtape source association.
streamable?: boolean;
parental_warning?: boolean;
source_item_id_hint?: string | null;
}
export interface PersistedSession {
+10
View File
@@ -4200,6 +4200,9 @@
// older versions that didn't persist source. Without this, a
// restored local queue routed next-track auto-advance to Qobuz.
source: trk.source ?? (trk.is_local ? 'local' : undefined),
streamable: trk.streamable ?? true,
parental_warning: trk.parental_warning ?? false,
source_item_id_hint: trk.source_item_id_hint ?? null,
}));
await setQueue(tracks, session.current_index ?? 0, true);
@@ -4479,6 +4482,13 @@
// auto-advance routed to v2_play_track with library row ids — which
// Qobuz then "resolved" to whatever track happened to share the id.
source: track.source ?? (track.is_local ? 'local' : null),
// Round-trip explicit-content + unstreamable visual state + the
// Mixtape/Collection source-item id so a restored queue mirrors
// the live one. Backend defaults streamable=true, parental=false
// and source_item_id_hint=null for tracks saved by older builds.
streamable: track.streamable ?? true,
parental_warning: track.parental_warning ?? false,
source_item_id_hint: track.source_item_id_hint ?? null,
});
}