Compare commits
2 Commits
8a7265194c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b6a0bb35a9 | |||
| d2d6500e23 |
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<String> 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<String> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ public class FullPlaylistDto {
|
||||
public List<ImageDto> images;
|
||||
public String primaryColor;
|
||||
public String snapshotId;
|
||||
public TracksPageDto tracks;
|
||||
/** Paging object for playlist entries; Spotify key {@code items}. */
|
||||
public PlaylistItemsPageDto items;
|
||||
|
||||
public List<ImageDto> imagesOrEmpty() {
|
||||
return images != null ? images : Collections.emptyList();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<PlaylistItemDto> items;
|
||||
|
||||
public List<PlaylistItemDto> itemsOrEmpty() {
|
||||
return items != null ? items : Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package at.lockstep.jukebox.api.dto;
|
||||
|
||||
public class PlaylistTrackItemDto {
|
||||
public TrackDto track;
|
||||
}
|
||||
@@ -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<PlaylistTrackItemDto> items;
|
||||
|
||||
public List<PlaylistTrackItemDto> itemsOrEmpty() {
|
||||
return items != null ? items : Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import androidx.room.RoomDatabase;
|
||||
TrackEntity.class,
|
||||
PlaylistTrackEntity.class
|
||||
},
|
||||
version = 1,
|
||||
version = 2,
|
||||
exportSchema = false
|
||||
)
|
||||
public abstract class JukeboxDatabase extends RoomDatabase {
|
||||
|
||||
@@ -29,7 +29,7 @@ public abstract class PlaylistDao {
|
||||
public void replacePlaylistContent(
|
||||
@NonNull PlaylistEntity playlist,
|
||||
@NonNull List<PlaylistImageEntity> images,
|
||||
@NonNull List<TrackEntity> tracks,
|
||||
@NonNull List<TrackEntity> trackEntities,
|
||||
@NonNull List<PlaylistTrackEntity> 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PlaylistTrackItemDto> items = dto.tracks != null ? dto.tracks.itemsOrEmpty() : new ArrayList<>();
|
||||
List<PlaylistItemDto> entries = dto.items != null ? dto.items.itemsOrEmpty() : new ArrayList<>();
|
||||
|
||||
Map<String, TrackEntity> 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<TrackEntity> trackEntities = new ArrayList<>(trackById.values());
|
||||
|
||||
List<PlaylistTrackEntity> 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<PlaylistImageEntity> images;
|
||||
@NonNull
|
||||
public final List<TrackEntity> tracks;
|
||||
public final List<TrackEntity> trackEntities;
|
||||
@NonNull
|
||||
public final List<PlaylistTrackEntity> playlistTracks;
|
||||
|
||||
public PlaylistStorageRows(
|
||||
@NonNull List<PlaylistImageEntity> images,
|
||||
@NonNull List<TrackEntity> tracks,
|
||||
@NonNull List<TrackEntity> trackEntities,
|
||||
@NonNull List<PlaylistTrackEntity> playlistTracks
|
||||
) {
|
||||
this.images = images;
|
||||
this.tracks = tracks;
|
||||
this.trackEntities = trackEntities;
|
||||
this.playlistTracks = playlistTracks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -37,6 +42,13 @@ import java.util.Set;
|
||||
*/
|
||||
public final class SyncCoordinator {
|
||||
|
||||
private static final String TAG = "SyncCoordinator";
|
||||
|
||||
/** Cap per-item title/artist lines so huge playlists do not overwhelm logcat. */
|
||||
private static final int SYNC_DETAIL_TITLE_LOG_LIMIT = 50;
|
||||
|
||||
private static final int SYNC_DETAIL_NULL_TRACK_LOG_LIMIT = 15;
|
||||
|
||||
private final PlaylistDao dao;
|
||||
private final LockstepPlaylistClient remote;
|
||||
|
||||
@@ -122,17 +134,91 @@ public final class SyncCoordinator {
|
||||
* that playlist only.
|
||||
*/
|
||||
public void syncPlaylistDetail(@NonNull String playlistId) throws IOException, LockstepApiException {
|
||||
Log.d(TAG, "syncPlaylistDetail start requestedId=" + playlistId);
|
||||
FullPlaylistDto detail = remote.fetchPlaylistDetail(playlistId);
|
||||
persistFullPlaylist(detail);
|
||||
String resolvedId = detail.id;
|
||||
Log.d(
|
||||
TAG,
|
||||
"syncPlaylistDetail fetched resolvedId="
|
||||
+ resolvedId
|
||||
+ " requestedMatch="
|
||||
+ Objects.equals(playlistId, resolvedId)
|
||||
+ " name="
|
||||
+ detail.name
|
||||
+ " snapshotId="
|
||||
+ detail.snapshotId
|
||||
);
|
||||
if (detail.items == null) {
|
||||
Log.w(TAG, "syncPlaylistDetail: detail.items is null (no playlist items page in response)");
|
||||
} else {
|
||||
List<PlaylistItemDto> entries = detail.items.itemsOrEmpty();
|
||||
int withItem = 0;
|
||||
int withoutItem = 0;
|
||||
int titlesLogged = 0;
|
||||
int nullItemLogged = 0;
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
PlaylistItemDto w = entries.get(i);
|
||||
if (w.item != null) {
|
||||
withItem++;
|
||||
if (titlesLogged < SYNC_DETAIL_TITLE_LOG_LIMIT) {
|
||||
TrackDto t = w.item;
|
||||
Log.d(
|
||||
TAG,
|
||||
"syncPlaylistDetail items[" + i + "] id=" + t.id
|
||||
+ " title=" + t.name
|
||||
+ " artist="
|
||||
+ PlaylistMappers.artistDisplayName(t.artistsOrEmpty()));
|
||||
titlesLogged++;
|
||||
}
|
||||
} else {
|
||||
withoutItem++;
|
||||
if (nullItemLogged < SYNC_DETAIL_NULL_TRACK_LOG_LIMIT) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"syncPlaylistDetail items[" + i + "] (null item; e.g. removed or unsupported type)");
|
||||
nullItemLogged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Integer total = detail.items.total;
|
||||
Log.d(
|
||||
TAG,
|
||||
"syncPlaylistDetail items page: entryCount="
|
||||
+ entries.size()
|
||||
+ " entriesWithItem="
|
||||
+ withItem
|
||||
+ " entriesWithNullItem="
|
||||
+ withoutItem
|
||||
+ " paging.total="
|
||||
+ total
|
||||
+ " titleArtistLinesLogged="
|
||||
+ titlesLogged
|
||||
+ (withItem > titlesLogged
|
||||
? " (cap=" + SYNC_DETAIL_TITLE_LOG_LIMIT + ", see SyncCoordinator constants)"
|
||||
: "")
|
||||
);
|
||||
}
|
||||
PlaylistMappers.PlaylistStorageRows rows = PlaylistMappers.toPlaylistStorageRows(detail);
|
||||
Log.d(
|
||||
TAG,
|
||||
"syncPlaylistDetail after map: dedupedTrackEntities="
|
||||
+ rows.trackEntities.size()
|
||||
+ " playlistTrackRows="
|
||||
+ rows.playlistTracks.size()
|
||||
);
|
||||
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
|
||||
);
|
||||
Log.d(TAG, "syncPlaylistDetail persisted replacePlaylistContent for id=" + detail.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user