Playlist Editing
Relevant source files
The following files were used as context for generating this wiki page:
- Sources/Spotty/Spotify/PlaylistChanges.swift
- Sources/Spotty/Spotify/PlaylistMutationController.swift
- Sources/SpottyDomain/PlaylistEditability.swift
- Sources/Spotty/Spotify/PathfinderPlaylist.swift
- Sources/Spotty/Spotify/PlaylistMutating.swift
- Sources/Spotty/Spotify/PartnerAPI.swift
- Tests/SpottyBoundaryTests/PlaylistMutationChecks.swift
Playlist Editing in Spotty enables users to modify their owned Spotify playlists by adding tracks, removing specific occurrences, or creating new playlists. The system enforces strict ownership checks to ensure users only modify content they have permission to edit. It coordinates between the UI, the domain logic for editability, and the Spotify backend services.
The system uses two primary communication methods for mutations:
- GraphQL Persisted Queries: Used via the
PartnerAPIfor most high-level mutations like adding items. - Protobuf over JSON: Used via the
playlist/v2service for library membership and attribute updates.
Sources: Sources/Spotty/Spotify/PlaylistChanges.swift:20-30, Sources/Spotty/Spotify/PlaylistMutationController.swift:15-25
Architecture and Control Flow
The PlaylistMutationController coordinates all playlist modifications. It acts as the gatekeeper for mutations, ensuring session availability and valid track selection before calling the backend.
Mutation Lifecycle
- Validation: The controller checks if the target playlist is editable and if the Spotify session is connected.
- Selection Preparation:
PlaylistMutationSelectionextracts the necessary identifiers (URIs for adding, UIDs for removing). - Execution: The
startMutationfunction wraps the async work in aTask, managing request scoping to handle superseded requests. - Reconciliation: Upon success, the controller forces the
PlaylistStoreto reload the modified playlist. - Feedback: The
TransientFeedbackPresenterdisplays success or failure messages to the user.
Sources: Sources/Spotty/Spotify/PlaylistMutationController.swift:65-120
The diagram above shows the typical sequence for adding tracks to an owned playlist, including the final store reconciliation.
Editability and Ownership
Spotty only permits edits when the signed-in profile URI matches the playlist owner URI. This prevents unauthorized write attempts that would result in API rejections.
| Component | Function | Description |
|---|---|---|
PlaylistEditability | canJustifyEdit | Compares playlistOwnerURI and profileURI using normalized user URIs. |
PlaylistEditability | userURI | Synthesizes a canonical spotify:user:id string from raw URI or username. |
PlaylistMutationSelection | occurrenceIDsForRemoval | Extracts Pathfinder uid strings, which identify specific track occurrences. |
Sources: Sources/SpottyDomain/PlaylistEditability.swift:10-50
URI Normalization
Ownership checks require normalized URIs to prevent case-sensitivity issues or malformed strings from blocking legitimate edits. The normalizeUserURI function trims whitespace and ensures the spotify:user: prefix is present.
Sources: Sources/SpottyDomain/PlaylistEditability.swift:20-35
This flow diagram illustrates the logic used by PlaylistEditability to justify a write operation.
Data Structures and Protobuf Mapping
Playlist operations are modeled as PlaylistOp structures. These are protobuf messages sent as JSON to spclient.wg.spotify.com/playlist/v2.
Core Operations
- UPDATE_LIST_ATTRIBUTES: Used for renaming or changing the description. Empty strings clear descriptions, while
nilvalues are omitted by the encoder. - ADD: Adds items to a list. The
addFirstflag (defaulttrue) places new items at the top of the library. - REM: Removes items. The
itemsAsKeyflag ensures the service drops the specific item rather than whatever sits at a specific index.
Sources: Sources/Spotty/Spotify/PlaylistChanges.swift:50-95
Identifiers: URI vs UID
A critical distinction in the Spotify playlist protocol is between a track's uri and a playlist occurrence's uid.
- URI: Identifies the song globally (e.g.,
spotify:track:abc). Adding items uses URIs. - UID: Identifies a specific occurrence of a song in a playlist. Removing or moving items requires UIDs to handle cases where a song appears multiple times.
Sources: Sources/Spotty/Spotify/PathfinderPlaylist.swift:75-85, Sources/SpottyDomain/PlaylistEditability.swift:65-75
Mutation Responses
The PathfinderMutationResponse handles the results of GraphQL-based writes. Because the Spotify API may return an HTTP 200 status for failed mutations, the system checks the __typename field to confirm success.
// Sources/Spotty/Spotify/PathfinderPlaylist.swift:129-138
private static let successTypes: Set<String> = [
"AddItemsToPlaylistPayload",
"RemoveItemsFromPlaylistPayload",
"MoveItemsInPlaylistPayload",
]
var failure: String? {
PathfinderMutationResult.failure(data?.result, unless: Self.successTypes)
}
Sources: Sources/Spotty/Spotify/PathfinderPlaylist.swift:125-145
Reconciling State
After a successful mutation, the application must synchronize its local state with the server. The PlaylistMutationController executes reconcileIfOpen, which triggers a forced load of the playlist if it is currently being viewed.
- Request Scoping: Each mutation increments a
requestScope. This prevents stale or superseded mutation tasks from updating the UI or presenting feedback for an outdated state. - Account Epochs: The system tracks an
accountEpoch. If the account session changes (e.g., re-authentication) during a mutation, the write is not applied to the UI.
Sources: Sources/Spotty/Spotify/PlaylistMutationController.swift:144-175, Tests/SpottyBoundaryTests/PlaylistMutationChecks.swift:420-440
Summary
Playlist editing in Spotty is managed by the PlaylistMutationController, which utilizes domain logic in PlaylistEditability for authorization. Mutations are executed via PartnerAPI or the playlist/v2 service using JSON-mapped Protobuf messages. Successful operations trigger a state reconciliation to ensure the user interface remains consistent with the Spotify backend, while request scoping ensures that only current and relevant feedback is presented.