Connected Devices: fixed rplaylist sync

This commit is contained in:
Ultradesu
2026-07-24 03:11:33 +03:00
parent 682794755e
commit e83f356005
+113 -48
View File
@@ -647,27 +647,59 @@ impl Library {
pub fn playlist(&self, id: i64) -> Result<PlaylistDetail> {
let conn = self.lock();
if id == LIKES_PLAYLIST_ID {
let mut tracks = query_tracks(
let local_tracks = query_tracks(
&conn,
&format!(
"SELECT {TRACK_COLUMNS} FROM tracks t
JOIN releases r ON r.id = t.release_id
JOIN likes k ON k.track_id = t.id
ORDER BY k.liked_at DESC"
JOIN likes k ON k.track_id = t.id"
),
params![],
)?;
drop(conn);
// Liked federated tracks follow the local ones as queueable
// placeholders (downloaded on playback like everywhere else).
for fed in self.fed_likes()? {
tracks.push(crate::federation::pending_track(&fed));
let mut liked_at_by_track = HashMap::new();
let mut liked_stmt = conn.prepare("SELECT track_id, liked_at FROM likes")?;
let liked_rows = liked_stmt.query_map([], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
})?;
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 {
id,
title: "Liked tracks".to_string(),
description: None,
tracks,
tracks: entries.into_iter().map(|(_, _, track)| track).collect(),
});
}
let (title, description, sync_id): (String, Option<String>, Option<String>) = conn
@@ -1154,45 +1186,6 @@ impl Library {
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).
pub fn fed_like_ids(&self) -> Result<Vec<String>> {
let conn = self.lock();
@@ -2088,6 +2081,78 @@ mod tests {
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]
fn synced_playlist_can_show_federated_pending_tracks() {
let lib = test_library();