Connected Devices: fixed rplaylist sync
This commit is contained in:
+113
-48
@@ -647,27 +647,59 @@ impl Library {
|
|||||||
pub fn playlist(&self, id: i64) -> Result<PlaylistDetail> {
|
pub fn playlist(&self, id: i64) -> Result<PlaylistDetail> {
|
||||||
let conn = self.lock();
|
let conn = self.lock();
|
||||||
if id == LIKES_PLAYLIST_ID {
|
if id == LIKES_PLAYLIST_ID {
|
||||||
let mut tracks = query_tracks(
|
let local_tracks = query_tracks(
|
||||||
&conn,
|
&conn,
|
||||||
&format!(
|
&format!(
|
||||||
"SELECT {TRACK_COLUMNS} FROM tracks t
|
"SELECT {TRACK_COLUMNS} FROM tracks t
|
||||||
JOIN releases r ON r.id = t.release_id
|
JOIN releases r ON r.id = t.release_id
|
||||||
JOIN likes k ON k.track_id = t.id
|
JOIN likes k ON k.track_id = t.id"
|
||||||
ORDER BY k.liked_at DESC"
|
|
||||||
),
|
),
|
||||||
params![],
|
params![],
|
||||||
)?;
|
)?;
|
||||||
drop(conn);
|
let mut liked_at_by_track = HashMap::new();
|
||||||
// Liked federated tracks follow the local ones as queueable
|
let mut liked_stmt = conn.prepare("SELECT track_id, liked_at FROM likes")?;
|
||||||
// placeholders (downloaded on playback like everywhere else).
|
let liked_rows = liked_stmt.query_map([], |row| {
|
||||||
for fed in self.fed_likes()? {
|
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
|
||||||
tracks.push(crate::federation::pending_track(&fed));
|
})?;
|
||||||
|
for row in liked_rows {
|
||||||
|
let (track_id, liked_at) = row?;
|
||||||
|
liked_at_by_track.insert(track_id, liked_at);
|
||||||
}
|
}
|
||||||
|
drop(liked_stmt);
|
||||||
|
|
||||||
|
let mut entries: Vec<(String, String, TrackItem)> = local_tracks
|
||||||
|
.into_iter()
|
||||||
|
.map(|track| {
|
||||||
|
let liked_at = liked_at_by_track
|
||||||
|
.get(&track.id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
(liked_at, track.title.clone(), track)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut fed_stmt = conn.prepare(
|
||||||
|
"SELECT liked_at, item_id, owner, title, artist_names, featured_artist_names,
|
||||||
|
year, duration_seconds, content_id, release_title, track_number, disc_number
|
||||||
|
FROM fed_likes",
|
||||||
|
)?;
|
||||||
|
let fed_rows = fed_stmt.query_map([], |row| {
|
||||||
|
Ok((row.get::<_, String>(0)?, fed_track_from_offset_row(row, 1)?))
|
||||||
|
})?;
|
||||||
|
for row in fed_rows {
|
||||||
|
let (liked_at, fed) = row?;
|
||||||
|
entries.push((
|
||||||
|
liked_at,
|
||||||
|
fed.title.clone(),
|
||||||
|
crate::federation::pending_track(&fed),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
entries.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| left.1.cmp(&right.1)));
|
||||||
return Ok(PlaylistDetail {
|
return Ok(PlaylistDetail {
|
||||||
id,
|
id,
|
||||||
title: "Liked tracks".to_string(),
|
title: "Liked tracks".to_string(),
|
||||||
description: None,
|
description: None,
|
||||||
tracks,
|
tracks: entries.into_iter().map(|(_, _, track)| track).collect(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let (title, description, sync_id): (String, Option<String>, Option<String>) = conn
|
let (title, description, sync_id): (String, Option<String>, Option<String>) = conn
|
||||||
@@ -1154,45 +1186,6 @@ impl Library {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Liked federated tracks, newest first, as playable references.
|
|
||||||
pub fn fed_likes(&self) -> Result<Vec<crate::federation::FedTrack>> {
|
|
||||||
let conn = self.lock();
|
|
||||||
let mut statement = conn.prepare(
|
|
||||||
"SELECT item_id, owner, title, artist_names, featured_artist_names,
|
|
||||||
year, duration_seconds, content_id, release_title, track_number, disc_number
|
|
||||||
FROM fed_likes ORDER BY liked_at DESC",
|
|
||||||
)?;
|
|
||||||
let rows = statement
|
|
||||||
.query_map([], |row| {
|
|
||||||
let artists: String = row.get(3)?;
|
|
||||||
Ok(crate::federation::FedTrack {
|
|
||||||
item_id: row.get(0)?,
|
|
||||||
owner: row.get(1)?,
|
|
||||||
own: false,
|
|
||||||
title: row.get(2)?,
|
|
||||||
artist_names: artists
|
|
||||||
.split("; ")
|
|
||||||
.filter(|name| !name.is_empty())
|
|
||||||
.map(str::to_string)
|
|
||||||
.collect(),
|
|
||||||
featured_artist_names: row
|
|
||||||
.get::<_, String>(4)?
|
|
||||||
.split("; ")
|
|
||||||
.filter(|name| !name.is_empty())
|
|
||||||
.map(str::to_string)
|
|
||||||
.collect(),
|
|
||||||
year: row.get(5)?,
|
|
||||||
duration_seconds: row.get::<_, Option<f64>>(6)?.map(|d| d.round() as i64),
|
|
||||||
content_id: row.get(7)?,
|
|
||||||
release_title: row.get(8)?,
|
|
||||||
track_number: row.get(9)?,
|
|
||||||
disc_number: row.get(10)?,
|
|
||||||
})
|
|
||||||
})?
|
|
||||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
|
||||||
Ok(rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Item ids and content ids of every liked federated track (for the ♥ markers).
|
/// Item ids and content ids of every liked federated track (for the ♥ markers).
|
||||||
pub fn fed_like_ids(&self) -> Result<Vec<String>> {
|
pub fn fed_like_ids(&self) -> Result<Vec<String>> {
|
||||||
let conn = self.lock();
|
let conn = self.lock();
|
||||||
@@ -2088,6 +2081,78 @@ mod tests {
|
|||||||
assert_eq!(lib.playlists().unwrap().len(), 1);
|
assert_eq!(lib.playlists().unwrap().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn likes_playlist_orders_local_and_federated_by_liked_at() {
|
||||||
|
let lib = test_library();
|
||||||
|
let old_id = add_track(&lib, "Old Local", "Artist", "Album");
|
||||||
|
let new_id = add_track(&lib, "New Local", "Artist", "Album");
|
||||||
|
let content_id = format!("b3:{}", "c".repeat(64));
|
||||||
|
let fed = crate::federation::FedTrack {
|
||||||
|
item_id: "fed_item_order".to_string(),
|
||||||
|
owner: "fed_owner_order".to_string(),
|
||||||
|
own: false,
|
||||||
|
title: "Middle Fed".to_string(),
|
||||||
|
artist_names: vec!["Remote Artist".to_string()],
|
||||||
|
featured_artist_names: Vec::new(),
|
||||||
|
year: Some(2026),
|
||||||
|
duration_seconds: Some(123),
|
||||||
|
content_id: Some(content_id),
|
||||||
|
release_title: Some("Remote Release".to_string()),
|
||||||
|
track_number: Some(1),
|
||||||
|
disc_number: Some(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(lib.toggle_like(old_id).unwrap());
|
||||||
|
assert!(lib.toggle_like(new_id).unwrap());
|
||||||
|
assert!(lib.toggle_fed_like(&fed).unwrap());
|
||||||
|
{
|
||||||
|
let conn = lib.lock();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE likes SET liked_at = ?2 WHERE track_id = ?1",
|
||||||
|
params![old_id, "2026-01-01 00:00:00"],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE likes SET liked_at = ?2 WHERE track_id = ?1",
|
||||||
|
params![new_id, "2026-01-02 00:00:00"],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE fed_likes SET liked_at = ?2 WHERE item_id = ?1",
|
||||||
|
params![fed.item_id, "2026-01-03 00:00:00"],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let titles: Vec<String> = lib
|
||||||
|
.playlist(LIKES_PLAYLIST_ID)
|
||||||
|
.unwrap()
|
||||||
|
.tracks
|
||||||
|
.into_iter()
|
||||||
|
.map(|track| track.title)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(titles, vec!["Middle Fed", "New Local", "Old Local"]);
|
||||||
|
|
||||||
|
assert!(!lib.toggle_like(old_id).unwrap());
|
||||||
|
assert!(lib.toggle_like(old_id).unwrap());
|
||||||
|
{
|
||||||
|
let conn = lib.lock();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE likes SET liked_at = ?2 WHERE track_id = ?1",
|
||||||
|
params![old_id, "2026-01-04 00:00:00"],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let titles: Vec<String> = lib
|
||||||
|
.playlist(LIKES_PLAYLIST_ID)
|
||||||
|
.unwrap()
|
||||||
|
.tracks
|
||||||
|
.into_iter()
|
||||||
|
.map(|track| track.title)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(titles, vec!["Old Local", "Middle Fed", "New Local"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn synced_playlist_can_show_federated_pending_tracks() {
|
fn synced_playlist_can_show_federated_pending_tracks() {
|
||||||
let lib = test_library();
|
let lib = test_library();
|
||||||
|
|||||||
Reference in New Issue
Block a user