From d2d6500e236069290939b7e913a8c23425c00b2f Mon Sep 17 00:00:00 2001 From: David Madl Date: Thu, 14 May 2026 12:26:49 +0200 Subject: [PATCH] fix: rename playlist tracks -> items --- .../jukebox/DefaultPlaylistRepository.java | 2 +- .../at/lockstep/jukebox/PlaylistSummary.java | 9 ++-- .../jukebox/api/dto/FullPlaylistDto.java | 3 +- .../jukebox/api/dto/PlaylistItemDto.java | 10 +++++ .../jukebox/api/dto/PlaylistItemsPageDto.java | 19 +++++++++ .../jukebox/api/dto/PlaylistTrackItemDto.java | 5 --- .../jukebox/api/dto/TracksPageDto.java | 14 ------- .../lockstep/jukebox/db/JukeboxDatabase.java | 2 +- .../at/lockstep/jukebox/db/PlaylistDao.java | 6 +-- .../lockstep/jukebox/db/PlaylistEntity.java | 34 ++++++++------- .../lockstep/jukebox/map/PlaylistMappers.java | 42 ++++++++++--------- .../jukebox/sync/SyncCoordinator.java | 16 +++++-- .../jukebox/sync/SyncCoordinatorTest.java | 14 +++---- schema/playlist_cache.sql | 8 ++-- 14 files changed, 103 insertions(+), 81 deletions(-) create mode 100644 jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistItemDto.java create mode 100644 jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistItemsPageDto.java delete mode 100644 jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistTrackItemDto.java delete mode 100644 jukebox/src/main/java/at/lockstep/jukebox/api/dto/TracksPageDto.java diff --git a/jukebox/src/main/java/at/lockstep/jukebox/DefaultPlaylistRepository.java b/jukebox/src/main/java/at/lockstep/jukebox/DefaultPlaylistRepository.java index 82c54dc..da40760 100644 --- a/jukebox/src/main/java/at/lockstep/jukebox/DefaultPlaylistRepository.java +++ b/jukebox/src/main/java/at/lockstep/jukebox/DefaultPlaylistRepository.java @@ -130,7 +130,7 @@ public final class DefaultPlaylistRepository implements PlaylistRepository { row.getPlaylist().getDescription(), row.getPlaylist().getPrimaryColor(), row.getPlaylist().getSnapshotId(), - row.getPlaylist().getTracksTotal(), + row.getPlaylist().getItemsTotal(), urls ); } diff --git a/jukebox/src/main/java/at/lockstep/jukebox/PlaylistSummary.java b/jukebox/src/main/java/at/lockstep/jukebox/PlaylistSummary.java index 82ac6cb..95821b5 100644 --- a/jukebox/src/main/java/at/lockstep/jukebox/PlaylistSummary.java +++ b/jukebox/src/main/java/at/lockstep/jukebox/PlaylistSummary.java @@ -2,8 +2,6 @@ package at.lockstep.jukebox; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import java.util.Collections; import java.util.List; public final class PlaylistSummary { @@ -18,8 +16,9 @@ public final class PlaylistSummary { public final String primaryColor; @NonNull public final String snapshotId; + /** Spotify simplified {@code tracks.total}, or full playlist {@code items.total} when synced. */ @Nullable - public final Integer tracksTotal; + public final Integer itemsTotal; @NonNull public final List imageUrls; @@ -29,7 +28,7 @@ public final class PlaylistSummary { @Nullable String description, @Nullable String primaryColor, @NonNull String snapshotId, - @Nullable Integer tracksTotal, + @Nullable Integer itemsTotal, @NonNull List imageUrls ) { this.id = id; @@ -37,7 +36,7 @@ public final class PlaylistSummary { this.description = description; this.primaryColor = primaryColor; this.snapshotId = snapshotId; - this.tracksTotal = tracksTotal; + this.itemsTotal = itemsTotal; this.imageUrls = imageUrls; } diff --git a/jukebox/src/main/java/at/lockstep/jukebox/api/dto/FullPlaylistDto.java b/jukebox/src/main/java/at/lockstep/jukebox/api/dto/FullPlaylistDto.java index 736085d..fa47893 100644 --- a/jukebox/src/main/java/at/lockstep/jukebox/api/dto/FullPlaylistDto.java +++ b/jukebox/src/main/java/at/lockstep/jukebox/api/dto/FullPlaylistDto.java @@ -10,7 +10,8 @@ public class FullPlaylistDto { public List images; public String primaryColor; public String snapshotId; - public TracksPageDto tracks; + /** Paging object for playlist entries; Spotify key {@code items}. */ + public PlaylistItemsPageDto items; public List imagesOrEmpty() { return images != null ? images : Collections.emptyList(); diff --git a/jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistItemDto.java b/jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistItemDto.java new file mode 100644 index 0000000..16287d1 --- /dev/null +++ b/jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistItemDto.java @@ -0,0 +1,10 @@ +package at.lockstep.jukebox.api.dto; + +/** + * One element of {@link PlaylistItemsPageDto#items}. Spotify nests the track under {@code item} + * (not {@code track}). + */ +public class PlaylistItemDto { + /** Nested playable (track) from Spotify; {@code null} when removed or unsupported. */ + public TrackDto item; +} diff --git a/jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistItemsPageDto.java b/jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistItemsPageDto.java new file mode 100644 index 0000000..2519654 --- /dev/null +++ b/jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistItemsPageDto.java @@ -0,0 +1,19 @@ +package at.lockstep.jukebox.api.dto; + +import java.util.Collections; +import java.util.List; + +/** + * Spotify full-playlist paging object keyed {@code items} at the playlist root: + * {@code playlist.items.href}, {@code playlist.items.total}, {@code playlist.items.items[]}. + */ +public class PlaylistItemsPageDto { + public String href; + public Integer total; + /** Playlist entries ({@link PlaylistItemDto}) in API order. */ + public List items; + + public List itemsOrEmpty() { + return items != null ? items : Collections.emptyList(); + } +} diff --git a/jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistTrackItemDto.java b/jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistTrackItemDto.java deleted file mode 100644 index d5c1cc0..0000000 --- a/jukebox/src/main/java/at/lockstep/jukebox/api/dto/PlaylistTrackItemDto.java +++ /dev/null @@ -1,5 +0,0 @@ -package at.lockstep.jukebox.api.dto; - -public class PlaylistTrackItemDto { - public TrackDto track; -} diff --git a/jukebox/src/main/java/at/lockstep/jukebox/api/dto/TracksPageDto.java b/jukebox/src/main/java/at/lockstep/jukebox/api/dto/TracksPageDto.java deleted file mode 100644 index c417053..0000000 --- a/jukebox/src/main/java/at/lockstep/jukebox/api/dto/TracksPageDto.java +++ /dev/null @@ -1,14 +0,0 @@ -package at.lockstep.jukebox.api.dto; - -import java.util.Collections; -import java.util.List; - -public class TracksPageDto { - public String href; - public Integer total; - public List items; - - public List itemsOrEmpty() { - return items != null ? items : Collections.emptyList(); - } -} diff --git a/jukebox/src/main/java/at/lockstep/jukebox/db/JukeboxDatabase.java b/jukebox/src/main/java/at/lockstep/jukebox/db/JukeboxDatabase.java index 4e43fe3..42cf769 100644 --- a/jukebox/src/main/java/at/lockstep/jukebox/db/JukeboxDatabase.java +++ b/jukebox/src/main/java/at/lockstep/jukebox/db/JukeboxDatabase.java @@ -14,7 +14,7 @@ import androidx.room.RoomDatabase; TrackEntity.class, PlaylistTrackEntity.class }, - version = 1, + version = 2, exportSchema = false ) public abstract class JukeboxDatabase extends RoomDatabase { diff --git a/jukebox/src/main/java/at/lockstep/jukebox/db/PlaylistDao.java b/jukebox/src/main/java/at/lockstep/jukebox/db/PlaylistDao.java index b4f39ac..c0eb8fa 100644 --- a/jukebox/src/main/java/at/lockstep/jukebox/db/PlaylistDao.java +++ b/jukebox/src/main/java/at/lockstep/jukebox/db/PlaylistDao.java @@ -29,7 +29,7 @@ public abstract class PlaylistDao { public void replacePlaylistContent( @NonNull PlaylistEntity playlist, @NonNull List images, - @NonNull List tracks, + @NonNull List trackEntities, @NonNull List playlistTracks ) { upsertPlaylist(playlist); @@ -38,8 +38,8 @@ public abstract class PlaylistDao { insertImages(images); } deletePlaylistTracksForPlaylist(playlist.getId()); - if (!tracks.isEmpty()) { - upsertTracks(tracks); + if (!trackEntities.isEmpty()) { + upsertTracks(trackEntities); } if (!playlistTracks.isEmpty()) { insertPlaylistTracks(playlistTracks); diff --git a/jukebox/src/main/java/at/lockstep/jukebox/db/PlaylistEntity.java b/jukebox/src/main/java/at/lockstep/jukebox/db/PlaylistEntity.java index 7210f86..49d1b5d 100644 --- a/jukebox/src/main/java/at/lockstep/jukebox/db/PlaylistEntity.java +++ b/jukebox/src/main/java/at/lockstep/jukebox/db/PlaylistEntity.java @@ -27,13 +27,15 @@ public class PlaylistEntity { @ColumnInfo(name = "snapshot_id") private String snapshotId; + /** From Spotify paging {@code playlist.items.href} (full playlist) or simplified {@code tracks.href}. */ @Nullable - @ColumnInfo(name = "tracks_href") - private String tracksHref; + @ColumnInfo(name = "items_href") + private String itemsHref; + /** Total count from paging object {@code playlist.items.total} or simplified {@code tracks.total}. */ @Nullable - @ColumnInfo(name = "tracks_total") - private Integer tracksTotal; + @ColumnInfo(name = "items_total") + private Integer itemsTotal; public PlaylistEntity( @NonNull String id, @@ -41,16 +43,16 @@ public class PlaylistEntity { @NonNull String name, @Nullable String primaryColor, @NonNull String snapshotId, - @Nullable String tracksHref, - @Nullable Integer tracksTotal + @Nullable String itemsHref, + @Nullable Integer itemsTotal ) { this.id = id; this.description = description; this.name = name; this.primaryColor = primaryColor; this.snapshotId = snapshotId; - this.tracksHref = tracksHref; - this.tracksTotal = tracksTotal; + this.itemsHref = itemsHref; + this.itemsTotal = itemsTotal; } @NonNull @@ -99,20 +101,20 @@ public class PlaylistEntity { } @Nullable - public String getTracksHref() { - return tracksHref; + public String getItemsHref() { + return itemsHref; } - public void setTracksHref(@Nullable String tracksHref) { - this.tracksHref = tracksHref; + public void setItemsHref(@Nullable String itemsHref) { + this.itemsHref = itemsHref; } @Nullable - public Integer getTracksTotal() { - return tracksTotal; + public Integer getItemsTotal() { + return itemsTotal; } - public void setTracksTotal(@Nullable Integer tracksTotal) { - this.tracksTotal = tracksTotal; + public void setItemsTotal(@Nullable Integer itemsTotal) { + this.itemsTotal = itemsTotal; } } diff --git a/jukebox/src/main/java/at/lockstep/jukebox/map/PlaylistMappers.java b/jukebox/src/main/java/at/lockstep/jukebox/map/PlaylistMappers.java index 17022b7..162f1fd 100644 --- a/jukebox/src/main/java/at/lockstep/jukebox/map/PlaylistMappers.java +++ b/jukebox/src/main/java/at/lockstep/jukebox/map/PlaylistMappers.java @@ -7,7 +7,7 @@ import androidx.annotation.NonNull; import at.lockstep.jukebox.api.dto.ArtistDto; import at.lockstep.jukebox.api.dto.FullPlaylistDto; import at.lockstep.jukebox.api.dto.ImageDto; -import at.lockstep.jukebox.api.dto.PlaylistTrackItemDto; +import at.lockstep.jukebox.api.dto.PlaylistItemDto; import at.lockstep.jukebox.api.dto.SimplifiedPlaylistDto; import at.lockstep.jukebox.api.dto.TrackDto; import at.lockstep.jukebox.db.PlaylistEntity; @@ -64,15 +64,16 @@ public final class PlaylistMappers { } /** - * Maps a playlist list entry to a {@link PlaylistEntity} without track items (shell row only). + * Maps a playlist list entry to a {@link PlaylistEntity} without playlist items loaded (shell row only). + * Uses simplified {@code tracks} stub ({@code href}, {@code total}). */ @NonNull public static PlaylistEntity toPlaylistEntity(@NonNull SimplifiedPlaylistDto dto) { if (dto.id == null || dto.id.isEmpty()) { throw new IllegalArgumentException("Simplified playlist missing id"); } - String tracksHref = dto.tracks != null ? dto.tracks.href : null; - Integer tracksTotal = dto.tracks != null ? dto.tracks.total : null; + String itemsHref = dto.tracks != null ? dto.tracks.href : null; + Integer itemsTotal = dto.tracks != null ? dto.tracks.total : null; String rawName = dto.name; boolean unnamed = rawName == null || rawName.trim().isEmpty(); String displayName = unnamed ? "Untitled playlist" : rawName; @@ -86,19 +87,20 @@ public final class PlaylistMappers { displayName, dto.primaryColor, snapshotId, - tracksHref, - tracksTotal + itemsHref, + itemsTotal ); } /** * Maps the playlist metadata from a full playlist response to a {@link PlaylistEntity} row. + * Uses root {@code items} paging object ({@code href}, {@code total}). * Empty or whitespace-only {@code name} becomes {@code "Untitled playlist"} and is logged for debugging. */ @NonNull public static PlaylistEntity toPlaylistEntity(@NonNull FullPlaylistDto dto) { - String tracksHref = dto.tracks != null ? dto.tracks.href : null; - Integer tracksTotal = dto.tracks != null ? dto.tracks.total : null; + String itemsHref = dto.items != null ? dto.items.href : null; + Integer itemsTotal = dto.items != null ? dto.items.total : null; String rawName = dto.name; boolean unnamed = rawName == null || rawName.trim().isEmpty(); String displayName = unnamed ? "Untitled playlist" : rawName; @@ -111,8 +113,8 @@ public final class PlaylistMappers { displayName, dto.primaryColor, dto.snapshotId != null ? dto.snapshotId : "", - tracksHref, - tracksTotal + itemsHref, + itemsTotal ); } @@ -130,12 +132,12 @@ public final class PlaylistMappers { for (int i = 0; i < imageDtos.size(); i++) { images.add(toImageEntity(imageDtos.get(i), dto.id, i)); } - List items = dto.tracks != null ? dto.tracks.itemsOrEmpty() : new ArrayList<>(); + List entries = dto.items != null ? dto.items.itemsOrEmpty() : new ArrayList<>(); Map trackById = new LinkedHashMap<>(); - for (PlaylistTrackItemDto wrapper : items) { - if (wrapper.track != null) { - TrackDto t = wrapper.track; + for (PlaylistItemDto wrapper : entries) { + if (wrapper.item != null) { + TrackDto t = wrapper.item; TrackEntity entity = new TrackEntity( t.id, t.name, @@ -148,9 +150,9 @@ public final class PlaylistMappers { List trackEntities = new ArrayList<>(trackById.values()); List playlistTrackEntities = new ArrayList<>(); - for (int i = 0; i < items.size(); i++) { - PlaylistTrackItemDto wrapper = items.get(i); - String tid = wrapper.track != null ? wrapper.track.id : null; + for (int i = 0; i < entries.size(); i++) { + PlaylistItemDto wrapper = entries.get(i); + String tid = wrapper.item != null ? wrapper.item.id : null; playlistTrackEntities.add(new PlaylistTrackEntity(dto.id, i, tid)); } return new PlaylistStorageRows(images, trackEntities, playlistTrackEntities); @@ -163,17 +165,17 @@ public final class PlaylistMappers { @NonNull public final List images; @NonNull - public final List tracks; + public final List trackEntities; @NonNull public final List playlistTracks; public PlaylistStorageRows( @NonNull List images, - @NonNull List tracks, + @NonNull List trackEntities, @NonNull List playlistTracks ) { this.images = images; - this.tracks = tracks; + this.trackEntities = trackEntities; this.playlistTracks = playlistTracks; } } diff --git a/jukebox/src/main/java/at/lockstep/jukebox/sync/SyncCoordinator.java b/jukebox/src/main/java/at/lockstep/jukebox/sync/SyncCoordinator.java index 5f019c4..ab03758 100644 --- a/jukebox/src/main/java/at/lockstep/jukebox/sync/SyncCoordinator.java +++ b/jukebox/src/main/java/at/lockstep/jukebox/sync/SyncCoordinator.java @@ -1,12 +1,16 @@ package at.lockstep.jukebox.sync; +import android.util.Log; + import androidx.annotation.NonNull; import at.lockstep.jukebox.api.LockstepApiException; import at.lockstep.jukebox.api.LockstepPlaylistClient; import at.lockstep.jukebox.api.dto.FullPlaylistDto; import at.lockstep.jukebox.api.dto.ImageDto; +import at.lockstep.jukebox.api.dto.PlaylistItemDto; import at.lockstep.jukebox.api.dto.SimplifiedPlaylistDto; +import at.lockstep.jukebox.api.dto.TrackDto; import at.lockstep.jukebox.db.PlaylistDao; import at.lockstep.jukebox.db.PlaylistEntity; import at.lockstep.jukebox.db.PlaylistImageEntity; @@ -20,6 +24,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; /** @@ -123,15 +128,18 @@ public final class SyncCoordinator { */ public void syncPlaylistDetail(@NonNull String playlistId) throws IOException, LockstepApiException { FullPlaylistDto detail = remote.fetchPlaylistDetail(playlistId); - persistFullPlaylist(detail); + PlaylistMappers.PlaylistStorageRows rows = PlaylistMappers.toPlaylistStorageRows(detail); + persistFullPlaylist(detail, rows); } - private void persistFullPlaylist(@NonNull FullPlaylistDto detail) { - PlaylistMappers.PlaylistStorageRows rows = PlaylistMappers.toPlaylistStorageRows(detail); + private void persistFullPlaylist( + @NonNull FullPlaylistDto detail, + @NonNull PlaylistMappers.PlaylistStorageRows rows + ) { dao.replacePlaylistContent( PlaylistMappers.toPlaylistEntity(detail), rows.images, - rows.tracks, + rows.trackEntities, rows.playlistTracks ); } diff --git a/jukebox/src/test/java/at/lockstep/jukebox/sync/SyncCoordinatorTest.java b/jukebox/src/test/java/at/lockstep/jukebox/sync/SyncCoordinatorTest.java index 8e6ba6f..d097b53 100644 --- a/jukebox/src/test/java/at/lockstep/jukebox/sync/SyncCoordinatorTest.java +++ b/jukebox/src/test/java/at/lockstep/jukebox/sync/SyncCoordinatorTest.java @@ -17,10 +17,10 @@ import at.lockstep.jukebox.api.LockstepApiException; import at.lockstep.jukebox.api.LockstepPlaylistClient; import at.lockstep.jukebox.api.dto.FullPlaylistDto; import at.lockstep.jukebox.api.dto.ImageDto; -import at.lockstep.jukebox.api.dto.PlaylistTrackItemDto; +import at.lockstep.jukebox.api.dto.PlaylistItemsPageDto; +import at.lockstep.jukebox.api.dto.PlaylistItemDto; import at.lockstep.jukebox.api.dto.SimplifiedPlaylistDto; import at.lockstep.jukebox.api.dto.TrackDto; -import at.lockstep.jukebox.api.dto.TracksPageDto; import at.lockstep.jukebox.db.JukeboxDatabase; import at.lockstep.jukebox.db.TrackRow; import at.lockstep.jukebox.map.PlaylistMappers; @@ -119,7 +119,7 @@ public class SyncCoordinatorTest { db.playlistDao().replacePlaylistContent( PlaylistMappers.toPlaylistEntity(orphan), rows.images, - rows.tracks, + rows.trackEntities, rows.playlistTracks ); coordinator.syncDelta(true); @@ -159,16 +159,16 @@ public class SyncCoordinatorTest { d.snapshotId = snap; d.images = List.of(new ImageDto()); d.images.get(0).url = "https://x.example/a.png"; - TracksPageDto page = new TracksPageDto(); - PlaylistTrackItemDto item = new PlaylistTrackItemDto(); + PlaylistItemsPageDto page = new PlaylistItemsPageDto(); + PlaylistItemDto item = new PlaylistItemDto(); TrackDto t = new TrackDto(); t.id = trackId; t.name = trackName; t.durationMs = 1000; - item.track = t; + item.item = t; page.items = new ArrayList<>(); page.items.add(item); - d.tracks = page; + d.items = page; return d; } diff --git a/schema/playlist_cache.sql b/schema/playlist_cache.sql index 5917e47..aee7e4d 100644 --- a/schema/playlist_cache.sql +++ b/schema/playlist_cache.sql @@ -11,8 +11,8 @@ CREATE TABLE playlists ( name TEXT NOT NULL, primary_color TEXT, snapshot_id TEXT NOT NULL, - tracks_href TEXT, - tracks_total INTEGER + items_href TEXT, + items_total INTEGER ); CREATE TABLE playlist_images ( @@ -33,8 +33,8 @@ CREATE TABLE tracks ( duration_ms INTEGER NOT NULL ); --- Order of tracks in a playlist (matches playlist.tracks.items[] order). --- track_id NULL when the API returns a removed track (wrapper with track: null). +-- Order of tracks in a playlist (matches playlist.items.items[] order). +-- track_id NULL when the API returns a removed entry (wrapper with item: null). CREATE TABLE playlist_tracks ( playlist_id TEXT NOT NULL REFERENCES playlists (id) ON DELETE CASCADE, position INTEGER NOT NULL,