Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# AGENTS.md

This file provides context for AI coding agents working in this repository.

## Project Overview

**spotify-web-api-java** is a Java wrapper library for the [Spotify Web API](https://developer.spotify.com/documentation/web-api/). It provides typed request builders and model objects for all Spotify API endpoints.

- **Group ID:** `se.michaelthelin.spotify`
- **Artifact ID:** `spotify-web-api-java`
- **Current Version:** `10.0.0-RC1`
- **Build Tool:** Maven (`pom.xml`)
- **Java Version:** 17+

## Repository Structure

```
src/
├── main/java/se/michaelthelin/spotify/
│ ├── SpotifyApi.java # Main entry point – all endpoint builders exposed here
│ ├── SpotifyHttpManager.java # HTTP client wrapper
│ ├── enums/ # Enum types (ModelObjectType, etc.)
│ ├── exceptions/ # Exception hierarchy
│ ├── model_objects/ # POJOs for API response objects
│ │ ├── specification/ # Core model objects (Playlist, Track, Album, etc.)
│ │ ├── special/ # Composite/special models
│ │ └── miscellaneous/ # Supporting models
│ └── requests/
│ └── data/ # One subfolder per API category
│ ├── albums/
│ ├── artists/
│ ├── playlists/ # Playlist endpoint request classes
│ └── ...
└── test/
├── java/se/michaelthelin/spotify/
│ ├── ITest.java # Shared test constants (IDs, names, etc.)
│ ├── TestUtil.java # Mock HTTP manager helpers
│ └── requests/data/ # Unit tests mirroring main structure
└── fixtures/requests/data/ # JSON fixture files for mock HTTP responses
```

## Adding a New Endpoint

To add a new Spotify API endpoint, follow these steps:

### 1. Create a Request class

Create `src/main/java/se/michaelthelin/spotify/requests/data/<category>/<EndpointName>Request.java`.

Key patterns:
- Extend `AbstractDataRequest<ReturnType>` (or `AbstractDataPagingRequest` for paginated results).
- Annotate with `@JsonDeserialize(builder = <ClassName>.Builder.class)`.
- Implement `execute()` which calls `getJson()`, `postJson()`, `putJson()`, or `deleteJson()` as appropriate.
- Add a `static final class Builder` with:
- Path parameters set via `setPathParameter("key", value)`
- Query parameters set via `setQueryParameter("key", value)`
- Body parameters set via `setBodyParameter("key", value)`
- `build()` method that calls `setPath("/v1/...")` and `setContentType(...)` for POST/PUT requests
- `self()` returning `this`

**Endpoint URL reference:** Use the new `POST /v1/me/playlists` style paths (not the older `/v1/users/{user_id}/...` style where Spotify has migrated endpoints).

### 2. Expose the method in SpotifyApi

In `SpotifyApi.java`, add a public method that:
- Returns `<EndpointName>Request.Builder`
- Creates a new builder via `new <EndpointName>Request.Builder(accessToken)`
- Chains `.setDefaults(httpManager, scheme, host, port)`
- Sets any required path parameters

Methods are grouped by API category (albums, artists, playlists, etc.).

### 3. Add a fixture file

Create `src/test/fixtures/requests/data/<category>/<EndpointName>Request.json` with a sample API response.

### 4. Add a test class

Create `src/test/java/se/michaelthelin/spotify/requests/data/<category>/<EndpointName>RequestTest.java`.

Extend `AbstractDataTest<ReturnType>`. Tests should:
- Verify the built URI using `assertEquals("https://api.spotify.com:443/v1/...", request.getUri().toString())`
- Verify headers with `assertHasHeader(...)` and `assertHasAuthorizationHeader(...)`
- Verify body parameters with `assertHasBodyParameter(...)` (from `se.michaelthelin.spotify.Assertions`)
- Verify the deserialized response via `shouldReturnDefault(...)` for both sync and async execution

Common test constants are in `ITest.java` (e.g., `ITest.NAME`, `ITest.ID_PLAYLIST`, `ITest.PUBLIC`).

## Building and Testing

```bash
# Compile
mvn compile

# Run all tests
mvn test

# Run a specific test class
mvn test -Dtest="CreatePlaylistRequestTest"

# Run tests matching a pattern
mvn test -Dtest="*Playlist*"
```

## Key Conventions

- **Trailing underscores** on Java-reserved words: e.g., `public_` for the `public` field.
- **Assertions** in tests are imported from both JUnit 5 (`org.junit.jupiter.api.Assertions`) and the project's own `se.michaelthelin.spotify.Assertions`.
- **Nullable fields** from the API should be tested with `assertNull(...)` when the fixture returns `null`.
- **POST/PUT** requests must set `ContentType.APPLICATION_JSON` in the `build()` method.
- **Path patterns** use `{param_name}` placeholders set with `setPathParameter("param_name", value)`.
8 changes: 8 additions & 0 deletions jitpack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
jdk:
- openjdk17
before_install:
- sdk install java 17.0.18-tem
- sdk use java 17.0.18-tem
- sdk install maven
install:
- mvn install -Dmaven.javadoc.skip=true -DskipTests
12 changes: 12 additions & 0 deletions src/main/java/se/michaelthelin/spotify/SpotifyApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,18 @@ public ChangePlaylistsDetailsRequest.Builder changePlaylistsDetails(String playl
.playlist_id(playlist_id);
}

/**
* Create a playlist for the current Spotify user.
*
* @param name The name for the new playlist.
* @return A {@link CreatePlaylistRequest.Builder}.
*/
public CreatePlaylistRequest.Builder createPlaylist(String name) {
return new CreatePlaylistRequest.Builder(accessToken)
.setDefaults(httpManager, scheme, host, port)
.name(name);
}

/**
* Get a list of the playlists owned or followed by the current Spotify user.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package se.michaelthelin.spotify.requests.data.playlists;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.ParseException;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.specification.Playlist;
import se.michaelthelin.spotify.requests.data.AbstractDataRequest;

import java.io.IOException;

/**
* Create a playlist for the current Spotify user. (The playlist will be empty until you add tracks.)
* Each user is generally limited to a maximum of 11000 playlists.
*/
@JsonDeserialize(builder = CreatePlaylistRequest.Builder.class)
public class CreatePlaylistRequest extends AbstractDataRequest<Playlist> {

/**
* The private {@link CreatePlaylistRequest} constructor.
*
* @param builder A {@link CreatePlaylistRequest.Builder}.
*/
private CreatePlaylistRequest(final Builder builder) {
super(builder);
}

/**
* Create a new playlist.
*
* @return The newly created {@link Playlist}.
* @throws IOException In case of networking issues.
* @throws SpotifyWebApiException The Web API returned an error further specified in this exception's root cause.
*/
public Playlist execute() throws
IOException,
SpotifyWebApiException,
ParseException {
return new Playlist.JsonUtil().createModelObject(postJson());
}

/**
* Builder class for building a {@link CreatePlaylistRequest}.
*/
public static final class Builder extends AbstractDataRequest.Builder<Playlist, Builder> {

/**
* Create a new {@link CreatePlaylistRequest.Builder}.
* <p>
* Creating a public playlist for a user requires authorization of the {@code playlist-modify-public}
* scope; creating a private playlist requires the {@code playlist-modify-private} scope.
*
* @param accessToken Required. A valid access token from the Spotify Accounts service.
* @see <a href="https://developer.spotify.com/documentation/web-api/concepts/scopes">Spotify: Using Scopes</a>
*/
public Builder(final String accessToken) {
super(accessToken);
}

/**
* The playlist name setter.
*
* @param name Required. The name for the new playlist, for example {@code "Your Coolest Playlist"}.
* This name does not need to be unique; a user may have several playlists with the same name.
* @return A {@link CreatePlaylistRequest.Builder}.
*/
public Builder name(final String name) {
assert (name != null);
assert (!name.isEmpty());
return setBodyParameter("name", name);
}

/**
* The public status setter.
*
* @param public_ Optional. Defaults to {@code true}. If {@code true} the playlist will be public, if
* {@code false} it will be private. To be able to create private playlists, the user must
* have granted the {@code playlist-modify-private} scope.
* @return A {@link CreatePlaylistRequest.Builder}.
*/
public Builder public_(final Boolean public_) {
return setBodyParameter("public", public_);
}

/**
* The collaborative state setter.
*
* @param collaborative Optional. Defaults to {@code false}. If {@code true} the playlist will be collaborative.
* <b>Note:</b> To create a collaborative playlist you must also set {@code public} to
* {@code false}. To create collaborative playlists you must have granted
* {@code playlist-modify-private} and {@code playlist-modify-public} scopes.
* @return A {@link CreatePlaylistRequest.Builder}.
*/
public Builder collaborative(final Boolean collaborative) {
return setBodyParameter("collaborative", collaborative);
}

/**
* The playlist description setter.
*
* @param description Optional. Value for playlist description as displayed in Spotify Clients and in the Web API.
* @return A {@link CreatePlaylistRequest.Builder}.
*/
public Builder description(final String description) {
assert (description != null);
assert (!description.isEmpty());
return setBodyParameter("description", description);
}

/**
* The request build method.
*
* @return A custom {@link CreatePlaylistRequest}.
*/
@Override
public CreatePlaylistRequest build() {
setContentType(ContentType.APPLICATION_JSON);
setPath("/v1/me/playlists");
return new CreatePlaylistRequest(this);
}

@Override
protected Builder self() {
return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"collaborative": false,
"description": "New playlist description",
"external_urls": {
"spotify": "https://open.spotify.com/playlist/3cEYpjA9oz9GiPac4AsH4n"
},
"href": "https://api.spotify.com/v1/playlists/3cEYpjA9oz9GiPac4AsH4n",
"id": "3cEYpjA9oz9GiPac4AsH4n",
"images": [],
"name": "New Playlist",
"owner": {
"external_urls": {
"spotify": "https://open.spotify.com/user/jmperezperez"
},
"href": "https://api.spotify.com/v1/users/jmperezperez",
"id": "jmperezperez",
"type": "user",
"uri": "spotify:user:jmperezperez"
},
"public": false,
"snapshot_id": "JbtmHBDBAkMzFjnFzSP0aeYCCMP1XSIY5VHZT_jUGrFTzNTa6tnPiSzeBMFIcH2",
"items": {
"href": "https://api.spotify.com/v1/playlists/3cEYpjA9oz9GiPac4AsH4n/tracks",
"limit": 100,
"next": null,
"offset": 0,
"previous": null,
"total": 0,
"items": []
},
"tracks": {
"href": "https://api.spotify.com/v1/playlists/3cEYpjA9oz9GiPac4AsH4n/tracks",
"limit": 100,
"next": null,
"offset": 0,
"previous": null,
"total": 0,
"items": []
},
"type": "playlist",
"uri": "spotify:playlist:3cEYpjA9oz9GiPac4AsH4n"
}
Loading
Loading